Comment gérer SettingWithCopyWarning dans Pandas?


630

Contexte

Je viens de mettre à jour mes Pandas de 0,11 à 0,13.0rc1. Maintenant, l'application fait apparaître de nombreux nouveaux avertissements. L'un d'eux aime ça:

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE

Je veux savoir ce que cela signifie exactement? Dois-je changer quelque chose?

Comment dois-je suspendre l'avertissement si j'insiste pour utiliser quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE ?

La fonction qui donne des erreurs

def _decode_stock_quote(list_of_150_stk_str):
    """decode the webpage and return dataframe"""

    from cStringIO import StringIO

    str_of_all = "".join(list_of_150_stk_str)

    quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
    quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
    quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
    quote_df['TClose'] = quote_df['TPrice']
    quote_df['RT']     = 100 * (quote_df['TPrice']/quote_df['TPCLOSE'] - 1)
    quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
    quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
    quote_df['STK_ID'] = quote_df['STK'].str.slice(13,19)
    quote_df['STK_Name'] = quote_df['STK'].str.slice(21,30)#.decode('gb2312')
    quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])

    return quote_df

Plus de messages d'erreur

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
E:\FinReporter\FM_EXT.py:450: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
E:\FinReporter\FM_EXT.py:453: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])

2
Voici un gestionnaire de contexte pour définir temporairement le niveau d'avertissement gist.github.com/notbanker/2be3ed34539c86e22ffdd88fd95ad8bc
Peter Cotton

2
vous pouvez utiliser df.set_value, docs ici - pandas.pydata.org/pandas-docs/stable/generated/…
leonprou

1
pandas.pydata.org/pandas-docs/stable/… document officiel expliquer en détail
wyx

3
@leonprou df.set_valueest obsolète. Pandas recommande désormais d'utiliser .at[]ou à la .iat[]place. docs ici pandas.pydata.org/pandas-docs/stable/generated/…
Kyle C

Je suis surpris que personne n'ait mentionné les pandas option_contextici: pandas.pydata.org/pandas-docs/stable/user_guide/options.html , utilisez aswith pd.option_context("mode.chained_assignment", None): [...]
m-dz

Réponses:


795

Le a SettingWithCopyWarningété créé pour signaler les affectations "chaînées" potentiellement déroutantes, telles que les suivantes, qui ne fonctionnent pas toujours comme prévu, en particulier lorsque la première sélection renvoie une copie . [Voir GH5390 et GH5597 pour une discussion de fond.]

df[df['A'] > 2]['B'] = new_val  # new_val not set in df

L'avertissement propose une suggestion de réécriture comme suit:

df.loc[df['A'] > 2, 'B'] = new_val

Cependant, cela ne correspond pas à votre utilisation, ce qui équivaut à:

df = df[df['A'] > 2]
df['B'] = new_val

Bien qu'il soit clair que vous ne vous souciez pas des écritures qui reviennent au cadre d'origine (puisque vous écrasez la référence), ce modèle ne peut malheureusement pas être différencié du premier exemple d'affectation chaînée. D'où l'avertissement (faux positif). Le potentiel de faux positifs est traité dans la documentation sur l'indexation , si vous souhaitez en savoir plus. Vous pouvez désactiver ce nouvel avertissement en toute sécurité avec l'affectation suivante.

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

34
Je pense que je serais surtout en faveur de ne pas avertir du tout. Si vous travaillez avec la syntaxe d'assignation chaînée, vous pouvez certainement déterminer l'ordre d'indexation qui doit se produire pour qu'il fonctionne comme prévu dans n'importe quelle situation donnée. Je pense que c'est trop paranoïaque qu'il y ait ces précautions exhaustives à ce sujet. Dans le même esprit que «laisser tout le monde être adulte» à propos des méthodes ou des attributs de classe «privés», je pense qu'il est préférable pour les pandas de laisser les utilisateurs devenir adultes sur les affectations enchaînées. Utilisez-les uniquement si vous savez ce que vous faites.
le

