熊猫:知道操作何时影响原始数据帧



我喜欢熊猫,并且已经使用它多年了,并且非常有信心我很好地处理了如何子集数据帧并适当地处理视图与副本(尽管我使用了很多断言来确定)。我也知道有很多关于SettingWithCopyWarning的问题,例如如何处理Pandas中的SettingWithCopyWarning? 以及最近一些关于在它发生时绕开你的头的很棒的指南,例如 了解熊猫中的设置与复制警告.

但我也知道像这个答案中的引用这样的特定内容不再出现在最新的文档(0.22.0),并且多年来许多东西已被弃用(导致一些不合适的旧 SO 答案),并且事情正在继续变化。

最近,在教熊猫完成新手的非常基本的一般Python知识之后,比如避免链式索引(并使用.iloc/.loc),我仍然在努力提供一般的经验法则,以了解何时关注SettingWithCopyWarning很重要(例如,何时可以安全地忽略它)。

我个人发现,根据某些规则(例如切片或布尔运算)对数据帧进行子集化,然后独立于原始数据帧修改该子集的特定模式比文档建议的要常见得多。在这种情况下,我们希望修改副本而不是原始副本,并且警告对新手来说是令人困惑/可怕的。

我知道提前知道何时返回视图与副本并非易事,例如,
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://stackoverflow.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://stackoverflow.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()的布尔索引创建新数据帧时,不会更改原始数据帧

这是熊猫中有些令人困惑甚至令人沮丧的部分,但在大多数情况下,如果您遵循一些简单的工作流程规则,您真的不必担心这一点。 特别要注意的是,当您有两个数据帧时,这里只有两种一般情况,其中一个是另一个数据的子集。

在这种情况下,Python的禅宗规则"显式优于隐式"是一个很好的指导方针。

情况 A:对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的蒙版。 有点不幸的是,由于混合了lociloc,您无法直接翻译上述代码,这对于此示例来说很好,尽管对于实际使用来说可能不切实际。

优点是您可以编写非常简单且可读的代码。 这是上面示例 #2 的替代版本,其中df2实际上只是df1的屏蔽版本。 但是,如果列"C"== 10,我将更改,而不是通过iloc进行更改。

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

现在,如果您打印df1df1[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

| 可乐| 刨乙

0| 123456789 | 123456789

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

| 可乐| 刨乙

0| 3456789 | 123456789

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

| 可乐| 刨乙

0| 56789 | 123456789

如前所述,修复是更改第二个块:

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

你只需要用.iat[0,1]替换.iloc[0,1]

更一般地说,如果您只想修改一个元素,则应使用.iat.at方法。相反,当您一次修改更多元素时,您应该使用.loc.iloc方法。

以这种方式做熊猫不会发出任何警告。

最新更新