Панды: знать, когда операция влияет на исходный кадр данных

Я люблю панд и использую их годами, и чувствую себя уверенно. Я хорошо разбираюсь в том, как правильно подбирать кадры данных и как правильно обращаться с представлениями и копиями (хотя для уверенности я использую множество утверждений). Я также знаю, что было множество вопросов о SettingWithCopyWarning, например, как справиться с SettingWithCopyWarning в Pandas? и некоторые замечательные недавние руководства по наведению головы, когда это происходит, например, Понимание SettingWithCopyWarning в пандах.

Но я также знаю, что конкретные вещи, такие как цитата из этого ответа, больше не содержатся в самых последних документах (0.22.0) и что многие вещи устарели за эти годы (что привело к некоторым неподходящим старым SO-ответам), и что вещи продолжают меняться.

Недавно после обучения панд, чтобы дополнить новичков базовыми знаниями Python о таких вещах, как избегание цепной индексации (и использование .iloc / .loc), Я все еще изо всех сил пытался предоставить общие правила, чтобы знать, когда важно обратить внимание на SettingWithCopyWarning (например, когда это безопасно игнорировать).

Я лично обнаружил, что конкретный шаблон подмножества фрейма данных в соответствии с каким-то правилом (например, нарезкой или логической операцией) и последующего изменения этого подмножества, независимо от исходного фрейма данных, является гораздо более распространенной операцией, чем предполагают документы. В этой ситуации мы хотим изменить копию, а не оригинал, и предупреждение вводит в заблуждение / пугает новичков.

Я знаю, что нетрудно знать заранее, когда возвращается представление против копии, например
Какие правила использует Pandas для создания представления против копии?
Проверка, является ли фрейм данных копированием или просмотром в Pandas

Поэтому вместо этого я ищу ответ на более общий (дружественный для начинающих) вопрос: когда выполнение операции с подмножественным фреймом данных влияет на исходный фрейм данных, из которого он был создан, и когда они независимы?,

Ниже я создал несколько случаев, которые, на мой взгляд, кажутся разумными, но я не уверен, что есть "ошибка", которую я пропускаю, или есть какой-то более простой способ подумать / проверить это. Я надеялся, что кто-то может подтвердить, что мои интуиции относительно следующих вариантов использования верны, поскольку они относятся к моему вопросу выше.

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) Предупреждение: нет
Оригинал изменен: нет

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

2) Предупреждение: да (я не совсем понял, почему)
Оригинал изменен: нет

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

3) Предупреждение: да
Оригинал изменен: нет

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

4) Предупреждение: нет
Оригинал изменен: нет

# 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) Предупреждение: нет
Оригинал изменен: Да (смущает новичков, но имеет смысл)

# 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 При создании нового фрейма данных из исходного, изменение нового фрейма данных:
Изменит оригинал, когда для создания нового фрейма данных используется скалярное / слайсное индексирование с помощью.loc/.iloc.
Не изменит оригинал при логическом индексировании с помощью.loc, .query() , или же .copy() используется для создания нового кадра данных

4 ответа

Это несколько запутанная и даже разочаровывающая часть панд, но по большей части вам не нужно беспокоиться об этом, если вы следуете некоторым простым правилам рабочего процесса. В частности, обратите внимание, что здесь есть только два общих случая, когда у вас есть два кадра данных, один из которых является подмножеством другого.

Это тот случай, когда правило Zen of Python "явное лучше, чем неявное" является хорошим руководством для подражания.

Случай А: изменения в df2 НЕ должен влиять df1

Это тривиально, конечно. Вам нужны два полностью независимых фрейма данных, поэтому вы просто явно делаете копию:

df2 = df1.copy()

После этого все, что вы делаете для df2 влияет только df2 и не df1 и наоборот.

Случай B: изменения в df2 ТАКЖЕ должен влиять df1

В этом случае я не думаю, что есть один общий способ решить проблему, потому что это зависит от того, что именно вы пытаетесь сделать. Однако есть несколько стандартных подходов, которые довольно просты и не должны иметь никакой двусмысленности относительно того, как они работают.

Метод 1: Скопируйте df1 в df2, затем используйте df2 для обновления df1

В этом случае вы можете в основном выполнить преобразование один к одному из приведенных выше примеров. Вот пример № 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')

К сожалению, повторное слияние через append немного многословно там. Вы можете сделать это более аккуратно с помощью следующего, хотя у него есть побочный эффект преобразования целых чисел в числа с плавающей точкой.

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

Способ 2: использовать маску (не создавать df2 совсем)

Я думаю, что лучший общий подход здесь не заключается в создании df2 на всех, а скорее это будет замаскированная версия df1, К сожалению, вы не можете сделать прямой перевод приведенного выше кода из-за его смешивания loc а также iloc что хорошо для этого примера, хотя, вероятно, нереально для реального использования.

Преимущество в том, что вы можете написать очень простой и читаемый код. Вот альтернативная версия примера № 2 выше, где df2 на самом деле просто замаскированная версия df1, Но вместо того, чтобы менять через iloc Я изменю, если столбец "C" == 10.

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

Теперь, если вы печатаете df1 или же df1[df2_mask] вы увидите этот столбец "B" = 100 для первой строки каждого кадра данных. Очевидно, что это не очень удивительно, но это является неотъемлемым преимуществом следования "явное лучше, чем неявное".

У меня такое же сомнение, я безуспешно искал этот ответ в прошлом. Итак, теперь я просто подтверждаю, что оригинал не меняется, и использую этот код в программе при начале удаления предупреждений:

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

Вот и пример простых эффектов того, почему вам нужен .copy()

Когда второй блок кода ниже выполняется в первый раз, он отрезает «12», во второй раз он отрезает «34» и т. д.

      df1=pd.DataFrame({'colA':['123456789'],'colB':['123456789']})
df1

| колА| colB

0| 123456789 | 123456789


| колА| colB

0| 3456789 | 123456789

      df2=df1
df2['col1'] = df2['col1'].[2:]

| колА| colB

0| 56789 | 123456789

Исправление, как уже упоминалось, заключается в изменении второго блока:

      df2=df1.copy()
df2['col1'] = df2['col1'].[2:]

Вам нужно только заменить .iloc[0,1] с .iat[0,1],

В общем, если вы хотите изменить только один элемент, вы должны использовать .iat или же .at метод. Вместо этого, когда вы изменяете больше элементов одновременно, вы должны использовать .loc или же .iloc методы.

Делая так, панды не должны предупреждать.

Другие вопросы по тегам