web-dev-qa-db-fra.com

Pandas: Savoir quand une opération affecte le cadre de données d'origine

J'adore les pandas et l'utilise depuis des années. Je suis assez confiant pour savoir comment sous-définir les cadres de données et gérer les vues par rapport aux copies de manière appropriée (bien que j'utilise beaucoup d'assertions pour en être sûr). Je sais également que SettingWithCopyWarning a suscité de nombreuses questions, par exemple: Comment gérer SettingWithCopyWarning dans les pandas? Et quelques excellents guides récents expliquant comment bien comprendre ce qui se passe, par exemple Comprendre SettingWithCopyWarning dans les pandas .

Mais je sais aussi des choses spécifiques comme la citation de cette réponse ne sont plus dans les documents les plus récents (0.22.0) et que beaucoup de choses ont été déconseillées au fil des ans (ce qui a conduit à un ancien SO inapproprié _ réponses), et que les choses sont continuer à changer .

Récemment, après avoir enseigné aux pandas à compléter les nouveaux venus avec des connaissances générales de base de Python sur, par exemple, éviter l’indexation chaînée (et utiliser .iloc/.loc), j’ai toujours eu du mal à fournir règles générales empiriques pour savoir quand il est important faire attention à la SettingWithCopyWarningpar exemple quand il est prudent de l'ignorer).

J'ai personnellement découvert que le modèle spécifique de sous-définition d'un cadre de données en fonction d'une règle (par exemple, une opération de découpage ou booléenne), puis de modifier ce sous-ensemble, indépendant du cadre de données d'origine, est une opération beaucoup plus courante que la méthode précédente. Docs suggèrent. Dans cette situation, nous souhaitons modifier la copie et non l'original et l'avertissement est source de confusion et d'effroi pour les nouveaux arrivants.

Je sais que ce n'est pas anodin de savoir à l'avance quand une vue ou une copie est renvoyée, par exemple.
_ (Quelles règles les Pandas utilisent-ils pour générer une vue ou une copie?

Vérifier si la trame de données est une copie ou une vue dans Pandas

Je cherche donc la réponse à une question plus générale (adaptée aux débutants): dans quels cas l'exécution d'une opération sur un cadre de données sous-défini affecte-t-elle le cadre de données d'origine à partir duquel il a été créé et quand sont-ils indépendants? .

J'ai créé quelques cas ci-dessous qui, à mon avis, semblent raisonnables, mais je ne suis pas sûr s'il me manque un "gotcha" ou s'il existe un moyen plus simple de penser/vérifier cela. J'espérais que quelqu'un pourrait confirmer que mes intuitions sur les cas d'utilisation suivants sont correctes, car elles concernent ma question ci-dessus.

import pandas as pd
df1 = pd.DataFrame({'A':[2,4,6,8,10],'B':[1,3,5,7,9],'C':[10,20,30,40,50]})

1) Attention: non
Original modifié: Non

# df1 will be unaffected because we use .copy() method explicitly 
df2 = df1.copy()
#
# Reference: docs
df2.iloc[0,1] = 100

2) Attention: oui (je ne comprends pas vraiment pourquoi)
Original modifié: Non

# df1 will be unaffected because .query() always returns a copy
#
# Reference:
# https://stackoverflow.com/a/23296545/8022335
df2 = df1.query('A < 10')
df2.iloc[0,1] = 100

3) Attention: oui
Original modifié: Non

# df1 will be unaffected because boolean indexing with .loc
# always returns a copy
#
# Reference:
# https://stackoverflow.com/a/17961468/8022335
df2 = df1.loc[df1['A'] < 10,:]
df2.iloc[0,1] = 100

4) Attention: non
Original modifié: Non

# df1 will be unaffected because list indexing with .loc (or .iloc)
# always returns a copy
#
# Reference:
# Same as 4)
df2 = df1.loc[[0,3,4],:]
df2.iloc[0,1] = 100

5) Attention: non
Original modifié: oui (déroutant pour les nouveaux arrivants mais est logique)

# df1 will be affected because scalar/slice indexing with .iloc/.loc
# always references the original dataframe, but may sometimes 
# provide a view and sometimes provide a copy
#
# Reference: docs
df2 = df1.loc[:10,:]
df2.iloc[0,1] = 100

