Jak radzić sobie z SettingWithCopyWarning w Pandach?


629

tło

Właśnie zaktualizowałem moje Pandy z 0.11 do 0.13.0rc1. Teraz aplikacja wyświetla wiele nowych ostrzeżeń. Jeden z nich taki:

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

Chcę wiedzieć, co to dokładnie znaczy? Czy muszę coś zmienić?

Jak zawiesić ostrzeżenie, jeśli nalegam na użycie quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE?

Funkcja powodująca błędy

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

Więcej komunikatów o błędach

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
Oto menedżer kontekstu, aby tymczasowo ustawić poziom ostrzeżenia gist.github.com/notbanker/2be3ed34539c86e22ffdd88fd95ad8bc
Peter Cotton

2
możesz użyć df.set_value, dokumenty tutaj - pandas.pydata.org/pandas-docs/stable/generated/…
leonprou

1
pandas.pydata.org/pandas-docs/stable/… oficjalny dokument szczegółowo wyjaśnić
wyx

3
@leonprou df.set_valuezostał uznany za przestarzały. Pandy teraz zalecają użycie .at[]lub .iat[]zamiast tego. Dokumenty
Kyle C

Dziwi mnie, że nikt nie wspomniał option_contexttutaj o pandach : pandas.pydata.org/pandas-docs/stable/user_guide/options.html , użyj jakowith pd.option_context("mode.chained_assignment", None): [...]
m-dz

Odpowiedzi:


793

SettingWithCopyWarningZostał stworzony, aby flaga potencjalnie mylące „przykuty” zadania, takie jak następujące, które nie zawsze działa zgodnie z oczekiwaniami, szczególnie gdy pierwszy wybór zwraca kopię . [patrz GH5390 i GH5597 w celu omówienia w tle.]

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

Ostrzeżenie oferuje propozycję przepisania w następujący sposób:

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

Nie pasuje to jednak do twojego zastosowania, co jest równoważne z:

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

Chociaż jasne jest, że nie obchodzi Cię zapisywanie go z powrotem do oryginalnej ramki (ponieważ zastępujesz odniesienie do niego), niestety ten wzorzec nie może być odróżniony od pierwszego przykładu przypisania łańcuchowego. Stąd ostrzeżenie (fałszywie pozytywne). Potencjalne wyniki fałszywie dodatnie zostały omówione w dokumentach dotyczących indeksowania , jeśli chcesz przeczytać więcej. Możesz bezpiecznie wyłączyć to nowe ostrzeżenie za pomocą następującego zadania.

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

34
Myślę, że w większości opowiadałbym się za tym, aby w ogóle o tym nie ostrzegać. Jeśli pracujesz z powiązaną składnią przypisania, możesz zdecydowanie ustalić kolejność indeksowania, która musi się zdarzyć, aby działała zgodnie z oczekiwaniami w danej sytuacji. Myślę, że to zbyt paranoiczne, że istnieją w tej kwestii wyczerpujące środki ostrożności. W tym samym duchu, co „pozwalanie wszystkim dorosłym” na temat metod i atrybutów klasy „prywatnej”, myślę, że lepiej jest, aby pandy pozwalały użytkownikom dorastać na temat powiązanych zadań. Używaj ich tylko wtedy, gdy wiesz, co robisz.
ely

48
Nieco mityczne jest próbowanie ostrzec ludzi, gdy szukają alternatywnych rozwiązań. Metody dostępu do Pand w nowszym stylu (ulepszone .ix, ulepszone .ilocitp.) Można zdecydowanie postrzegać jako „podstawowy sposób” bez ciągłego ostrzegania wszystkich o innych sposobach. Zamiast tego pozwól im być dorosłymi, a jeśli chcą wykonywać powiązane zadania, niech tak będzie. W każdym razie moje dwa centy. Widzimy tu niezadowolone komentarze deweloperów Pandas, gdy powiązane zadania będą działać na rzecz rozwiązania problemu, ale nie będą uważane za „podstawowy” sposób na zrobienie tego.
el

8
@EMS problem polega na tym, że nie zawsze jest jasne w kodzie, w którym tworzona jest kopia kontra widok i powstaje szereg błędów / pomyłek z tego problemu. Zastanawialiśmy się nad wprowadzeniem pliku / opcji rc do automatycznej konfiguracji, co może być bardziej przydatne, biorąc pod uwagę działanie ustawienia z ostrzeżeniem przed kopiowaniem.
Jeff Tratner,

