Copy-on-Write (CoW)#
注
Copy-on-Writeはpandas 3.0でデフォルトになります。すべての改善の恩恵を受けるために、今すぐ有効にすることをお勧めします。
Copy-on-Writeはバージョン1.5.0で初めて導入されました。バージョン2.0からは、CoWによって可能になる最適化のほとんどが実装され、サポートされています。すべての可能な最適化はpandas 2.1からサポートされています。
CoWはバージョン3.0でデフォルトで有効になります。
CoWは、インデックス操作やメソッドなどが副作用を持たないため、1つのステートメントで複数のオブジェクトを更新できないため、より予測可能な動作につながります。さらに、可能な限りコピーを遅延させることで、平均的なパフォーマンスとメモリ使用量が向上します。
以前の動作#
pandasのインデックス動作は理解するのが難しいです。一部の操作はビューを返し、他の操作はコピーを返します。操作の結果に応じて、1つのオブジェクトを変更すると、誤って別のオブジェクトを変更する可能性があります。
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
subsetを変更すると、たとえばその値を更新すると、dfも更新されます。正確な動作は予測が困難です。Copy-on-Writeは、誤って複数のオブジェクトを変更する問題を解決し、これを明示的に禁止します。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
以下のセクションでは、これが何を意味し、既存のアプリケーションにどのように影響するかを説明します。
Copy-on-Writeへの移行#
Copy-on-Writeはpandas 3.0でデフォルトかつ唯一のモードになります。これは、ユーザーがCoWのルールに準拠するようにコードを移行する必要があることを意味します。
pandasのデフォルトモードでは、動作を積極的に変更し、したがってユーザーが意図した動作を変更する特定のケースについて警告が発せられます。
別のモードを追加しました。例:
pd.options.mode.copy_on_write = "warn"
CoWで動作が変更されるすべての操作について警告します。このモードは非常にノイジーになると予想されます。なぜなら、ユーザーに影響を与えるとは予想されない多くのケースでも警告が発せられるからです。このモードをチェックし、警告を分析することをお勧めしますが、これらの警告すべてに対処する必要はありません。既存のコードがCoWで機能するように対処する必要があるのは、以下のリストの最初の2つの項目だけです。
以下のいくつかの項目は、ユーザーに表示される変更を説明しています。
連鎖代入は機能しません
locを代替として使用する必要があります。詳細は連鎖代入のセクションを参照してください。
pandasオブジェクトの基になる配列にアクセスすると、読み取り専用のビューが返されます
In [10]: ser = pd.Series([1, 2, 3])
In [11]: ser.to_numpy()
Out[11]: array([1, 2, 3])
この例は、SeriesオブジェクトのビューであるNumPy配列を返します。このビューは変更可能であり、pandasオブジェクトも変更する可能性があります。これはCoWルールに準拠していません。この動作から保護するために、返される配列は書き込み不可に設定されます。この配列のコピーを作成することで、変更が可能になります。pandasオブジェクトに関心がなくなった場合は、配列を再び書き込み可能にすることもできます。
詳細は読み取り専用NumPy配列に関するセクションを参照してください。
一度に更新されるpandasオブジェクトは1つだけです
以下のコードスニペットは、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は他のオブジェクトとデータを共有しません。オブジェクトを変更するときにコピーは不要です。これは、Copy-on-Writeの最適化にリストされているすべてのメソッドに一般的に当てはまります。
以前は、ビューを操作すると、ビューと親オブジェクトの両方が変更されました。
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
CoWは、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
連鎖代入#
連鎖代入とは、2つの連続するインデックス操作によってオブジェクトが更新される手法を指します。例:
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を一度に修正する必要があるため、CoWの原則に違反します。したがって、連鎖代入は一貫して機能せず、CoWが有効な場合は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についても同じことが言えます。
これには2つの潜在的な解決策があります。
配列とメモリを共有するDataFrameの更新を避けたい場合は、手動でコピーをトリガーします。
配列を書き込み可能にします。これはよりパフォーマンスの高い解決策ですが、Copy-on-Writeのルールを迂回するため、注意して使用する必要があります。
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]])
避けるべきパターン#
2つのオブジェクトが同じデータを共有している間に1つのオブジェクトをインプレースで変更しても、防御的なコピーは実行されません。
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
これにより、データを共有する2つのオブジェクトが作成され、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
この例ではコピーは不要です。複数の参照を作成すると、不要な参照が保持され続け、Copy-on-Writeのパフォーマンスを低下させます。
Copy-on-Writeの最適化#
新しい遅延コピーメカニズムは、問題のオブジェクトが変更された場合、かつそのオブジェクトが別のオブジェクトとデータを共有している場合にのみコピーを延期します。このメカニズムは、基になるデータのコピーを必要としないメソッドに追加されました。一般的な例としては、axis=1のDataFrame.drop()とDataFrame.rename()があります。
これらのメソッドは、Copy-on-Writeが有効な場合にビューを返し、通常の実行と比較して大幅なパフォーマンス向上をもたらします。
CoWを有効にする方法#
Copy-on-Writeは、設定オプションcopy_on_writeを通じて有効にできます。このオプションは、以下のいずれかの方法で__グローバルに__オンにすることができます。
In [65]: pd.set_option("mode.copy_on_write", True)
In [66]: pd.options.mode.copy_on_write = True