流沙河鎮

情報技術系のこと書きます。

Pandas2系でpandas.DataFrame.append()が削除された対処と背景

2023年4月、pandas 2.0がリリースされた。
What’s new in 2.0.0 (April 3, 2023) — pandas 2.1.0.dev0+766.g935244a9b9 documentation
さっそく手元のツール群のバージョンを上げた所、従来append()を使っていた処理が動かなくなりハマった。

pandas.DataFrame.append()が削除

Pandas 1系では、Dataframeに新たな行を追加する関数としてpandas.DataFrame.append()が用意されていた。

import pandas as pd

df = pd.DataFrame(
    {
        "名前": ["Alice", "Bob", "Charlie", "Dave"],
        "年齢": [25, 30, 35, 40],
        "都市": ["東京", "ニューヨーク", "ロンドン", "パリ"],
    }
)

new_row = {"名前": "Eve", "年齢": 45, "都市": "ベルリン"}

# pandas 2系では動かない
df = df.append(new_row, ignore_index=True)
print(df)

しかし、Pandas2系ではpandas.DataFrame.append()が削除された。1系の時点ですでにDeprecateであったのが、2系で削除された格好だ。

対応

代わりに、pandas.concatの使用が推奨されている。pd.concat([df1, df2])のように、連結したい複数のDataFrameを渡す形になる。

import pandas as pd

df = pd.DataFrame(
    {
        "名前": ["Alice", "Bob", "Charlie", "Dave"],
        "年齢": [25, 30, 35, 40],
        "都市": ["東京", "ニューヨーク", "ロンドン", "パリ"],
    }
)

new_row = {"名前": "Eve", "年齢": 45, "都市": "ベルリン"}

df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
print(df)

変更の背景

上記を知っておけば取り敢えず動くが、pandasを正しく使用するためには変更の背景を理解して実装する必要がある。 df.append()が削除された背景には、ユーザが効率の良いコードを書けるよう、当該メソッドをPython標準ライブラリのlist.append()のように使ってほしくない開発陣の想いがこめられている。
df.append()のポピュラーなユースケースとして、ループ処理の中でdf.append()を繰り返し呼び出して行を追加していくような実装が為されがちである。しかし、この実装は性能面で問題があり、df.append()を呼び出す度に新しいDataFrameを作成してしまう(O(n)になる)   

効率の悪いコードの例

import pandas as pd

df = pd.DataFrame(columns=['名前', '年齢', '都市'])

for i in range(1000):
    name = f'Person {i+1}'
    age = 25
    city = 'Tokyo'
    
    new_row = {'名前': name, '年齢': age, '都市': city}
    # appendする度に新しいDataFrameが作成される
    df = df.append(new_row, ignore_index=True)

print(df)

人々がこういった実装をしてしまう背景には、標準関数のlist.append()とdf.append()が本質的に異なるにも関わらず、見かけ上とても似ている点が起因しているのではないかと開発陣は考えた。そこでdf.append()を削除して、pd.concat()を推奨する方向へ舵を切ったということらしい。
従って、df.append()が使えなくなったからと言って、何も考えずループの中でpd.concat()を呼び出してしまっては全然意味がない。

こういう実装をしてはダメ

import pandas as pd

df = pd.DataFrame(columns=['名前', '年齢', '都市'])

for i in range(1000):
    name = f'Person {i+1}'
    age = 25
    city = 'Tokyo'
    
    new_row = {'名前': name, '年齢': age, '都市': city}
    df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)

print(df)

代わりに、ループ内で行のデータをリストに追加し、ループの後でリストを使用してDataFrameを作成した上で、DataFrameに行を追加するのが効率が良い。

import pandas as pd

df = pd.DataFrame(
    {
        "名前": ["Alice", "Bob", "Charlie", "Dave"],
        "年齢": [25, 30, 35, 40],
        "都市": ["東京", "ニューヨーク", "ロンドン", "パリ"],
    }
)

data = []
for i in range(1000):
    name = f'Person {i+1}'
    age = 25
    city = 'Tokyo'
    
    new_row = {'名前': name, '年齢': age, '都市': city}
    data.append(new_row)

df = pd.concat([df, pd.DataFrame(data)])

print(df)

"悪い例"とされている実装パターンは多数のpandas紹介ページで見かけるし、自分もそのようなコードを書いたことがある。ライブラリの裏側の挙動を理解する重要性を噛み締めると共に、df.append()関数を削除することでユーザを導こうとする開発陣の工夫がとても面白いと思った。
また、Pandas2系では他にも様々な変更が入っているので、アップグレードの際は慎重なテストが必要そうだ。変更点の詳細は以下を参照して欲しい。
pandas.pydata.org

なお、本記事の内容は以下の議論を元に学び、書いた。 stackoverflow.com github.com