3
Powodem ostrzeżenia są oczywiście osoby aktualizujące stary kod. I zdecydowanie potrzebuję ostrzeżenia, ponieważ mam do czynienia z jakimś bardzo brzydkim starym kodem.
Thomas Andrews,

15
Na marginesie zauważyłem, że wyłączenie ostrzeżenia chained_assignment: pd.options.mode.chained_assignment = Nonespowodowało, że mój kod działał około 6 razy szybciej. Czy ktoś jeszcze doświadczył podobnych wyników?
Muon

209

Jak radzić sobie SettingWithCopyWarningw Pandach?

Ten post jest przeznaczony dla czytelników, którzy:

  1. Chciałby zrozumieć, co oznacza to ostrzeżenie
  2. Chciałby zrozumieć różne sposoby tłumienia tego ostrzeżenia
  3. Chciałby zrozumieć, jak ulepszyć swój kod i postępować zgodnie z dobrymi praktykami, aby uniknąć tego ostrzeżenia w przyszłości.

Ustawiać

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

Co to jest SettingWithCopyWarning?

Aby wiedzieć, jak sobie poradzić z tym ostrzeżeniem, ważne jest, aby zrozumieć, co to znaczy i dlaczego jest ono podnoszone.

Podczas filtrowania ramek danych istnieje możliwość wycinania / indeksowania ramki w celu zwrócenia widoku lub kopii , w zależności od układu wewnętrznego i różnych szczegółów implementacji. „Widok” to, jak sugeruje to termin, widok oryginalnych danych, więc modyfikacja widoku może modyfikować oryginalny obiekt. Z drugiej strony „kopia” jest replikacją danych z oryginału, a modyfikacja kopii nie ma wpływu na oryginał.

Jak wspomniano w innych odpowiedziach, SettingWithCopyWarningutworzono go w celu oznaczenia operacji „przypisania łańcuchowego”. Rozważ dfpowyższą konfigurację. Załóżmy, że chcesz wybrać wszystkie wartości w kolumnie „B”, gdzie wartości w kolumnie „A” to> 5. Panda pozwala to robić na różne sposoby, niektóre bardziej poprawne niż inne. Na przykład,

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

1    3
2    6
Name: B, dtype: int64

I,

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

1    3
2    6
Name: B, dtype: int64

Zwracają ten sam wynik, więc jeśli czytasz tylko te wartości, nie ma to znaczenia. Więc o co chodzi? Problem z połączeniem łańcuchowym polega na tym, że zazwyczaj trudno jest przewidzieć, czy widok lub kopia zostaną zwrócone, więc w dużej mierze staje się to problemem, gdy próbujesz przypisać wartości z powrotem. Aby skorzystać z wcześniejszego przykładu, zastanów się, w jaki sposób ten kod jest wykonywany przez interpretera:

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

Za pomocą jednego __setitem__połączenia z df. OTOH, rozważ ten kod:

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

Teraz, w zależności od tego, czy __getitem__zwrócił widok, czy kopię, __setitem__operacja może nie działać .

Zasadniczo należy używać locdo przypisywania opartego na etykietach i przypisywania ilocliczb całkowitych / pozycyjnych, ponieważ specyfikacja gwarantuje, że zawsze działają one na oryginale. Dodatkowo do ustawienia pojedynczej komórki należy użyć ati iat.

Więcej można znaleźć w dokumentacji .

Uwaga
Wszystkie operacje indeksowania logicznego wykonane za pomocą locmożna również wykonać za pomocą iloc. Jedyna różnica polega na tym, że ilococzekuje się liczb całkowitych / pozycji dla indeksu lub tablicy liczbowej wartości boolowskich oraz indeksów liczb całkowitych / pozycji dla kolumn.

Na przykład,

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

Można napisać nas

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

I,

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

Można zapisać jako

df.iloc[1, 0] = 100

I tak dalej.


Po prostu powiedz mi, jak ukryć ostrzeżenie!

Rozważ prostą operację na kolumnie „A” w df. Wybranie „A” i podzielenie przez 2 spowoduje wyświetlenie ostrzeżenia, ale operacja będzie działać.

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

Istnieje kilka sposobów bezpośredniego wyciszenia tego ostrzeżenia:

  1. Zrobić deepcopy

    df2 = df[['A']].copy(deep=True)
    df2['A'] /= 2
  2. Zmianapd.options.mode.chained_assignment
    może być ustawiony None, "warn"albo "raise". "warn"jest wartością domyślną. Nonecałkowicie wyciszy to ostrzeżenie i "raise"rzuci SettingWithCopyError, uniemożliwiając przejście operacji.

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