48
C'est un peu anti-pythonique d'essayer d'avertir les gens quand ils vont pirater des alternatives. Les méthodes d'accès plus récentes de Pandas (améliorées .ix, améliorées .iloc, etc.) peuvent certainement être considérées comme "la voie principale" sans avertir sans cesse tout le monde des autres voies. Au lieu de cela, laissez-les devenir des adultes et s'ils veulent faire une affectation enchaînée, qu'il en soit ainsi. Mes deux sous quand même. On voit souvent des commentaires mécontents des développeurs Pandas lorsque les affectations enchaînées fonctionneront pour résoudre un problème, mais ne seraient pas considérées comme la manière "principale" de le faire.
le

8
@EMS, le problème est qu'il n'est pas toujours clair dans le code où une copie par rapport à une vue est effectuée et un certain nombre de bugs / confusion résulte de ce problème. Nous envisagions de mettre un fichier / options rc pour effectuer automatiquement la configuration, ce qui pourrait être plus utile compte tenu du fonctionnement du paramètre avec avertissement de copie.
Jeff Tratner

3
La raison de l'avertissement est bien sûr pour les personnes qui mettent à jour l'ancien code. Et j'ai certainement besoin d'un avertissement, car j'ai affaire à un vieux code très moche.
Thomas Andrews,

16
Sur une note latérale, j'ai constaté que la désactivation de l'avertissement chained_assignment: pd.options.mode.chained_assignment = Nonea fait en sorte que mon code s'exécute environ 6 fois plus rapidement. Quelqu'un d'autre a connu des résultats similaires?
Muon

209

Comment faire face aux SettingWithCopyWarningPandas?

Ce message est destiné aux lecteurs qui,

  1. Voudrais comprendre ce que cet avertissement signifie
  2. Aimerait comprendre différentes façons de supprimer cet avertissement
  3. Souhaite comprendre comment améliorer son code et suivre les bonnes pratiques pour éviter cet avertissement à l'avenir.

Installer

np.random.seed(0)
df = pd.DataFrame(np.random.choice(10, (3, 5)), columns=list('ABCDE'))
df
   A  B  C  D  E
0  5  0  3  3  7
1  9  3  5  2  4
2  7  6  8  8  1

C'est quoi SettingWithCopyWarning?

Pour savoir comment gérer cet avertissement, il est important de comprendre ce qu'il signifie et pourquoi il est émis en premier lieu.

Lors du filtrage des DataFrames, il est possible de découper / indexer un cadre pour renvoyer soit une vue , soit une copie , selon la disposition interne et divers détails d'implémentation. Une "vue" est, comme le terme l'indique, une vue dans les données d'origine, donc la modification de la vue peut modifier l'objet d'origine. D'un autre côté, une "copie" est une réplication des données de l'original, et la modification de la copie n'a aucun effet sur l'original.

Comme mentionné dans d'autres réponses, le a SettingWithCopyWarningété créé pour signaler les opérations d '"affectation chaînée". Considérez dfdans la configuration ci-dessus. Supposons que vous souhaitiez sélectionner toutes les valeurs de la colonne "B" où les valeurs de la colonne "A" sont> 5. Pandas vous permet de le faire de différentes manières, certaines plus correctes que d'autres. Par exemple,

df[df.A > 5]['B']

1    3
2    6
Name: B, dtype: int64

Et,

df.loc[df.A > 5, 'B']

1    3
2    6
Name: B, dtype: int64

Ceux-ci renvoient le même résultat, donc si vous ne lisez que ces valeurs, cela ne fait aucune différence. Alors, quel est le problème? Le problème avec l'affectation chaînée, c'est qu'il est généralement difficile de prédire si une vue ou une copie est retournée, donc cela devient largement un problème lorsque vous essayez de réattribuer des valeurs. Pour construire sur l'exemple précédent, considérez comment ce code est exécuté par l'interpréteur:

df.loc[df.A > 5, 'B'] = 4
# becomes
df.__setitem__((df.A > 5, 'B'), 4)

Avec un seul __setitem__appel à df. OTOH, considérez ce code:

