コピーオンライト (CoW)#

注記

コピーオンライトはpandas 3.0でデフォルトになります。すべての改善を活用するために、今すぐ有効にすることをお勧めします。

コピーオンライトはバージョン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も更新されます。正確な動作は予測が困難です。コピーオンライトは、複数のオブジェクトを誤って変更することを解決し、これを明示的に禁止します。 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で動作が変わるすべての操作に対して警告が表示されます。ユーザーに影響を与えないと予想される多くのケースでも警告が表示されるため、このモードは非常にノイズが多いと予想されます。このモードをチェックして警告を分析することをお勧めしますが、これらの警告すべてに対処する必要はありません。以下のリストの最初の2つの項目は、既存のコードをCoWで動作させるために対処する必要がある唯一のケースです。

以下の項目では、ユーザーに見える変更について説明します。

連鎖代入は機能しません

代わりに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配列に関するセクションを参照してください。

一度に1つのpandasオブジェクトのみが更新されます

次のコードスニペットは、CoWなしでdfsubsetの両方を更新します

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配列をコピーするようになりました。これは、pandasの外部でNumPy配列がインプレースで変更されたときに、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なしではデータをコピーします。 dfdf2の両方のオブジェクトが同じデータを共有しているため、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

CoWは、viewの変更も回避するために、dfが変更されたときにコピーをトリガーします

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
   ....: 

fooは、列barが5より大きい場合に更新されます。ただし、これはビューdf["foo"]dfを1ステップで変更する必要があるため、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が1つの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]])

避けるべきパターン#

1つのオブジェクトをインプレースで変更している間に、2つのオブジェクトが同じデータを共有している場合、防御的なコピーは実行されません。

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=1DataFrame.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