Pandas 如何判断生成视图还是副本的规则?
我对Pandas在决定从数据框中选择的内容是原始数据框的副本还是视图时的规则感到困惑。
比如说,如果我有:
df = pd.DataFrame(np.random.randn(8,8), columns=list('ABCDEFGH'), index=range(1,9))
我知道query
会返回一个副本,所以像下面这样的操作:
foo = df.query('2 < index <= 5')
foo.loc[:,'E'] = 40
不会对原始数据框df
产生影响。我也明白,标量或命名切片会返回一个视图,因此对这些切片的赋值,比如:
df.iloc[3] = 70
或者
df.ix[1,'B':'E'] = 222
会改变df
。但是当涉及到更复杂的情况时,我就搞不清楚了。例如:
df[df.C <= df.B] = 7654321
会改变df
,但
df[df.C <= df.B].ix[:,'B':'E']
则不会。
有没有什么简单的规则是Pandas在使用的,而我却没有注意到?这些特定情况下发生了什么;特别是,我该如何更改数据框中满足特定查询的所有值(或部分值),就像我在上面的最后一个例子中尝试做的那样?
注意:这和这个问题不一样;我也看过文档,但并没有明白。我还浏览了关于这个主题的“相关”问题,但我仍然没有找到Pandas使用的简单规则,以及我该如何应用它来——比如说——修改数据框中满足特定查询的值(或部分值)。
3 个回答
这里有件有趣的事情:
u = df
v = df.loc[:, :]
w = df.iloc[:,:]
z = df.iloc[0:, ]
前面三个看起来都是在提到df,但最后一个却不是!
自从pandas 1.5.0版本开始,pandas引入了一种叫做“写时复制”(Copy-on-Write,简称CoW)的模式。这种模式让从另一个数据框(dataframe)或序列(Series)派生出来的数据框/序列的行为像是复制了一样。开启这个模式后,只有在数据与其他数据框/序列共享时,才会创建一个副本。如果没有开启CoW,像切片这样的操作会创建一个视图(view),这意味着如果你修改了新数据框,原来的数据框也会意外地被改变;而开启CoW后,切片操作会创建一个副本。
pd.options.mode.copy_on_write = False # disable CoW (this is the default as of pandas 2.0)
df = pd.DataFrame({'A': range(4), 'B': list('abcd')})
df1 = df.iloc[:4] # view
df1.iloc[0] = 100
df.equals(df1) # True <--- df changes together with df1
pd.options.mode.copy_on_write = True # enable CoW (this is planned to be the default by pandas 3.0)
df = pd.DataFrame({'A': range(4), 'B': list('abcd')})
df1 = df.iloc[:4] # copy because data is shared
df1.iloc[0] = 100
df.equals(df1) # False <--- df doesn't change when df1 changes
这样做的一个结果是,使用CoW时,pandas的操作会更快。在下面的例子中,第一种情况(CoW未开启)中,所有的中间步骤都会创建副本,而在第二种情况(CoW开启)中,副本只在赋值时创建(所有中间步骤都是在视图上进行的)。你可以看到,由于这个原因,运行时间是有差别的(在第二种情况下,数据没有被不必要地复制)。
df = pd.DataFrame({'A': range(1_000_000), 'B': range(1_000_000)})
%%timeit
with pd.option_context('mode.copy_on_write', False): # disable CoW in a context manager
df1 = df.add_prefix('col ').set_index('col A').rename_axis('index col').reset_index()
# 30.5 ms ± 561 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit
with pd.option_context('mode.copy_on_write', True): # enable CoW in a context manager
df2 = df.add_prefix('col ').set_index('col A').rename_axis('index col').reset_index()
# 18 ms ± 513 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这里有一些规则,后面的操作会覆盖前面的:
所有操作都会生成一个副本。
如果提供了
inplace=True
,那么会直接在原地修改;不过并不是所有操作都支持这个。使用设置索引的方式,比如
.loc/.iloc/.iat/.at
,会直接在原地修改。如果你用索引获取单一数据类型的对象,几乎总是会得到一个视图(不过这取决于内存的布局,有时候可能不是,所以这并不总是可靠)。这样做主要是为了提高效率。(上面的例子是关于
.query
的;这个操作总是会返回一个副本,因为它是通过numexpr
进行计算的。)如果你用索引获取多个数据类型的对象,结果总是一个副本。
你提到的 链式索引
df[df.C <= df.B].loc[:,'B':'E']
并不能保证有效(所以你绝对不应该这样做)。
相反,你应该这样做:
df.loc[df.C <= df.B, 'B':'E']
这样会更快,而且总是有效。
链式索引实际上是两个独立的 Python 操作,因此 pandas 无法可靠地拦截(你经常会看到 SettingWithCopyWarning
,但这也不是100%可检测的)。你提到的 开发文档 提供了更详细的解释。