df[df.A > 5]['B'] = 4
# becomes
df.__getitem__(df.A > 5).__setitem__('B", 4)

Maintenant, selon qu'il a __getitem__renvoyé une vue ou une copie, l' __setitem__opération peut ne pas fonctionner .

En général, vous devez utiliser locpour l'affectation basée sur les étiquettes et ilocpour l' affectation basée sur les nombres entiers / positionnels, car la spécification garantit qu'ils fonctionnent toujours sur l'original. De plus, pour définir une seule cellule, vous devez utiliser atetiat .

Plus d'informations peuvent être trouvées dans la documentation .

Remarque
Toutes les opérations d'indexation booléennes effectuées avec locpeuvent également être effectuées avec iloc. La seule différence est queiloc attend soit des entiers / positions pour l'index, soit un tableau numpy de valeurs booléennes, et des index entiers / position pour les colonnes.

Par exemple,

df.loc[df.A > 5, 'B'] = 4

Peut être écrit nas

df.iloc[(df.A > 5).values, 1] = 4

Et,

df.loc[1, 'A'] = 100

Peut être écrit comme

df.iloc[1, 0] = 100

Etc.


Dites-moi simplement comment supprimer l'avertissement!

Considérez une opération simple sur la colonne "A" de df. La sélection de "A" et la division par 2 déclencheront l'avertissement, mais l'opération fonctionnera.

df2 = df[['A']]
df2['A'] /= 2
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/IPython/__main__.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

df2
     A
0  2.5
1  4.5
2  3.5

Il existe deux façons de désactiver directement cet avertissement:

  1. Faire un deepcopy

    df2 = df[['A']].copy(deep=True)
    df2['A'] /= 2
  2. Changementpd.options.mode.chained_assignment
    peut -il être réglé sur None, "warn"ou "raise". "warn"est la valeur par défaut. Nonesupprimera complètement l'avertissement et "raise"lancera un SettingWithCopyError, empêchant l'opération de se poursuivre.

    pd.options.mode.chained_assignment = None
    df2['A'] /= 2

@Peter Cotton dans les commentaires, a trouvé une belle façon de changer le mode de manière non intrusive (modifié à partir de cet essentiel ) en utilisant un gestionnaire de contexte, pour définir le mode uniquement aussi longtemps qu'il est nécessaire, et le réinitialiser à la état d'origine une fois terminé.

class ChainedAssignent:
    def __init__(self, chained=None):
        acceptable = [None, 'warn', 'raise']
        assert chained in acceptable, "chained must be in " + str(acceptable)
        self.swcw = chained

    def __enter__(self):
        self.saved_swcw = pd.options.mode.chained_assignment
        pd.options.mode.chained_assignment = self.swcw
        return self

    def __exit__(self, *args):
        pd.options.mode.chained_assignment = self.saved_swcw

L'utilisation est la suivante:

# some code here
with ChainedAssignent():
    df2['A'] /= 2
# more code follows

Ou, pour soulever l'exception

with ChainedAssignent(chained='raise'):
    df2['A'] /= 2

SettingWithCopyError: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

Le "problème XY": qu'est-ce que je fais mal?

La plupart du temps, les utilisateurs tentent de rechercher des moyens de supprimer cette exception sans bien comprendre pourquoi elle a été levée en premier lieu. Ceci est un bon exemple d'un problème XY , où les utilisateurs tentent de résoudre un problème "Y" qui est en fait le symptôme d'un problème enraciné plus profond "X". Des questions seront posées sur la base des problèmes courants rencontrés par cet avertissement, et des solutions seront ensuite présentées.

Question 1
J'ai un DataFrame

df
       A  B  C  D  E
    0  5  0  3  3  7
    1  9  3  5  2  4
    2  7  6  8  8  1

Je veux affecter des valeurs dans le col "A"> 5 à 1000. Ma sortie attendue est

      A  B  C  D  E
0     5  0  3  3  7
1  1000  3  5  2  4
2  1000  6  8  8  1

Mauvaise façon de procéder:

df.A[df.A > 5] = 1000         # works, because df.A returns a view
df[df.A > 5]['A'] = 1000      # does not work
df.loc[df.A  5]['A'] = 1000   # does not work

Bonne façon d'utiliser loc:

df.loc[df.A > 5, 'A'] = 1000


Question 2 1
J'essaie de définir la valeur de la cellule (1, 'D') sur 12345. Ma sortie attendue est

   A  B  C      D  E
0  5  0  3      3  7
1  9  3  5  12345  4
2  7  6  8      8  1

J'ai essayé différentes façons d'accéder à cette cellule, telles que df['D'][1]. Quelle est la meilleure façon de procéder?

1. Cette question n'est pas spécifiquement liée à l'avertissement, mais il est bon de comprendre comment effectuer correctement cette opération particulière afin d'éviter des situations où l'avertissement pourrait éventuellement se produire à l'avenir.

Vous pouvez utiliser l'une des méthodes suivantes pour ce faire.

df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345


Question 3
J'essaie de sous-définir les valeurs en fonction d'une condition. J'ai un DataFrame

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

Je voudrais attribuer des valeurs de "D" à 123 telles que "C" == 5. J'ai essayé

df2.loc[df2.C == 5, 'D'] = 123

Ce qui semble bien mais je reçois toujours le SettingWithCopyWarning! Comment puis-je réparer ça?

C'est en fait probablement à cause du code plus haut dans votre pipeline. Avez-vous créé df2quelque chose de plus grand, comme

df2 = df[df.A > 5]

? Dans ce cas, l'indexation booléenne retournera une vue, donc df2référencera l'original. Ce que vous devez faire est d'assigner df2à une copie :

df2 = df[df.A > 5].copy()
# Or,
# df2 = df.loc[df.A > 5, :]


Question 4
J'essaie de supprimer la colonne "C" en place de

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

Mais en utilisant

df2.drop('C', axis=1, inplace=True)

Jette SettingWithCopyWarning. Pourquoi cela arrive-t-il?

En effet, df2doit avoir été créé en tant que vue à partir d'une autre opération de découpage, telle que

df2 = df[df.A > 5]

La solution est ici pour faire soit un copy()des df, ou l' utilisation loc, comme avant.


7
PS: Faites-moi savoir si votre situation n'est pas couverte par la liste de questions de la section 3. Je modifierai mon message.
cs95

150

En général, l'objectif de la SettingWithCopyWarningest de montrer aux utilisateurs (et en particulier aux nouveaux utilisateurs) qu'ils peuvent fonctionner sur une copie et non sur l'original comme ils le pensent. Il y a des faux positifs (OIEau si vous savez ce que vous faites , il pourrait être ok ). Une possibilité consiste simplement à désactiver l' avertissement (par défaut d' avertissement ) comme le suggère @Garrett.

Voici une autre option:

In [1]: df = DataFrame(np.random.randn(5, 2), columns=list('AB'))

In [2]: dfa = df.ix[:, [1, 0]]

In [3]: dfa.is_copy
Out[3]: True

In [4]: dfa['A'] /= 2
/usr/local/bin/ipython:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  #!/usr/local/bin/python

Vous pouvez définir l' is_copyindicateur sur False, ce qui désactivera efficacement la vérification, pour cet objet :

In [5]: dfa.is_copy = False

In [6]: dfa['A'] /= 2

Si vous copiez explicitement, aucun autre avertissement ne se produira:

In [7]: dfa = df.ix[:, [1, 0]].copy()

In [8]: dfa['A'] /= 2

Le code que l'OP montre ci-dessus, bien que légitime, et probablement quelque chose que je fais aussi, est techniquement un cas pour cet avertissement, et non un faux positif. Une autre façon de ne pas avoir d'avertissement serait de faire l'opération de sélection via reindex, par exemple

quote_df = quote_df.reindex(columns=['STK', ...])

Ou,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21

Merci pour l'information et la discussion, je désactive simplement l'avertissement pour laisser la console silencieuse à ce sujet. Cela ressemble à la vue et à la table dans la base de données SQL. J'ai besoin d'en savoir plus sur les avantages de l'introduction du concept de «copie», mais à
mon humble avis

19
Je suis d'accord avec la copie (); c'est clair et cela a résolu mon problème (qui était un faux positif).
rdchambers

5
après la mise à jour, 0.16je vois beaucoup plus de faux positifs, le problème des faux positifs est qu'on apprend à l'ignorer, même si parfois c'est légitime.
dashesy

3
@dashesy vous manque le point. parfois peut-être même la plupart du temps cela pourrait fonctionner. Mais cela peut arriver par exemple si le cadre est plus grand / plus petit ou si vous ajoutez une colonne disons d'un type différent qui ne fonctionne pas . C'est le but. Vous faites quelque chose qui peut fonctionner mais qui n'est pas garanti. Ceci est très différent des avertissements de dépréciation. Si vous souhaitez continuer à l'utiliser et que cela fonctionne, tant mieux. Mais soyez prévenu.
Jeff

3
@Jeff a du sens maintenant, c'est donc un undefinedcomportement. Si quoi que ce soit, il devrait alors générer une erreur (pour éviter les pièges observés dans C), car apiétant gelé, le comportement actuel de l'avertissement a un sens pour la compatibilité descendante. Et je vais les faire lancer pour les attraper comme des erreurs dans mon code de production ( warnings.filterwarnings('error', r'SettingWithCopyWarning). De plus, la suggestion d'utiliser .locparfois n'aide pas non plus (si elle est en groupe).
dashesy

41

Avertissement de copie de trame de données Pandas

Lorsque vous allez faire quelque chose comme ça:

quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

pandas.ix dans ce cas, renvoie une nouvelle trame de données autonome.

Les valeurs que vous décidez de modifier dans ce cadre de données ne changeront pas le cadre de données d'origine.

C'est ce que les pandas essaient de vous avertir.


Pourquoi .ix est-ce une mauvaise idée

L' .ixobjet essaie de faire plus d'une chose, et pour toute personne qui a lu quelque chose sur le code propre, c'est une forte odeur.

Compte tenu de cette trame de données:

df = pd.DataFrame({"a": [1,2,3,4], "b": [1,1,2,2]})

Deux comportements:

dfcopy = df.ix[:,["a"]]
dfcopy.a.ix[0] = 2

Comportement un: dfcopyest désormais une trame de données autonome. Le changer ne changera pasdf

df.ix[0, "a"] = 3

Comportement deux: cela modifie la trame de données d'origine.


Utiliser à la .locplace

Les développeurs de pandas ont reconnu que l' .ixobjet était assez malodorant [spéculativement] et ont donc créé deux nouveaux objets qui aident à l'accession et à l'attribution des données. (L'autre étant .iloc)

.loc est plus rapide, car il n'essaie pas de créer une copie des données.

.loc est destiné à modifier votre cadre de données existant sur place, ce qui est plus efficace en mémoire.

.loc est prévisible, il a un comportement.


La solution

Ce que vous faites dans votre exemple de code, c'est charger un gros fichier avec beaucoup de colonnes, puis le modifier pour qu'il soit plus petit.

La pd.read_csvfonction peut vous aider avec beaucoup de cela et également accélérer le chargement du fichier.

Donc au lieu de faire ça

quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

Faites ceci

columns = ['STK', 'TPrice', 'TPCLOSE', 'TOpen', 'THigh', 'TLow', 'TVol', 'TAmt', 'TDate', 'TTime']
df = pd.read_csv(StringIO(str_of_all), sep=',', usecols=[0,3,2,1,4,5,8,9,30,31])
df.columns = columns

Cela ne lira que les colonnes qui vous intéressent et les nommera correctement. Pas besoin d'utiliser l' .ixobjet diabolique pour faire des trucs magiques.


"Les développeurs de pandas ont reconnu que l'objet .ix était assez malodorant [spéculativement] et ont donc créé deux nouveaux objets" - quel est l'autre?
jf328

3
@ jf328 .iloc je pense
Brian Bien

1
Oui, ça l'est .iloc. Ce sont les deux principales méthodes d'indexation des structures de données des pandas. En savoir plus dans la documentation.
Ninjakannon

Comment doit-on remplacer une colonne DataFrame avec des horodatages en une colonne avec un objet date-heure ou une chaîne?
boldnik

@boldnik Cochez cette réponse stackoverflow.com/a/37453925/3730397
firelynx

20

Ici, je réponds directement à la question. Comment y faire face?

Faites un .copy(deep=False)après avoir coupé. Voir pandas.DataFrame.copy .

Attendez, une tranche ne retourne pas une copie? Après tout, c'est ce que le message d'avertissement tente de dire? Lisez la longue réponse:

import pandas as pd
df = pd.DataFrame({'x':[1,2,3]})

Cela donne un avertissement:

df0 = df[df.x>2]
df0['foo'] = 'bar'

Cela ne signifie pas:

df1 = df[df.x>2].copy(deep=False)
df1['foo'] = 'bar'

Les deux df0et df1sont des DataFrameobjets, mais quelque chose à leur sujet est différent qui permet d'imprimer Pandas l'avertissement. Voyons ce que c'est.

import inspect
slice= df[df.x>2]
slice_copy = df[df.x>2].copy(deep=False)
inspect.getmembers(slice)
inspect.getmembers(slice_copy)

En utilisant votre outil de diff de choix, vous verrez qu'au-delà de quelques adresses, la seule différence matérielle est la suivante:

|          | slice   | slice_copy |
| _is_copy | weakref | None       |

La méthode qui décide d'avertir est celle DataFrame._check_setitem_copyqui vérifie _is_copy. Alors voilà. Faites en copysorte que votre DataFrame ne soit pas_is_copy .

L'avertissement suggère d'utiliser .loc, mais si vous l'utilisez .locsur un cadre _is_copy, vous obtiendrez toujours le même avertissement. Trompeur? Oui. Ennuyeux? Tu paries. Utile? Potentiellement, lorsque l'affectation chaînée est utilisée. Mais il ne peut pas détecter correctement l'attribution de chaîne et imprime l'avertissement sans discrimination.


11

Ce sujet est vraiment déroutant avec les Pandas. Heureusement, il a une solution relativement simple.

Le problème est qu'il n'est pas toujours clair si les opérations de filtrage des données (par exemple loc) renvoient une copie ou une vue du DataFrame. Une utilisation ultérieure de ces DataFrame filtrés pourrait donc prêter à confusion.

La solution simple est (sauf si vous devez travailler avec de très grands ensembles de données):

Chaque fois que vous devez mettre à jour des valeurs, assurez-vous toujours de copier implicitement le DataFrame avant l'affectation.

df  # Some DataFrame
df = df.loc[:, 0:2]  # Some filtering (unsure whether a view or copy is returned)
df = df.copy()  # Ensuring a copy is made
df[df["Name"] == "John"] = "Johny"  # Assignment can be done now (no warning)

Il y a une faute de frappe: devrait implicitement être explicitement
s9527

7

Pour lever tout doute, ma solution était de faire une copie complète de la tranche au lieu d'une copie régulière. Cela peut ne pas être applicable en fonction de votre contexte (contraintes de mémoire / taille de la tranche, potentiel de dégradation des performances - surtout si la copie se produit dans une boucle comme ce fut le cas pour moi, etc ...)

Pour être clair, voici l'avertissement que j'ai reçu:

/opt/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:54:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

Illustration

J'avais des doutes que l'avertissement a été lancé à cause d'une colonne que je déposais sur une copie de la tranche. Sans essayer techniquement de définir une valeur dans la copie de la tranche, il s'agissait tout de même d'une modification de la copie de la tranche. Voici les étapes (simplifiées) que j'ai prises pour confirmer les soupçons, j'espère que cela aidera ceux d'entre nous qui tentent de comprendre l'avertissement.

Exemple 1: déposer une colonne sur l'original affecte la copie

Nous le savions déjà, mais c'est un rappel sain. Ce n'est PAS l'objet de l'avertissement.

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123


>> df2 = df1
>> df2

A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df1 affects df2
>> df1.drop('A', axis=1, inplace=True)
>> df2
    B
0   121
1   122
2   123

Il est possible d'éviter les modifications apportées sur df1 pour affecter df2

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

A   B
0   111 121
1   112 122
2   113 123

>> import copy
>> df2 = copy.deepcopy(df1)
>> df2
A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df1 does not affect df2
>> df1.drop('A', axis=1, inplace=True)
>> df2
    A   B
0   111 121
1   112 122
2   113 123

Exemple 2: la suppression d'une colonne sur la copie peut affecter l'original

Cela illustre en fait l'avertissement.

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123

>> df2 = df1
>> df2

    A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df2 can affect df1
# No slice involved here, but I believe the principle remains the same?
# Let me know if not
>> df2.drop('A', axis=1, inplace=True)
>> df1

B
0   121
1   122
2   123

Il est possible d'éviter les modifications apportées sur df2 pour affecter df1

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123

>> import copy
>> df2 = copy.deepcopy(df1)
>> df2

A   B
0   111 121
1   112 122
2   113 123

>> df2.drop('A', axis=1, inplace=True)
>> df1

A   B
0   111 121
1   112 122
2   113 123

À votre santé!


4

Cela devrait fonctionner:

quote_df.loc[:,'TVol'] = quote_df['TVol']/TVOL_SCALE

4

Certains voudront peut-être simplement supprimer l'avertissement:

class SupressSettingWithCopyWarning:
    def __enter__(self):
        pd.options.mode.chained_assignment = None

    def __exit__(self, *args):
        pd.options.mode.chained_assignment = 'warn'

with SupressSettingWithCopyWarning():
    #code that produces warning

3

Si vous avez affecté la tranche à une variable et que vous souhaitez définir à l'aide de la variable comme suit:

df2 = df[df['A'] > 2]
df2['B'] = value

Et vous ne voulez pas utiliser la solution Jeffs car votre calcul de condition df2est trop long ou pour une autre raison, vous pouvez utiliser les éléments suivants:

df.loc[df2.index.tolist(), 'B'] = value

df2.index.tolist() renvoie les indices de toutes les entrées de df2, qui seront ensuite utilisés pour définir la colonne B dans la trame de données d'origine.


c'est 9 fois plus cher que df ["B"] = valeur
Claudiu Creanga

Pouvez-vous expliquer cela plus en profondeur @ClaudiuCreanga?
gies0r

2

Pour moi, ce problème s'est produit dans un exemple> simplifié <suivant. Et j'ai également pu le résoudre (avec une bonne solution, espérons-le):

ancien code avec avertissement:

def update_old_dataframe(old_dataframe, new_dataframe):
    for new_index, new_row in new_dataframe.iterrorws():
        old_dataframe.loc[new_index] = update_row(old_dataframe.loc[new_index], new_row)

def update_row(old_row, new_row):
    for field in [list_of_columns]:
        # line with warning because of chain indexing old_dataframe[new_index][field]
        old_row[field] = new_row[field]  
    return old_row

Cela a imprimé l'avertissement pour la ligne old_row[field] = new_row[field]

Étant donné que les lignes de la méthode update_row sont en fait de type Series, j'ai remplacé la ligne par:

old_row.at[field] = new_row.at[field]

c'est-à-dire la méthode d'accès / recherche pour a Series. Bien que les deux fonctionnent très bien et le résultat est le même, de cette façon, je n'ai pas à désactiver les avertissements (= conservez-les pour d'autres problèmes d'indexation de la chaîne ailleurs).

J'espère que cela peut aider quelqu'un.


2

Vous pourriez éviter tout le problème comme celui-ci, je crois:

return (
    pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
    .rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
    .ix[:,[0,3,2,1,4,5,8,9,30,31]]
    .assign(
        TClose=lambda df: df['TPrice'],
        RT=lambda df: 100 * (df['TPrice']/quote_df['TPCLOSE'] - 1),
        TVol=lambda df: df['TVol']/TVOL_SCALE,
        TAmt=lambda df: df['TAmt']/TAMT_SCALE,
        STK_ID=lambda df: df['STK'].str.slice(13,19),
        STK_Name=lambda df: df['STK'].str.slice(21,30)#.decode('gb2312'),
        TDate=lambda df: df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10]),
    )
)

Utilisation de l'attribution. De la documentation : Attribuez de nouvelles colonnes à un DataFrame, en renvoyant un nouvel objet (une copie) avec toutes les colonnes d'origine en plus des nouvelles.

Voir l'article de Tom Augspurger sur le chaînage de méthodes dans les pandas: https://tomaugspurger.github.io/method-chaining


2

Question / remarque pour débutant

Peut-être une clarification pour d'autres débutants comme moi (je viens de R qui semble fonctionner un peu différemment sous le capot). Le code fonctionnel et inoffensif suivant a continué de produire l'avertissement SettingWithCopy, et je n'ai pas pu comprendre pourquoi. J'avais à la fois lu et compris le produit avec "l'indexation chaînée", mais mon code n'en contient pas:

def plot(pdb, df, title, **kw):
    df['target'] = (df['ogg'] + df['ugg']) / 2
    # ...

Mais ensuite, beaucoup trop tard, j'ai regardé où la fonction plot () est appelée:

    df = data[data['anz_emw'] > 0]
    pixbuf = plot(pdb, df, title)

Donc, "df" n'est pas un bloc de données mais un objet qui se souvient en quelque sorte qu'il a été créé en indexant un bloc de données (est-ce donc une vue?) Qui ferait la ligne dans plot ()

 df['target'] = ...

équivalent à

 data[data['anz_emw'] > 0]['target'] = ...

qui est une indexation chaînée. Ai-je bien compris?

En tous cas,

def plot(pdb, df, title, **kw):
    df.loc[:,'target'] = (df['ogg'] + df['ugg']) / 2

l'a corrigé.


1

Comme cette question est déjà entièrement expliquée et discutée dans les réponses existantes, je vais simplement fournir une pandasapproche soignée au gestionnaire de contexte à l'aide de pandas.option_context(liens vers des documents et des exemples ) - il n'est absolument pas nécessaire de créer une classe personnalisée avec toutes les méthodes de dunder et d'autres cloches et sifflets.

Tout d'abord, le code du gestionnaire de contexte lui-même:

from contextlib import contextmanager

@contextmanager
def SuppressPandasWarning():
    with pd.option_context("mode.chained_assignment", None):
        yield

Puis un exemple:

import pandas as pd
from string import ascii_letters

a = pd.DataFrame({"A": list(ascii_letters[0:4]), "B": range(0,4)})

mask = a["A"].isin(["c", "d"])
# Even shallow copy below is enough to not raise the warning, but why is a mystery to me.
b = a.loc[mask]  # .copy(deep=False)

# Raises the `SettingWithCopyWarning`
b["B"] = b["B"] * 2

# Does not!
with SuppressPandasWarning():
    b["B"] = b["B"] * 2

Il convient de noter que les deux approches ne modifient pas a, ce qui est un peu surprenant pour moi, et même une copie df peu profonde avec .copy(deep=False)empêcherait que cet avertissement ne soit émis (pour autant que je sache, la copie peu profonde devrait au moins également être modifiée a, mais elle ne le fait pas pas pandasmagique.).


hmmm, je le comprends si l'avertissement a soulevé quelque chose est mal évidemment, alors mieux vaut éviter l'avertissement comme le supprimer, qu'en pensez-vous?
jezrael

Non, l'avertissement n'est qu'un avertissement. Comme ici, cela vous avertit que quelque chose ne va pas, ce qui est formidable à savoir, mais si vous savez quoi et pourquoi vous faites, il est parfaitement bien de supprimer certains d'entre eux. Voir l'explication dans stackoverflow.com/a/20627316/4272484 sur la réaffectation des références.
m-dz

1

J'avais eu ce problème avec .apply()lors de l'attribution d'une nouvelle trame de données à partir d'une trame de données préexistante sur laquelle j'ai utilisé la .query()méthode. Par exemple:

prop_df = df.query('column == "value"')
prop_df['new_column'] = prop_df.apply(function, axis=1)

Renverrait cette erreur. Le correctif qui semble résoudre l'erreur dans ce cas consiste à le remplacer par:

prop_df = df.copy(deep=True)
prop_df = prop_df.query('column == "value"')
prop_df['new_column'] = prop_df.apply(function, axis=1)

Cependant, cela n'est PAS efficace, en particulier lorsque vous utilisez des trames de données volumineuses, car vous devez faire une nouvelle copie.

Si vous utilisez la .apply()méthode pour générer une nouvelle colonne et ses valeurs, un correctif qui résout l'erreur et est plus efficace consiste à ajouter .reset_index(drop=True):

prop_df = df.query('column == "value"').reset_index(drop=True)
prop_df['new_column'] = prop_df.apply(function, axis=1)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.