Pandasのinplaceオプション：なぜ避けるべきなのか

## 目次
1. [イントロダクション](#1-イントロダクション)
2. [`inplace`オプションの動作原理](#2-inplaceオプションの動作原理)
3. [`inplace=True`の使用に関する一般的な誤解](#3-inplacetrue-の使用に関する一般的な誤解)
4. [パフォーマンスに関する検証](#4-パフォーマンスに関する検証)
5. [`inplace=True`がもたらす問題点](#5-inplacetrueがもたらす問題点)
6. [ケーススタディ：`inplace`の影響の実例](#6-ケーススタディ-inplacetrueの影響の実例)
7. [ベストプラクティスについて考える](#7-ベストプラクティスについて考える)
8. [まとめ](#8-まとめ)
9. [余談](#9-余談)

- [参考資料](#参考資料)

# 1. イントロダクション

`Python`の世界では、コードの品質と効率性を高めるためのツールが絶えず進化しています。その中で、私が最近出会ったのが「`ruff`」、Python用の新しいlinterツールです。このツールは、コードの潜在的な問題点を瞬時に指摘し、開発者の日々の作業を劇的に改善することができます。

特に興味深かったのは、`ruff`が提供する`Pandas`関連の指摘です。`Pandas`はデータ分析で広く使われるライブラリですが、その使用には潜在的な落とし穴もあります。`ruff`を使っているとき、ある特定の機能―`inplace`オプションに関する注意点に出くわしました([pandas-use-of-inplace-argument (PD002) - Ruff](https://docs.astral.sh/ruff/rules/pandas-use-of-inplace-argument/))。これは、データフレームを操作する際によく使われるオプションですが、実は多くの問題を秘めています。

この発見は、私にとってまさに目から鱗でした。そこで、この記事では`Pandas`の`inplace`オプションにスポットライトを当て、その使用を避けるべき理由と、より良い代替手段について掘り下げていきたいと思います。inplaceオプションの落とし穴を理解し、よりクリーンで効率的なPandasコードを書くための洞察を提供します。

# 2. `inplace`オプションの動作原理
`Pandas`の`inplace`オプションは様々な関数で用意されています。
今回は`class DataFrame`で定義されている`drop`関数について注目します。
- 他の関数としては、以下のようなものがあります:
  - [pandas.DataFrame.set_index — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.set_index.html)
  - [pandas.DataFrame.sort_values — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.sort_values.html)
  - [pandas.DataFrame.sort_index — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.sort_index.html)
  - [pandas.DataFrame.fillna — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html)
  - [pandas.DataFrame.dropna — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.dropna.html)

どの関数においても、デフォルトでは`inplace=False`であることがわかります。

## 2.1. `drop`関数の動作について
`drop`関数は、特定のインデックスを削除する関数です。
公式ドキュメント: [pandas.DataFrame.drop — pandas 2.1.3 documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.drop.html)
## 2.1.1. `inplace=False`
`inplace=False`とした場合、`drop`関数を実行した後で元々の変数`df`については変更されていません。

```python
>>> 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`については変更されます。
```python
>>> 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`の中身については、つぎのように辿ることができます。
1. `Python`のパッケージにおいて`__init__.py`の中身は重要であるためその中身を確認します([6. モジュール — Python 3.12.0 ドキュメント](https://docs.python.org/ja/3/tutorial/modules.html#packages))。
1. [pandas/pandas/\_\_init\_\_.py · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/__init__.py#L258) をみると `DataFrame` は `pandas.core.api` から import されています。
1. [pandas/pandas/core/api.py · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/api.py#L81) をみると `DataFrame` は `pandas.core.frame` から import されています。
1. [pandas/pandas/core/frame.py · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/frame.py#L499) をみると `DataFrame`の実態が定義されています。

一方で`drop`関数の実態は親クラスで定義されていることがわかります。
1. [pandas/pandas/core/frame.py  · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/frame.py#L5290-L5488) をみると `drop` 関数は overload が沢山用意され、かつ 親クラスで定義されています。
1. [pandas/pandas/core/frame.py · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/frame.py#L499)をみると `DataFrame` クラスは 多重継承されたクラスです。
1. Method Resolution Order([用語集 — Python 3.12.0 ドキュメント](https://docs.python.org/ja/3/glossary.html#term-method-resolution-order))をみると 次の順番で 親クラスの method を見ています。
   -  ```python
      >>> 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'>)
      ``` 
1. [pandas/pandas/core/generic.py · pandas-dev/pandas](https://github.com/pandas-dev/pandas/blob/87d3fe4702bf885ab8f9c01d22804352189469f2/pandas/core/generic.py#L4718-L4755) をみると `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. 使用コードについて
<details><summary>Python環境と使用ライブラリ,環境変数について</summary><div>

```python
cpython@3.10.12
```

```python
pandas==2.0.3
delogger==0.3.0
```


```python
"""inplaceについて検証するためのコード."""
import gc

import pandas as pd
from delogger.presets.profiler import (
    logger,
)

NUM = 10


```
</div></details>


### 4.1.1. `inplace=True`の場合

```python
@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()

```
<details><summary>ログの内容</summary><div>

```python
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()

```
</div></details>

### 4.1.2. `inplace=False`の場合

```python
@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()

```

<details><summary>ログの内容</summary><div>

```python
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()

```
</div></details>

## 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`を使用すると、元のデータフレームは変更されず、新しいデータフレームが作成されます。これにより、元のデータを保持し続けることができ、データの誤操作によるリスクを低減できます。

```python
df_original = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df_dropped = df_original.drop("A", axis=1)  # 元のデータフレームは変更されない
```

## 7.2. デバッグのしやすさ

- 新しいデータフレームを生成することで、各変更ステップを個別に確認しやすくなり、デバッグが容易になります。

```python
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. コードの柔軟性

- 複数のデータフレームを操作することで、異なるデータ処理のパスを容易に試すことができます。これにより、データ分析の柔軟性が高まります。

```python
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. 可読性の向上

- 全ての前処理に対して、変数を持たせておくのは確かに面倒ですが、メソッドチェーンを使用するとこの問題は解消され、分析の中で試行錯誤しながらも、コードの可読性が向上させることができます。

```python
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](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.drop_in_place.html), [polars.DataFrame.drop — Polars documentation](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.drop.html)のように そもそも `inplace` を使用する際と使用しない場合でメソッド名が異なるようです。

## 9.2. ヒューマンエラーについて

仮に`inplace=False`としても次のようなことをすると元のデータは失われます。

```python
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](https://qiita.com/making111/items/19b85240dc52f3b8dc5a)


# 参考資料
- 本記事を執筆するにあたり参考にした記事
  - [Why You Should Probably Never Use pandas inplace=True | by Sven Harris | Towards Data Science](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4)
- 関連ライブラリに関する詳細
  - [astral-sh/ruff: An extremely fast Python linter and code formatter, written in Rust.](https://github.com/astral-sh/ruff)
  - [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](https://github.com/pandas-dev/pandas)
  - [deppen8/pandas-vet: A plugin for Flake8 that checks pandas code](https://github.com/deppen8/pandas-vet)
  - [jupyter/notebook: Jupyter Interactive Notebook](https://github.com/jupyter/notebook)
  - [Pythonのline_profilerとmemory_profilerの紹介 #Python3 - Qiita](https://qiita.com/aratana_tamutomo/items/aa3b723a3dd7a44e45d6)