web-dev-qa-db-fra.com

Comment gérer SettingWithCopyWarning dans les pandas?

Contexte

Je viens de mettre à niveau mes Pandas de 0,11 à 0,13.0rc1. Maintenant, l'application affiche 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])
386
bigbug

La variable SettingWithCopyWarning a été créée pour signaler les affectations "chaînées" potentiellement confuses, telles que les suivantes, qui ne fonctionnent pas toujours comme prévu, en particulier lorsque la première sélection renvoie un copy. [voir GH5390 et GH5597 pour une discussion en arrière-plan.]

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

L’avertissement propose de réécrire 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 de revenir au cadre d'origine (puisque vous avez écrasé sa 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 les docs sur l'indexation , si vous souhaitez en savoir plus. Vous pouvez désactiver ce nouvel avertissement en toute sécurité avec l'affectation suivante.

pd.options.mode.chained_assignment = None  # default='warn'
512
Garrett

En général, le but de la SettingWithCopyWarning est de montrer aux utilisateurs (et particulièrement aux nouveaux utilisateurs) qu'ils peuvent opèrent sur une copie et non sur l'original comme ils le pensent. Là sont faux positifs (IOW si vous savez ce que vous faites cela pourrait être ok). Une possibilité consiste simplement à désactiver l'avertissement (par défaut avertir) 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 le drapeau is_copy sur False, ce qui désactive effectivement 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 le PO 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 pas l'avertissement serait de faire l'opération de sélection via reindex, par ex.

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

Ou,

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

Avertissement de copie de cadre de données Pandas

Quand tu vas faire quelque chose comme ça:

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

pandas.ixdans ce cas renvoie un nouveau cadre de données autonome.

Toutes les valeurs que vous décidez de modifier dans cette image de données ne modifieront pas l'image de données d'origine.

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


Pourquoi .ix est une mauvaise idée

L'objet .ix essaie de faire plus d'une chose, et pour quiconque a lu quelque chose sur le code propre, c'est une odeur forte.

Étant donné cette base 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: dfcopy est maintenant un cadre de données autonome. Le changer ne changera pas df

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

Comportement deux: Cela modifie la base de données d'origine.


Utilisez .loc à la place

Les développeurs de pandas ont reconnu que l’objet .ix était assez odorant [spéculativement] et ont ainsi créé deux nouveaux objets qui facilitent l’accession et l’attribution de données. (L'autre étant .iloc)

.loc est plus rapide, car il ne tente pas de créer une copie des données.

.loc est censé modifier votre structure de données existante, ce qui optimise l’utilisation de la mémoire.

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


La solution

Dans votre exemple de code, vous chargez un gros fichier avec beaucoup de colonnes, puis vous le modifiez pour le rendre plus petit.

La fonction pd.read_csv peut vous aider avec beaucoup de choses et aussi rendre le chargement du fichier beaucoup plus rapide.

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]]

Faire 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'objet pervers .ix pour faire des choses magiques.

34
firelynx

Pour dissiper tout doute, ma solution a été de créer une copie complète de la tranche au lieu d’une copie normale . Cela peut ne pas s’appliquer en fonction de votre contexte (contraintes de mémoire/taille de la tranche, potentiel de dégradation des performances - en particulier si la copie se fait en boucle comme elle l’a fait 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 avait été lancé à cause d'une colonne que je déposais sur une copie de la tranche. Bien que n'essayant pas techniquement de définir une valeur dans la copie de la tranche, il s'agissait toujours d'une modification de la copie de la tranche . Ci-dessous, les étapes (simplifiées) que j'ai prises pour confirmer la suspicion, j'espère que cela aidera. ceux d'entre nous qui essayons de comprendre l'avertissement.

Exemple 1: le fait de supprimer une colonne de l'original affecte la copie.

Nous le savions déjà, mais c’est un bon rappel. C’estPASl’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 que les modifications apportées sur df1 affectent 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: le fait de déposer une colonne sur la copie peut affecter l'original

Ceci illustre réellement 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 que les modifications apportées sur df2 affectent 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é!

6
Raphvanns

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

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

Et vous ne voulez pas utiliser la solution Jeffs parce que votre condition informatique df2 est trop longue ou pour une autre raison, vous pouvez utiliser les éléments suivants:

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

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

2
Steohan

Vous pouvez éviter tout le problème comme ceci, 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]),
    )
)