@Peter Cotton w komentarzach wymyślił fajny sposób nieinwazyjnej zmiany trybu (zmodyfikowany z tej treści ) za pomocą menedżera kontekstu, aby ustawić tryb tylko tak długo, jak jest to wymagane, i zresetować go z powrotem do stan oryginalny po zakończeniu.

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

Wykorzystanie jest następujące:

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

Lub, aby podnieść wyjątek

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

„Problem XY”: Co robię źle?

Przez większość czasu użytkownicy próbują znaleźć sposoby na wyeliminowanie tego wyjątku, nie w pełni rozumiejąc, dlaczego został zgłoszony. To dobry przykład problemu XY , w którym użytkownicy próbują rozwiązać problem „Y”, który w rzeczywistości jest objawem głębiej zakorzenionego problemu „X”. Pytania będą zadawane w oparciu o typowe problemy, które napotykają to ostrzeżenie, a następnie zostaną przedstawione rozwiązania.

Pytanie 1
Mam ramkę danych

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

Chcę przypisać wartości w kolumnie „A”> 5 do 1000. Moje oczekiwane wyniki to

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

Niewłaściwy sposób to zrobić:

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

Właściwy sposób za pomocą loc:

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


Pytanie 2 1
Próbuję ustawić wartość w komórce (1, „D”) na 12345. Moje oczekiwane wyjście to

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

Próbowałem różnych sposobów dostępu do tej komórki, takich jak df['D'][1]. Jak najlepiej to zrobić?

1. To pytanie nie jest ściśle związane z ostrzeżeniem, ale dobrze jest zrozumieć, jak prawidłowo wykonać tę konkretną operację, aby uniknąć sytuacji, w których ostrzeżenie mogłoby się pojawić w przyszłości.

Aby to zrobić, możesz użyć dowolnej z następujących metod.

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


Pytanie 3
Próbuję rozdzielić wartości w oparciu o jakiś warunek. Mam DataFrame

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

Chciałbym przypisać wartości w „D” do 123 tak, aby „C” == 5. Próbowałem

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

Co wydaje się w porządku, ale wciąż dostaję SettingWithCopyWarning! Jak to naprawić?

Prawdopodobnie jest to prawdopodobnie spowodowane kodem znajdującym się wyżej w potoku. Czy stworzyłeś df2z czegoś większego, takiego jak

df2 = df[df.A > 5]

? W takim przypadku indeksowanie logiczne zwróci widok, więc df2odniesie się do oryginału. To, co musisz zrobić, to przypisać df2do kopii :

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


Pytanie 4
Próbuję usunąć z miejsca kolumnę „C”

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

Ale za pomocą

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

Zgłasza SettingWithCopyWarning. Dlaczego to się dzieje?

Wynika to z faktu, że df2musiał zostać utworzony jako widok z innej operacji krojenia, takiej jak

df2 = df[df.A > 5]

Rozwiązaniem tego problemu jest albo zrobić copy()z dflub wykorzystania loc, jak poprzednio.


7
PS: Daj mi znać, jeśli twoja sytuacja nie jest uwzględniona w liście pytań sekcji 3. Poprawię swój post.
cs95

150

Ogólnie rzecz biorąc, chodzi o SettingWithCopyWarningto, aby pokazać użytkownikom (a zwłaszcza nowym użytkownikom), że mogą działać na kopii, a nie na oryginale, jak myślą. Nie fałszywe pozytywy (IOW, jeśli wiesz, co robisz, to może być ok ). Jedną z możliwości jest po prostu wyłączenie ostrzeżenia (domyślnie ostrzegaj ), jak sugeruje @Garrett.

Oto kolejna opcja:

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

Możesz ustawić is_copyflagę na False, która skutecznie wyłączy sprawdzanie dla tego obiektu :

In [5]: dfa.is_copy = False

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

Jeśli wyraźnie skopiujesz, nie pojawi się żadne dodatkowe ostrzeżenie:

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

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

Kod, który pokazuje powyższy OP, chociaż jest zgodny z prawdą i prawdopodobnie coś, co również robię, jest technicznie uzasadnieniem tego ostrzeżenia, a nie fałszywym pozytywem. Innym sposobem na brak ostrzeżenia byłoby wykonanie operacji wyboru za pomocą reindex, np

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

