目次
- イントロダクション
inplace
オプションの動作原理inplace=True
の使用に関する一般的な誤解- パフォーマンスに関する検証
inplace=True
がもたらす問題点- ケーススタディ:
inplace
の影響の実例 - ベストプラクティスについて考える
- まとめ
- 余談
1. イントロダクション
Python
の世界では、コードの品質と効率性を高めるためのツールが絶えず進化しています。その中で、私が最近出会ったのが「ruff
」、Python用の新しいlinterツールです。このツールは、コードの潜在的な問題点を瞬時に指摘し、開発者の日々の作業を劇的に改善することができます。
特に興味深かったのは、ruff
が提供するPandas
関連の指摘です。Pandas
はデータ分析で広く使われるライブラリですが、その使用には潜在的な落とし穴もあります。ruff
を使っているとき、ある特定の機能―inplace
オプションに関する注意点に出くわしました(pandas-use-of-inplace-argument (PD002) - Ruff)。これは、データフレームを操作する際によく使われるオプションですが、実は多くの問題を秘めています。
この発見は、私にとってまさに目から鱗でした。そこで、この記事ではPandas
のinplace
オプションにスポットライトを当て、その使用を避けるべき理由と、より良い代替手段について掘り下げていきたいと思います。inplaceオプションの落とし穴を理解し、よりクリーンで効率的なPandasコードを書くための洞察を提供します。
2. inplace
オプションの動作原理
Pandas
のinplace
オプションは様々な関数で用意されています。
今回はclass DataFrame
で定義されているdrop
関数について注目します。
- 他の関数としては、以下のようなものがあります:
どの関数においても、デフォルトではinplace=False
であることがわかります。
2.1. drop
関数の動作について
drop
関数は、特定のインデックスを削除する関数です。
公式ドキュメント: pandas.DataFrame.drop — pandas 2.1.3 documentation
2.1.1. inplace=False
inplace=False
とした場合、drop
関数を実行した後で元々の変数df
については変更されていません。
>>> import pandas as pd
>>> df = pd.DataFrame({'A': [1, 2], 'B': [4, 5]})
>>> print(df)
A B
0 1 4
1 2 5
>>> df_dropped = df.drop('A', axis=1, inplace=False)
>>> print(df_dropped)
B
0 4
1 5
>>> print(df)
A B
0 1 4
1 2 5
2.2. inplace=True
inplace=True
とした場合、drop
関数を実行した後で元々の変数df
については変更されます。
>>> df = pd.DataFrame({'A': [1, 2], 'B': [4, 5]})
>>> df_dropped = df.drop('A', axis=1, inplace=True)
>>> print(df_dropped)
None
>>> print(df)
B
0 4
1 5
2.2. drop
の内部実装について
pd.DataFrame
の中身については、つぎのように辿ることができます。
-
Python
のパッケージにおいて__init__.py
の中身は重要であるためその中身を確認します(6. モジュール — Python 3.12.0 ドキュメント)。 -
pandas/pandas/__init__.py · pandas-dev/pandas をみると
DataFrame
はpandas.core.api
から import されています。 -
pandas/pandas/core/api.py · pandas-dev/pandas をみると
DataFrame
はpandas.core.frame
から import されています。 -
pandas/pandas/core/frame.py · pandas-dev/pandas をみると
DataFrame
の実態が定義されています。
一方でdrop
関数の実態は親クラスで定義されていることがわかります。
-
pandas/pandas/core/frame.py · pandas-dev/pandas をみると
drop
関数は overload が沢山用意され、かつ 親クラスで定義されています。 -
pandas/pandas/core/frame.py · pandas-dev/pandasをみると
DataFrame
クラスは 多重継承されたクラスです。 - Method Resolution Order(用語集 — Python 3.12.0 ドキュメント)をみると 次の順番で 親クラスの method を見ています。
-
>>> pd.DataFrame.__mro__ (<class 'pandas.core.frame.DataFrame'>, <class 'pandas.core.generic.NDFrame'>, <class 'pandas.core.base.PandasObject'>, <class 'pandas.core.accessor.DirNamesMixin'>, <class 'pandas.core.indexing.IndexingMixin'>, <class 'pandas.core.arraylike.OpsMixin'>, <class 'object'>)
-
-
pandas/pandas/core/generic.py · pandas-dev/pandas をみると
NDFrame
にdrop
関数が定義されています。
ここまでたどり着くと、inplace
の箇所の実装を確認することができます。
- inplaceの有無の分岐箇所: https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/generic.py#L4751-L4755
3. inplace=True の使用に関する一般的な誤解
inplace=True
とすることに対して、次の誤解が存在するかと思います。
- メモリ使用量の削減という誤った信念
- パフォーマンスへの誤解
これらは、inplace=True
を行うことで、drop
後の変数を宣言する必要がないために発生するかと思います。
しかし、コードを見ると inplace
が True
である場合は 関数が追加で実装される上に、inplace
がTrue
でもFalse
でも 内部的に変数が設定されるため、メモリ使用量はあまり変わらずパフォーマンスについては低下するように思えます。
4. パフォーマンスに関する検証
inplace=True
を用いた際に、本当にパフォーマンスは良くならないか検証してみました。
検証の内容としては簡単なデータフレームを作成しdrop
動作をさせ、その際のメモリ使用量と実行時間について確認しました。
4.1. 使用コードについて
Python環境と使用ライブラリ,環境変数について
cpython@3.10.12
pandas==2.0.3
delogger==0.3.0
"""inplaceについて検証するためのコード."""
import gc
import pandas as pd
from delogger.presets.profiler import (
logger,
)
NUM = 10
4.1.1. inplace=True
の場合
@logger.line_memory_profile
def use_inplace() -> None:
"""use_inplace."""
for _ in range(NUM):
df: pd.DataFrame = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df.drop("A", axis=1, inplace=True)
del df
gc.collect()
if __name__ == "__main__":
use_inplace()
ログの内容
DEBUG line_memory_profile.py:83 wrapper line, memory profiler result
Timer unit: 1e-09 s
Total time: 0.357716 s
File: ./include_inplace.py
Function: use_inplace at line 12
Line # Hits Time Per Hit % Time Mem usage Increment Line Contents
=========================================================================================
12 70.94 MiB 70.94 MiB 1 @logger.line_memory_profile
13 def use_inplace() -> None:
14 """use_inplace."""
15 11 44000.0 4000.0 0.0 71.34 MiB 0.00 MiB 11 for _ in range(NUM):
16 10 8568000.0 856800.0 2.4 71.34 MiB 0.13 MiB 10 df: pd.DataFrame = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
17 10 14424000.0 1e+06 4.0 71.34 MiB 0.27 MiB 10 df.drop("A", axis=1, inplace=True)
18 10 67000.0 6700.0 0.0 71.34 MiB 0.00 MiB 10 del df
19 10 334613000.0 3e+07 93.5 71.34 MiB 0.00 MiB 10 gc.collect()
4.1.2. inplace=False
の場合
@logger.line_memory_profile
def not_use_inplace() -> None:
"""not_use_inplace."""
for _ in range(NUM):
df: pd.DataFrame = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df_dropped: pd.DataFrame = df.drop("A", axis=1)
del df, df_dropped
gc.collect()
if __name__ == "__main__":
not_use_inplace()
ログの内容
DEBUG line_memory_profile.py:83 wrapper line, memory profiler result
Timer unit: 1e-09 s
Total time: 0.349473 s
File: ./exclude_inplace.py
Function: not_use_inplace at line 12
Line # Hits Time Per Hit % Time Mem usage Increment Line Contents
=========================================================================================
12 70.96 MiB 70.96 MiB 1 @logger.line_memory_profile
13 def not_use_inplace() -> None:
14 """not_use_inplace."""
15 11 41000.0 3727.3 0.0 71.37 MiB 0.00 MiB 11 for _ in range(NUM):
16 10 7865000.0 786500.0 2.3 71.37 MiB 0.13 MiB 10 df: pd.DataFrame = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
17 10 13549000.0 1e+06 3.9 71.37 MiB 0.27 MiB 10 df_dropped: pd.DataFrame = df.drop("A", axis=1)
18 10 196000.0 19600.0 0.1 71.37 MiB 0.00 MiB 10 del df, df_dropped
19 10 327822000.0 3e+07 93.8 71.37 MiB 0.00 MiB 10 gc.collect()
4.2. ログ結果の比較
項目 |
use_inplace() (inplace=True ) |
not_use_inplace() (inplace=False ) |
---|---|---|
drop の実行 |
0.27 MiB | 0.27 MiB |
drop の実行時間 |
1.4424000 x 1e-2 秒 | 1.3549000 x 1e-2 秒 |
4.3. ログ結果の解釈
- 両関数の
drop
の実行によるメモリ使用量の増加がほぼ同じです(0.27 MiB)。これは、inplace=True
を使用しても、内部で新しいデータフレームが一時的に作成されているためと考えられます。 - 実行時間もほぼ同じで、
inplace=True
を使用した場合が若干長いですが、この差はごくわずかです。
4.4. 検証の結論
今回の検証では、inplace=True
とinplace=False
の間でメモリ使用量に大きな違いは見られませんでした。また、実行時間もほぼ同じでした。これは、inplace
パラメーターの使用が必ずしもパフォーマンスやメモリ効率に大きな影響を与えるわけではないことを示しています。
5. inplace=True
がもたらす問題点
さて、先程の項でinplace
はパフォーマンスの影響にあまり関与しないことがわかりました。それでは、inplace=True
を使用することに対して、一体何が問題なのでしょうか。
問題を考える前に利点について考え、その後に欠点について考えてみます。
5.1. inplace=True
の利点について考える
5.1.1. 中間生成物の命名の不要について
データ分析のプロセスはしばしば、複雑で多段階のデータ変換を伴います。このような環境で、Pandas
のinplace=True
オプションは、分析の効率を高める重要なツールとなる可能性があります。このオプションの最大の利点は、中間生成物を作成せずにデータを直接変更することができる点にあります。
inplace=True
を使用すると、このプロセスが大幅に簡略化されます。新しい変数を作成し、それをトラッキングする必要がなくなるため、コードの可読性が向上するように思えます。
しかしながら、実際の分析においては本当に可読性・運用性を向上させているか注意深く考える必要があります。
5.2. inplace=True
の欠点について考える
5.2.1. データの不意な変更とその影響
inplace=True
を使用すると、データフレームの変更が不可逆的になります。これは、データ分析の過程で特に問題となる場合があります。例えば、データのクリーニングや前処理を行っている際に、誤って重要なデータを削除してしまう可能性があります。一旦削除してしまうと、その操作を元に戻すことはできません。このように、inplace=True
の使用は、データの誤操作によるリスクを増大させます。
5.2.2. デバッグと保守の困難さ
inplace=True
による直接的な変更は、コードのデバッグと保守を困難にします。特に、複雑なデータ分析プロセスでは、一つのデータフレームに対して多数の操作が行われることがあります。inplace=True
を使用していると、どの操作がデータにどのような影響を与えたかを追跡することが難しくなります。また、将来のデータセットに同じ処理を適用する際に、予期しない結果やエラーが発生する可能性があり、コードの再利用性が低下します。
6. ケーススタディ: inplace=True
の影響の実例
前述した内容を踏まえて次のようなケースを考えてみました。こちらを考えるとより、inplace=True
を使うべきかinplace=False
を使うべきかの助けになるかと存じます。
6.1. データの分析における効率の悪化
6.1.1. シナリオ
槇くんは、Kaggleのコンペティションや業務で大規模なデータセットの前処理をJupyter Notebook
で行っています。データクリーニングの一環として、欠損値がある行や特定の条件に該当する行を削除する必要が生じました。槇くんはpd.DataFrame.dropna()
やDataFrame.drop()
メソッドを使用し、変数の命名が面倒という観点からinplace=True
オプションでこれらの行をデータフレームから直接削除しました。
6.1.2. 問題の発生
しかし、モデルを構築する段階で、削除された行が実は重要な情報を含んでいたことがわかりました。さらに、inplace=True
を使用しているため、これらの行は元に戻すことができませんでした。
6.1.3. 影響
データを復元するためには、データソースからデータを再度読み込む必要がありますが、これにはかなりの時間がかかります。結果として、槇くんは作業を一時中断し、分析プロジェクトのスケジュールに遅延が生じました。
7. ベストプラクティスについて考える
今までの話から、Pandas
の前処理に関してベストプラクティスはinplace=False
を使用することであることが伺えました。この章では、データの保全、デバッグのしやすさ、コードの柔軟性、そして可読性に注目して改めて詳細をまとめます。
7.1. データの保全
-
inplace=False
を使用すると、元のデータフレームは変更されず、新しいデータフレームが作成されます。これにより、元のデータを保持し続けることができ、データの誤操作によるリスクを低減できます。
df_original = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df_dropped = df_original.drop("A", axis=1) # 元のデータフレームは変更されない
7.2. デバッグのしやすさ
- 新しいデータフレームを生成することで、各変更ステップを個別に確認しやすくなり、デバッグが容易になります。
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df_renamed = df.rename(columns={"A": "a"}) # 変更点を確認しやすい
df_dropped = df_renamed.drop("B", axis=1)
7.3. コードの柔軟性
- 複数のデータフレームを操作することで、異なるデータ処理のパスを容易に試すことができます。これにより、データ分析の柔軟性が高まります。
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df_with_a = df.drop("B", axis=1)
df_with_b = df.drop("A", axis=1) # 異なる処理を並行して実行できる
7.4. 可読性の向上
- 全ての前処理に対して、変数を持たせておくのは確かに面倒ですが、メソッドチェーンを使用するとこの問題は解消され、分析の中で試行錯誤しながらも、コードの可読性が向上させることができます。
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# inplace=Falseでの作業(開始)
# ...
# inplace=Falseでの作業(終了)
df_processed = (df
.rename(columns={"A": "a"})
.drop("B", axis=1))
8. まとめ
inplace=False
の使用は、データ分析のプロセスをより透明で安全かつ柔軟にし、結果としてプロジェクトの全体的な成功に貢献します。データの不可逆的な変更を避け、デバッグと保守のしやすさを保ちながら、より高い可読性を実現することができます。
9. 余談
9.1. Polarsについて
Polarsでは polars.DataFrame.drop_in_place — Polars documentation, polars.DataFrame.drop — Polars documentationのように そもそも inplace
を使用する際と使用しない場合でメソッド名が異なるようです。
9.2. ヒューマンエラーについて
仮にinplace=False
としても次のようなことをすると元のデータは失われます。
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df = df.drop("B", axis=1)
このコードでは、drop
メソッドを使用して"B"
列を削除していますが、その結果は元の変数df
に再代入されています。これにより、元の"B"
列を含むデータフレームは失われます。
そのため、Python
コードを扱うときは 変数への再代入は避ける といった基本的なルールが大切なのかもしれません。
9.3. 分析にNotebookを使用した際の共有について
分析作業の一環としてJupyter Notebook
を使用する際には、他人にコードを読んでもらうことも重要で、ruff
などのlinterツールを通せるようにできる形に出力しておくことは重要です。この点において、Jupyter Notebook
からPython
コードへの変換を効率的に行うフォーマットは非常に便利で、分析プロセスをより構造化し、コードの可読性と共有性を高めることができます。
過去に私の自前ですがPython
ライブラリを公開したためそちらについての紹介リンクを添付させていただきます。
詳しくは、こちらの記事をご覧ください:Kaggleのコードを英語で読めないから,NotebookをPythonコードに変更しよう #Python - Qiita
参考資料
- 本記事を執筆するにあたり参考にした記事
- 関連ライブラリに関する詳細
- astral-sh/ruff: An extremely fast Python linter and code formatter, written in Rust.
- pandas-dev/pandas: Flexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more
- deppen8/pandas-vet: A plugin for Flake8 that checks pandas code
- jupyter/notebook: Jupyter Interactive Notebook
- Pythonのline_profilerとmemory_profilerの紹介 #Python3 - Qiita