Utiliser Assign. A partir de documentation : Affectez 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 l'enchaînement de méthodes dans les pandas: https://tomaugspurger.github.io/method-chaining

1
hughdbrown

Pour moi, ce problème est survenu dans un exemple> simplifié <suivant. Et j'ai aussi été capable de le résoudre (si tout va bien avec une solution correcte):

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 imprimait l'avertissement pour la ligne old_row[field] = new_row[field]

Comme les lignes de la méthode update_row sont en réalité du type Series, j'ai remplacé la ligne par:

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

c'est-à-dire méthode pour accéder à/recherches pour une Series. Même si les deux fonctionnent parfaitement et que le résultat est identique, je n'ai donc pas à désactiver les avertissements (= les conserver pour les autres problèmes d'indexation en chaîne ailleurs).

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

1
Petr Szturc

Cela devrait fonctionner:

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

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
0
delica

Ce sujet est vraiment déroutant avec les pandas. Heureusement, la solution est relativement simple.

Le problème est qu’il n’est pas toujours clair si les opérations de filtrage de données (par exemple, loc) renvoient une copie ou une vue du DataFrame. L'utilisation ultérieure d'un tel DataFrame filtré pourrait donc être source de confusion.

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

Lorsque 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)

0
Mikulas

Question de débutant suivante/remarque

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 inoffensif et fonctionnel suivant continue de générer l'avertissement SettingWithCopy et je ne comprends pas pourquoi. J'avais à la fois lu et compris le numéro publié avec "indexation en chaîne", mais mon code n'en contient aucun:

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

Mais ensuite, beaucoup trop tard, j'ai regardé à quel endroit la fonction intrigue () est appelée:

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

Donc, "df" n'est pas un cadre de données, mais un objet qui se souvient qu'il a été créé en indexant un cadre de données (est-ce donc une vue?) Qui créerait une 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

corrigé.

0
musbur

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

Créez un .copy() après avoir découpé1.

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

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

Cela donne un avertissement:

df0['foo'] = 'bar'

Cela ne veut pas:

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

df0 et df1 sont tous deux des objets DataFrame, mais quelque chose à leur sujet est différent et permet aux pandas d’imprimer l’avertissement. Voyons ce que c'est.

import inspect
test = df[df.x>2]
test_copy = df[df.x>2].copy()
inspect.getmembers(test)
inspect.getmembers(test_copy)

En utilisant votre outil de choix, vous verrez qu'au-delà de quelques adresses, les seules différences importantes sont les suivantes:

|          | test    | test_copy |
| _is_copy | weakref | None      |
| _is_view | True    | False     |

La méthode qui décide d’avertir est DataFrame._check_setitem_copy qui vérifie _is_copy.

L'avertissement suggère d'utiliser .loc, mais si vous utilisez .loc sur une image qui _is_copy, vous aurez toujours le même avertissement. Trompeur? Oui. Ennuyeux? Tu paries. Utile? Potentiellement, lorsque l'attribution en chaîne est utilisée. Mais il ne peut même pas distinguer ce cas et affiche l'avertissement sans discernement.


1 Cela produira une copie complète des données, de sorte que vous ne souhaiterez probablement pas utiliser cette méthode pour les grandes trames de données ou lorsqu'une copie superficielle est souhaitée. Attention: si vous avez des objets python dans le cadre de données, seules les références seront copiées contrairement à copy.deepcopy. Plus d'infos dans le doc .

0
user443854