Lub,

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

Dzięki za informacje i dyskusję, po prostu wyłączam ostrzeżenie, aby konsola milczała na ten temat. Brzmi jak widok i tabela w bazie danych SQL. Muszę dowiedzieć się więcej o korzyściach związanych z wprowadzeniem koncepcji „kopiowania”, ale IMHO jest nieco ciężarem, aby zająć się subtelną różnicą składni semantycznej ...
bigbug

19
Zgadzam się z copy (); jest jasne i naprawiło mój problem (co było fałszywie pozytywne).
rdchambers

5
po aktualizacji 0.16widzę o wiele więcej fałszywych alarmów, problem z fałszywymi alarmami polega na tym, że uczy się go ignorować, nawet jeśli czasem jest to uzasadnione.
dashy

3
@ dashesy brakuje Ci punktu. czasem może nawet przez większość czasu może działać. Ale może się tak zdarzyć na przykład, jeśli ramka jest większa / mniejsza lub jeśli dodasz kolumnę mówiącą o innym typie, że nie działa. O to chodzi. Robisz coś, co może działać, ale nie jest gwarantowane. To bardzo różni się od ostrzeżeń o rezygnacji. Jeśli chcesz nadal z niego korzystać i działa, świetnie. Ale bądź ostrzeżony.
Jeff