tl; dr Lors de la création d'une nouvelle trame de données à partir de l'original, la modification de la nouvelle trame de données:
Change l'original lorsque scalar/slice indexing avec .loc/.iloc est utilisé pour créer le nouveau cadre de données .
Est-ce que pas modifiera l'original lorsque l'indexation booléenne avec .loc, .query() ou .copy() est utilisée pour créer le nouveau cadre de données

30
ejolly

C'est une partie quelque peu déroutante et même frustrante des pandas, mais la plupart du temps, vous ne devriez pas avoir à vous en préoccuper si vous suivez certaines règles simples de workflow. En particulier, notez qu'il n'y a que deux cas généraux ici lorsque vous avez deux cadres de données, l'un étant un sous-ensemble de l'autre.

C’est un cas où la règle du Zen of Python "Explicite est préférable à implicite" est une excellente directive à suivre.

Cas A: Les modifications apportées à df2 ne devraient PAS affecter df1

C'est trivial, bien sûr. Vous voulez deux images complètement indépendantes, vous devez donc explicitement faire une copie:

df2 = df1.copy()

Après cela, tout ce que vous faites pour df2 n'affecte que df2 et non pas df1 et vice versa.

Cas B: Les modifications apportées à df2 doivent également affecter df1

Dans ce cas, je ne pense pas qu'il existe un moyen général de résoudre le problème, car cela dépend de ce que vous essayez de faire. Cependant, il existe quelques approches standard qui sont assez simples et ne devraient pas avoir d'ambiguïté sur leur fonctionnement.

Méthode 1: Copiez df1 dans df2, puis utilisez df2 pour mettre à jour df1

Dans ce cas, vous pouvez effectuer une conversion un à un des exemples ci-dessus. Voici l'exemple n ° 2:

df2 = df1.copy()
df2 = df1.query('A < 10')
df2.iloc[0,1] = 100

df1 = df2.append(df1).reset_index().drop_duplicates(subset='index').drop(columns='index')

Malheureusement, la re-fusion via append est un peu commentée. Vous pouvez le faire plus proprement avec ce qui suit, bien que cela ait pour effet secondaire de convertir des entiers en flottants.

df1.update(df2)   # note that this is an inplace operation

Méthode 2: Utiliser un masque (ne pas créer du tout df2)

Je pense que la meilleure approche générale ici est de ne pas créer du tout df2, mais plutôt d’avoir une version masquée de df1. Malheureusement, vous ne pouvez pas traduire directement le code ci-dessus en raison de sa combinaison de loc et iloc, ce qui est bien pour cet exemple, bien que probablement irréaliste pour une utilisation réelle.

L'avantage est que vous pouvez écrire du code très simple et lisible. Voici une version alternative de l'exemple n ° 2 ci-dessus où df2 n'est en réalité qu'une version masquée de df1. Mais au lieu de changer via iloc, je changerai si la colonne "C" == 10.

df2_mask = df1['A'] < 10
df1.loc[ df2_mask & (df1['C'] == 10), 'B'] = 100

Maintenant, si vous imprimez df1 ou df1[df2_mask], vous verrez que la colonne "B" = 100 pour la première ligne de chaque trame de données. Évidemment, cela n’est pas très surprenant ici, mais c’est l’avantage inhérent de suivre "explicite, c’est mieux qu’implicite".

2
JohnE

J'ai le même doute, j'ai cherché cette réponse dans le passé sans succès. Alors maintenant, je certifie que l'original ne change pas et utilise cette paix de code dans le programme au début pour supprimer les avertissements:

 import pandas as pd
 pd.options.mode.chained_assignment = None  # default='warn'
0
romulomadu

Il vous suffit de remplacer .iloc[0,1] par .iat[0,1].

Plus généralement, si vous souhaitez modifier un seul élément, vous devez utiliser la méthode .iat ou .at. Au lieu de cela, lorsque vous modifiez plus d'éléments en même temps, vous devez utiliser les méthodes .loc ou .iloc.

De cette façon, les pandas ne devraient lancer aucun avertissement.

0
alububu