寫入時複製 (CoW)#
注意
寫入時複製將成為 pandas 3.0 的預設值。我們建議 現在就啟用,以受益於所有改進。
寫入時複製最初於 1.5.0 版中引入。從 2.0 版開始,透過 CoW 而實現的大部分最佳化都已實作並獲得支援。從 pandas 2.1 開始,支援所有可能的最佳化。
CoW 將在 3.0 版中預設啟用。
CoW 將帶來更可預測的行為,因為不可能使用一個陳述式更新多個物件,例如索引運算或方法不會有副作用。此外,透過盡可能延後複製,平均效能和記憶體使用量將會提升。
先前的行為#
pandas 索引行為難以理解。有些運算會傳回檢視,而其他會傳回複製。根據運算結果,變異一個物件可能會意外變異另一個物件
In [1]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [2]: subset = df["foo"]
In [3]: subset.iloc[0] = 100
In [4]: df
Out[4]:
foo bar
0 100 4
1 2 5
2 3 6
變異 子集
,例如更新其值,也會更新 df
。確切的行為難以預測。寫入時複製解決了意外修改多個物件的問題,它明確禁止這種情況。啟用 CoW 後,df
保持不變
In [5]: pd.options.mode.copy_on_write = True
In [6]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [7]: subset = df["foo"]
In [8]: subset.iloc[0] = 100
In [9]: df
Out[9]:
foo bar
0 1 4
1 2 5
2 3 6
以下各節將說明這表示什麼,以及它如何影響現有應用程式。
移轉至寫入時複製#
寫入時複製將是 pandas 3.0 中的預設值和唯一模式。這表示使用者需要將其程式碼移轉為符合 CoW 規則。
pandas 中的預設模式會針對某些案例提出警告,這些案例將主動變更行為,進而變更使用者的預期行為。
我們新增了另一個模式,例如
pd.options.mode.copy_on_write = "warn"
這會對任何會因為 CoW 而改變行為的運算發出警告。我們預期這個模式會非常吵雜,因為許多我們不預期會影響使用者的案例也會發出警告。我們建議檢查這個模式並分析警告,但不需要處理所有這些警告。下列清單的前兩項是讓現有程式碼與 CoW 搭配運作時唯一需要處理的案例。
以下幾項說明使用者可見的變更
鍊式指定永遠不會運作
loc
應作為替代方案使用。查看鍊式指定區段以取得更多詳細資訊。
存取 pandas 物件的底層陣列會傳回唯讀檢視
In [10]: ser = pd.Series([1, 2, 3])
In [11]: ser.to_numpy()
Out[11]: array([1, 2, 3])
這個範例傳回一個 NumPy 陣列,該陣列是 Series 物件的檢視。這個檢視可以修改,因此也可以修改 pandas 物件。這不符合 CoW 規則。傳回的陣列設定為不可寫入,以防止這種行為。建立這個陣列的副本允許修改。如果您不再關心 pandas 物件,也可以再次讓陣列可寫入。
查看有關唯讀 NumPy 陣列的區段以取得更多詳細資訊。
一次只更新一個 pandas 物件
下列程式碼片段在沒有 CoW 的情況下更新 df
和 subset
In [12]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [13]: subset = df["foo"]
In [14]: subset.iloc[0] = 100
In [15]: df
Out[15]:
foo bar
0 1 4
1 2 5
2 3 6
這在 CoW 中不再可行,因為 CoW 規則明確禁止這一點。這包括將單一欄位更新為 Series
,並依賴變更傳播回父 DataFrame
。如果需要這種行為,可以用 loc
或 iloc
將此陳述重寫成單一陳述。 DataFrame.where()
是此案例的另一種合適替代方案。
使用內部方法更新從 DataFrame
選取的欄位也不再可行。
In [16]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [17]: df["foo"].replace(1, 5, inplace=True)
In [18]: df
Out[18]:
foo bar
0 1 4
1 2 5
2 3 6
這是鏈結指定另一種形式。這通常可以改寫成 2 種不同形式
In [19]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [20]: df.replace({"foo": {1: 5}}, inplace=True)
In [21]: df
Out[21]:
foo bar
0 5 4
1 2 5
2 3 6
另一種替代方案是不使用 inplace
In [22]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [23]: df["foo"] = df["foo"].replace(1, 5)
In [24]: df
Out[24]:
foo bar
0 5 4
1 2 5
2 3 6
建構函式現在預設複製 NumPy 陣列
除非另有指定,否則 Series 和 DataFrame 建構函式現在會預設複製 NumPy 陣列。這項變更的目的是避免在 NumPy 陣列在 pandas 外部內部變更時,變更 pandas 物件。您可以設定 copy=False
來避免這種複製。
說明#
CoW 表示任何以任何方式從其他 DataFrame 或 Series 衍生的 DataFrame 或 Series 始終表現為一份拷貝。因此,我們只能透過修改物件本身來變更物件的值。CoW 不允許更新與其他 DataFrame 或 Series 物件共用資料的 DataFrame 或 Series。
這在修改值時避免了副作用,因此,大多數方法可以避免實際拷貝資料,並僅在必要時觸發拷貝。
下列範例將透過 CoW 就地運作
In [25]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [26]: df.iloc[0, 0] = 100
In [27]: df
Out[27]:
foo bar
0 100 4
1 2 5
2 3 6
物件 df
與任何其他物件不共用任何資料,因此在更新值時不會觸發拷貝。相反地,下列運算會觸發 CoW 下的資料拷貝
In [28]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [29]: df2 = df.reset_index(drop=True)
In [30]: df2.iloc[0, 0] = 100
In [31]: df
Out[31]:
foo bar
0 1 4
1 2 5
2 3 6
In [32]: df2
Out[32]:
foo bar
0 100 4
1 2 5
2 3 6
reset_index
傳回一份 CoW 的延遲拷貝,而它會在沒有 CoW 的情況下拷貝資料。由於兩個物件 df
和 df2
共用相同的資料,因此在修改 df2
時會觸發拷貝。物件 df
仍具有與最初相同的數值,而 df2
已被修改。
如果在執行 reset_index
運算後不再需要物件 df
,您可以透過將 reset_index
的輸出指定給同一個變數,來模擬就地運算。
In [33]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [34]: df = df.reset_index(drop=True)
In [35]: df.iloc[0, 0] = 100
In [36]: df
Out[36]:
foo bar
0 100 4
1 2 5
2 3 6
初始物件在重新指派 reset_index
的結果後會超出範圍,因此 df
沒有與任何其他物件共用資料。修改物件時不需要複製。這通常適用於 寫入時複製最佳化 中列出的所有方法。
先前在操作檢視時,檢視和父物件會被修改
In [37]: with pd.option_context("mode.copy_on_write", False):
....: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
....: view = df[:]
....: df.iloc[0, 0] = 100
....:
In [38]: df
Out[38]:
foo bar
0 100 4
1 2 5
2 3 6
In [39]: view
Out[39]:
foo bar
0 100 4
1 2 5
2 3 6
寫入時複製會在 df
變更時觸發複製,以避免同時變異 view
In [40]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [41]: view = df[:]
In [42]: df.iloc[0, 0] = 100
In [43]: df
Out[43]:
foo bar
0 100 4
1 2 5
2 3 6
In [44]: view
Out[44]:
foo bar
0 1 4
1 2 5
2 3 6
鏈式指派#
鏈式指派是指透過兩個後續索引操作來更新物件的技術,例如
In [45]: with pd.option_context("mode.copy_on_write", False):
....: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
....: df["foo"][df["bar"] > 5] = 100
....: df
....:
當欄位 bar
大於 5 時,欄位 foo
會被更新。這違反了寫入時複製原則,因為它必須在一個步驟中修改檢視 df["foo"]
和 df
。因此,在啟用寫入時複製時,鏈式指派將永遠無法運作,並會引發 ChainedAssignmentError
警告
In [46]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [47]: df["foo"][df["bar"] > 5] = 100
使用寫入時複製,這可以使用 loc
來完成。
In [48]: df.loc[df["bar"] > 5, "foo"] = 100
唯讀 NumPy 陣列#
存取 DataFrame 的底層 NumPy 陣列,如果陣列與初始的 DataFrame 共享資料,將會傳回唯讀陣列
如果初始的 DataFrame 包含多個陣列,則陣列為拷貝
In [49]: df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
In [50]: df.to_numpy()
Out[50]:
array([[1. , 1.5],
[2. , 2.5]])
如果 DataFrame 僅包含一個 NumPy 陣列,則陣列與 DataFrame 共享資料
In [51]: df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [52]: df.to_numpy()
Out[52]:
array([[1, 3],
[2, 4]])
此陣列為唯讀,表示無法就地修改
In [53]: arr = df.to_numpy()
In [54]: arr[0, 0] = 100
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[54], line 1
----> 1 arr[0, 0] = 100
ValueError: assignment destination is read-only
Series 也是如此,因為 Series 總是包含單一陣列。
有兩種可能的解決方案
如果您想避免更新與陣列共享記憶體的 DataFrame,請手動觸發拷貝。
讓陣列可寫入。這是一個效能較佳的解決方案,但會規避寫入時拷貝規則,因此應謹慎使用。
In [55]: arr = df.to_numpy()
In [56]: arr.flags.writeable = True
In [57]: arr[0, 0] = 100
In [58]: arr
Out[58]:
array([[100, 3],
[ 2, 4]])
應避免的模式#
如果您在就地修改一個物件時,兩個物件共享相同的資料,則不會執行防禦性拷貝。
In [59]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
In [60]: df2 = df.reset_index(drop=True)
In [61]: df2.iloc[0, 0] = 100
這會建立兩個共享資料的物件,因此 setitem 作業會觸發拷貝。如果初始物件 df
不再需要,則不需要這樣做。只要重新指派給同一個變數,就會使物件持有的參考失效。
In [62]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
In [63]: df = df.reset_index(drop=True)
In [64]: df.iloc[0, 0] = 100
此範例不需要拷貝。建立多個參考會讓不必要的參考保持作用,因此會損害寫入時拷貝的效能。
寫入時拷貝最佳化#
一種新的延遲複製機制,直到有問題的物件被修改,而且僅當此物件與其他物件共用資料時,才會延遲複製。此機制已新增至不需要複製基礎資料的方法。熱門範例包括 DataFrame.drop()
的 axis=1
和 DataFrame.rename()
。
當啟用寫入時複製時,這些方法會傳回檢視,與一般執行相比,這提供了顯著的效能改善。
如何啟用 CoW#
寫入時複製可透過設定選項 copy_on_write
來啟用。此選項可透過以下任一方式在 __全域__ 中開啟
In [65]: pd.set_option("mode.copy_on_write", True)
In [66]: pd.options.mode.copy_on_write = True