3
@Jeff ma teraz sens, więc jest to undefinedzachowanie. Jeśli cokolwiek, powinno to wygenerować błąd (aby uniknąć pułapek C), ponieważ apijest zamrożone, obecne zachowanie ostrzeżenia ma sens dla kompatybilności wstecznej. I sprawię, że rzucą, aby złapać je jako błędy w moim kodzie produkcyjnym ( warnings.filterwarnings('error', r'SettingWithCopyWarning). Również sugestia użycia .locczasami nie pomaga (jeśli jest w grupie).
dashy

41

Ostrzeżenie przed kopiowaniem ramki danych Pandy

Kiedy idziesz i robisz coś takiego:

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

pandas.ix w takim przypadku zwraca nową, autonomiczną ramkę danych.

Wszelkie wartości, które zdecydujesz się zmienić w tej ramce danych, nie zmienią oryginalnej ramki danych.

Właśnie przed tym ostrzegają cię pandy.


Dlaczego .ixto zły pomysł

.ixObiekt stara się zrobić więcej niż jedną rzecz, a dla każdego, kto czytał coś o czystego kodu, jest to silny zapach.

Biorąc pod uwagę tę ramkę danych:

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

Dwa zachowania:

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

Zachowanie pierwsze: dfcopyjest teraz samodzielną ramką danych. Zmiana nie zmieni siędf

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

Zachowanie drugie: Zmienia oryginalną ramkę danych.


Użyj .loczamiast tego

Twórcy pand rozpoznali, że .ixobiekt był śmierdzący [spekulacyjnie] i dlatego stworzyli dwa nowe obiekty, które pomagają w przystąpieniu i przypisaniu danych. (Druga istota .iloc)

.loc jest szybszy, ponieważ nie próbuje utworzyć kopii danych.

.loc ma na celu zmodyfikowanie istniejącej ramki danych w miejscu, co jest bardziej wydajne pod względem pamięci.

.loc jest przewidywalny, ma jedno zachowanie.


Rozwiązanie

W przykładzie kodu ładujesz duży plik z dużą ilością kolumn, a następnie modyfikujesz go tak, aby był mniejszy.

Ta pd.read_csvfunkcja może ci w tym pomóc, a także znacznie przyspieszyć ładowanie pliku.

Zamiast tego robić

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

Zrób to

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

Spowoduje to jedynie odczytanie interesujących Cię kolumn i odpowiednie nazwanie ich. Nie trzeba używać złego .ixprzedmiotu do robienia magicznych rzeczy.


„Twórcy pand rozpoznali, że obiekt .ix był śmierdzący [spekulacyjnie] i dlatego stworzyli dwa nowe obiekty” - co to takiego?
jf328,

3
@ jf328 .iloc Myślę
Brian Bien

1
Tak jest .iloc. Są to dwie podstawowe metody indeksowania struktur danych pand. Przeczytaj więcej w dokumentacji.
Ninjakannon

Jak należy zastąpić kolumnę DataFrame znacznikami czasu w kolumnie z obiektem lub ciągiem daty / godziny?
boldnik

@boldnik Sprawdź tę odpowiedź stackoverflow.com/a/37453925/3730397
firelynx

20

Tutaj odpowiadam bezpośrednio na pytanie. Jak sobie z tym poradzić?

Zrób .copy(deep=False)po krojeniu. Zobacz pandas.DataFrame.copy .

Zaraz, czy plasterek nie zwraca kopii? W końcu to właśnie próbuje ostrzec komunikat ostrzegawczy? Przeczytaj długą odpowiedź:

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

To daje ostrzeżenie:

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

To nie:

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

Zarówno df0i df1DataFrameobiekty, ale coś o nich jest inny, który umożliwia pandy wydrukować ostrzeżenie. Dowiedzmy się, co to jest.

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

Za pomocą wybranego narzędzia różnicowego zobaczysz, że poza kilkoma adresami jedyną istotną różnicą jest:

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

Metodą decydującą o tym, czy ostrzec, jest DataFrame._check_setitem_copykontrola _is_copy. Więc proszę bardzo. Zrób copytak, aby twoja DataFrame nie była _is_copy.

Ostrzeżenie sugeruje użycie .loc, ale jeśli użyjesz .locgo w ramce _is_copy, nadal otrzymasz to samo ostrzeżenie. Zwodniczy? Tak. Denerwujący? Ty stawiasz Pomocny? Potencjalnie, gdy używane jest przypisanie łańcuchowe. Ale nie może poprawnie wykryć przypisania łańcucha i drukuje ostrzeżenie bez rozróżnienia.


11

Ten temat jest naprawdę mylący z Pandami. Na szczęście ma stosunkowo proste rozwiązanie.

Problem polega na tym, że nie zawsze jest jasne, czy operacje filtrowania danych (np. Loc) zwracają kopię lub widok DataFrame. Dalsze użycie takiej filtrowanej DataFrame może być mylące.

Proste rozwiązanie to (chyba że musisz pracować z bardzo dużymi zestawami danych):

Ilekroć musisz zaktualizować jakiekolwiek wartości, zawsze upewnij się, że niejawnie skopiujesz DataFrame przed przypisaniem.

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)

Istnieje literówka: domyślnie powinno być jawnie
s9527

7

Aby usunąć wszelkie wątpliwości, moim rozwiązaniem było wykonanie głębokiej kopii wycinka zamiast zwykłej kopii. Może to nie mieć zastosowania w zależności od kontekstu (ograniczenia pamięci / rozmiar wycinka, potencjał obniżenia wydajności - szczególnie jeśli kopia występuje w pętli, jak to zrobiłem dla mnie itp.)

Dla jasności, oto ostrzeżenie, które otrzymałem:

/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

Ilustracja

Miałem wątpliwości, czy ostrzeżenie zostało rzucone z powodu kolumny, którą upuszczałem na kopię wycinka. Chociaż technicznie nie próbował ustawić wartości w kopii wycinka, była to modyfikacja kopii wycinka. Poniżej znajdują się (uproszczone) kroki, które podjąłem, aby potwierdzić to podejrzenie, mam nadzieję, że pomoże to tym z nas, którzy próbują zrozumieć ostrzeżenie.

Przykład 1: upuszczenie kolumny na oryginale wpływa na kopię

Wiedzieliśmy to już, ale jest to zdrowe przypomnienie. To nie to, co jest o ostrzeżenie.

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

Możliwe jest uniknięcie zmian wprowadzonych na df1, aby wpłynąć na 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

Przykład 2: upuszczenie kolumny na kopii może wpłynąć na oryginał

To faktycznie ilustruje ostrzeżenie.

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

Można uniknąć zmian wprowadzonych w df2, aby wpłynąć na 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

Twoje zdrowie!


4

To powinno działać:

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

4

Niektórzy mogą chcieć po prostu ukryć ostrzeżenie:

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

Jeśli plasterek został przypisany do zmiennej i chcesz ustawić ją za pomocą zmiennej, jak pokazano poniżej:

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

I nie chcesz używać rozwiązania Jeffsa, ponieważ obliczanie warunków df2jest zbyt długie lub z innego powodu, możesz użyć następujących opcji:

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

df2.index.tolist() zwraca indeksy ze wszystkich pozycji w df2, które zostaną następnie wykorzystane do ustawienia kolumny B w oryginalnej ramce danych.


to jest 9 razy droższe niż df [„B”] = wartość
Claudiu Creanga

Czy możesz to dokładniej wyjaśnić @ClaudiuCreanga?
gies0r

2

Dla mnie ten problem wystąpił w następującym> uproszczonym <przykładzie. Byłem też w stanie go rozwiązać (mam nadzieję, że z poprawnym rozwiązaniem):

stary kod z ostrzeżeniem:

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

To wydrukowało ostrzeżenie dla linii old_row[field] = new_row[field]

Ponieważ wiersze w metodzie update_row są w rzeczywistości typem Series , zastąpiłem wiersz:

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

to znaczy metoda uzyskiwania dostępu / wyszukiwania dla Series. Chociaż oba działają dobrze, a wynik jest taki sam, w ten sposób nie muszę wyłączać ostrzeżeń (= zachowaj je dla innych problemów z indeksowaniem łańcucha gdzie indziej).

Mam nadzieję, że to może komuś pomóc.


2

Uważam, że można uniknąć całego problemu w ten sposób:

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

Korzystanie z Przypisz. Z dokumentacji : Przypisz nowe kolumny do DataFrame, zwracając nowy obiekt (kopię) ze wszystkimi oryginalnymi kolumnami oprócz nowych.

Zobacz artykuł Toma Augspurgera na temat łączenia łańcuchów w pandach: https://tomaugspurger.github.io/method-chaining


2

Dalsze pytanie / uwaga dla początkujących

Być może wyjaśnienie dla innych początkujących, takich jak ja (pochodzę z R, która wydaje się działać nieco inaczej pod maską). Poniższy nieszkodliwy i funkcjonalny kod generował ostrzeżenie SettingWithCopy i nie mogłem zrozumieć, dlaczego. Przeczytałem i zrozumiałem wydanie z „indeksowaniem łańcuchowym”, ale mój kod nie zawiera:

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

Ale później, o wiele za późno, przyjrzałem się nazwie funkcji plot ():

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

Zatem „df” nie jest ramką danych, ale obiektem, który w jakiś sposób pamięta, że ​​został utworzony przez indeksowanie ramki danych (a więc to widok?), Który utworzyłby linię w plot ()

 df['target'] = ...

równoważny

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

czyli indeksowanie łańcuchowe. Czy dobrze to zrozumiałem?

Tak czy siak,

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

naprawione.


1

Ponieważ pytanie to zostało już w pełni wyjaśnione i omówione w istniejących odpowiedziach, przedstawię tylko schludne pandaspodejście do menedżera kontekstu przy użyciu pandas.option_context(linki do dokumentów i przykład ) - absolutnie nie ma potrzeby tworzenia niestandardowej klasy ze wszystkimi metodami dundera i innymi dzwonkami i gwizdy.

Najpierw sam kod menedżera kontekstu:

from contextlib import contextmanager

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

Następnie przykład:

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

Warto zauważyć, że oba approches nie modyfikują a, co jest dla mnie nieco zaskakujące, a nawet płytka kopia df .copy(deep=False)uniemożliwiłaby wygenerowanie tego ostrzeżenia (o ile rozumiem, płytka kopia powinna przynajmniej zmodyfikować a, ale nie zmienia „t. pandasmagia.).


hmmm, rozumiem to, jeśli ostrzeżenie podniesione coś jest oczywiście złe, więc lepiej unikać ostrzeżeń, jak je stłumić, co tak myślisz?
jezrael

Nie, ostrzeżenie to tylko ostrzeżenie. Tak jak tutaj, ostrzega cię, że coś może być nie tak, co dobrze wiedzieć, ale jeśli wiesz, co i dlaczego robisz, możesz całkowicie usunąć niektóre z nich. Zobacz wyjaśnienie w stackoverflow.com/a/20627316/4272484 na temat ponownego przypisywania referencji.
m-dz

1

Ten problem występował .apply()podczas przypisywania nowej ramki danych z wcześniej istniejącej ramki danych, w której użyłem tej .query()metody. Na przykład:

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

Zwróciłby ten błąd. Naprawą, która wydaje się rozwiązać problem w tym przypadku, jest zmiana na:

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

Jednak NIE jest to efektywne, szczególnie przy użyciu dużych ramek danych, ze względu na konieczność wykonania nowej kopii.

Jeśli używasz tej .apply()metody do generowania nowej kolumny i jej wartości, poprawką, która rozwiązuje błąd i jest bardziej wydajna, jest dodanie .reset_index(drop=True):

prop_df = df.query('column == "value"').reset_index(drop=True)
prop_df['new_column'] = prop_df.apply(function, axis=1)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.