Πώς να αντιμετωπίσετε το SettingWithCopyWarning στον Pandas;

Background

Μόλις αναβάθμισα το Pandas μου από 0.11 σε 0.13.0rc1. Τώρα, η εφαρμογή βγάζει πολλές νέες προειδοποιήσεις. Μία από αυτές είναι η εξής: "Οπότε, αν δεν είναι απαραίτητο, θα πρέπει να βάλω μια προειδοποίηση..:

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

Θέλω να μάθω τι ακριβώς σημαίνει; Πρέπει να αλλάξω κάτι;

Πώς πρέπει να αναστείλω την προειδοποίηση αν επιμένω να χρησιμοποιώ quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE;

Η συνάρτηση που προκαλεί σφάλματα

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

Περισσότερα μηνύματα σφάλματος

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])
Λύση

Η προειδοποίηση SettingWithCopyWarning δημιουργήθηκε για να επισημάνει δυνητικά συγκεχυμένες "αλυσιδωτές" αναθέσεις, όπως οι ακόλουθες, οι οποίες δεν λειτουργούν πάντα όπως αναμένεται, ιδιαίτερα όταν η πρώτη επιλογή επιστρέφει ένα αντίγραφο. [βλέπε GH5390 και GH5597 για το ιστορικό της συζήτησης.]

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

Η προειδοποίηση προσφέρει μια πρόταση για την επαναδιατύπωση ως εξής:

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

Ωστόσο, αυτό δεν ταιριάζει με τη δική σας χρήση, η οποία είναι ισοδύναμη με:

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

Ενώ είναι'ξεκάθαρο ότι δεν σας ενδιαφέρει να επιστρέψουν οι εγγραφές στο αρχικό πλαίσιο (αφού αντικαταστήσατε την αναφορά σε αυτό), δυστυχώς αυτό το μοτίβο δεν μπορεί να διαφοροποιηθεί από το πρώτο παράδειγμα αλυσιδωτής ανάθεσης, εξ ου και η (ψευδώς θετική) προειδοποίηση. Το ενδεχόμενο ψευδώς θετικών αποτελεσμάτων εξετάζεται στα docs on indexing, αν θέλετε να διαβάσετε περισσότερα. Μπορείτε να απενεργοποιήσετε με ασφάλεια αυτή τη νέα προειδοποίηση με την ακόλουθη ανάθεση.

pd.options.mode.chained_assignment = None  # default='warn'
Σχόλια (10)

Γενικά, το νόημα της προειδοποίησης SettingWithCopyWarning είναι να δείξει στους χρήστες (και ειδικά στους νέους χρήστες) ότι μπορεί να λειτουργούν σε ένα αντίγραφο και όχι στο πρωτότυπο όπως νομίζουν. Υπάρχουν ψευδώς θετικά αποτελέσματα (δηλαδή αν ξέρετε τι κάνετε μπορεί να είναι οκ). Μια δυνατότητα είναι απλά να απενεργοποιήσετε την (εξ ορισμού προειδοποίηση) προειδοποίηση όπως προτείνει ο @Garrett.

Εδώ είναι μια άλλη επιλογή:

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

Μπορείτε να ορίσετε τη σημαία is_copy σε False, η οποία ουσιαστικά θα απενεργοποιήσει τον έλεγχο, για το συγκεκριμένο αντικείμενο:

In [5]: dfa.is_copy = False

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

Εάν αντιγράψετε ρητά, τότε δεν θα συμβεί καμία περαιτέρω προειδοποίηση:

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

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

Ο κώδικας που δείχνει ο OP παραπάνω, ενώ είναι νόμιμος, και πιθανώς κάτι που κάνω και εγώ, είναι τεχνικά μια περίπτωση για αυτή την προειδοποίηση, και όχι ένα ψευδές θετικό αποτέλεσμα. Ένας άλλος τρόπος για να δεν έχουμε την προειδοποίηση θα ήταν να κάνουμε την λειτουργία επιλογής μέσω του reindex, π.χ.

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

Ή,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21
Σχόλια (10)

Πάντας προειδοποίηση αντιγραφής πλαισίου δεδομένων

Όταν πάτε και κάνετε κάτι τέτοιο:

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

Το pandas.ix σε αυτή την περίπτωση επιστρέφει ένα νέο, αυτόνομο πλαίσιο δεδομένων.

Οποιεσδήποτε τιμές αποφασίσετε να αλλάξετε σε αυτό το πλαίσιο δεδομένων, δεν θα αλλάξουν το αρχικό πλαίσιο δεδομένων.

Αυτό είναι που προσπαθεί να σας προειδοποιήσει το pandas.


Γιατί το .ix είναι κακή ιδέα

Το αντικείμενο .ix προσπαθεί να κάνει περισσότερα από ένα πράγματα, και για όποιον έχει διαβάσει οτιδήποτε για καθαρό κώδικα, αυτό είναι μια έντονη μυρωδιά.

Δεδομένου αυτού του πλαισίου δεδομένων:

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

Δύο συμπεριφορές:

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

Συμπεριφορά πρώτη: Το dfcopy είναι τώρα ένα αυτόνομο πλαίσιο δεδομένων. Η αλλαγή του δεν θα αλλάξει το df.

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

Συμπεριφορά δύο: Αυτό αλλάζει το αρχικό πλαίσιο δεδομένων.


Χρησιμοποιήστε το .loc αντί αυτού

Οι προγραμματιστές του pandas αναγνώρισαν ότι το αντικείμενο .ix ήταν αρκετά δύσοσμο[κερδοσκοπικά] και έτσι δημιούργησαν δύο νέα αντικείμενα τα οποία βοηθούν στην ένταξη και ανάθεση δεδομένων. (Το άλλο είναι το .iloc)

Το .loc είναι ταχύτερο, επειδή δεν προσπαθεί να δημιουργήσει αντίγραφο των δεδομένων.

Το .loc προορίζεται να τροποποιεί το υπάρχον πλαίσιο δεδομένων σας in place, το οποίο είναι πιο αποδοτικό στη μνήμη.

Η .loc είναι προβλέψιμη, έχει μία συμπεριφορά.


Η λύση

Αυτό που κάνετε στο παράδειγμα κώδικα σας είναι να φορτώνετε ένα μεγάλο αρχείο με πολλές στήλες και στη συνέχεια να το τροποποιείτε ώστε να γίνει μικρότερο.

Η συνάρτηση pd.read_csv μπορεί να σας βοηθήσει πολύ σε αυτό και επίσης να κάνει τη φόρτωση του αρχείου πολύ πιο γρήγορη.

Έτσι, αντί να κάνετε αυτό

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

Κάνε αυτό

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

Αυτό θα διαβάσει μόνο τις στήλες που σας ενδιαφέρουν και θα τις ονομάσει σωστά. Δεν χρειάζεται να χρησιμοποιείτε το κακό αντικείμενο .ix για να κάνετε μαγικά πράγματα.

Σχόλια (5)