pandasのデータフレームにシリーズを追加しようとするとjoinに近い挙動となるので気を付けましょうという記事。
まえおき
pandasでデータ処理を行なっているとき、データフレームに列を追加する処理は頻出です。
デーフレームに列を追加する時には大きく分けて二つのやり方があります。
- 列名を指定して列追加
-
pd.DataFrame.assign
メソッドで追加
1. 列名を指定して列追加
df['new_col'] = data
2. assignメソッドを用いて列追加
df.assign(new_col=data)
いずれの場合においても、値、サイズの等しいリスト・np.array
・pd.Series
などを渡すことができます。
シリーズを代入したときに起こる直感に反した挙動
任意のデータフレームと、同じレコード数のシリーズを用意します。
df = pd.DataFrame(
[[1,2,3], [4,5,6], [7,8,9]],
columns=['a', 'b', 'c'],
index=[1,2,3]
)
sr = pd.Series([-1, -2, -3])
df
# a b c
# 1 1 2 3
# 2 4 5 6
# 3 7 8 9
sr
# 0 -1
# 1 -2
# 2 -3
# dtype: int64
df
に対して、新しい列'd'
としてsr
のデータを追加したいとき、以下のようにすると思います。
df = df.assign(d=sr)
このようなテーブルが作られることを期待します。
a | b | c | d | |
---|---|---|---|---|
1 | 1 | 2 | 3 | -1 |
2 | 4 | 5 | 6 | -2 |
3 | 7 | 8 | 9 | -3 |
ところが実際はこのようなデータフレームが返ってきます。
a | b | c | d | |
---|---|---|---|---|
1 | 1 | 2 | 3 | -2 |
2 | 4 | 5 | 6 | -3 |
3 | 7 | 8 | 9 | NaN |
何が起こっているのか
改めてデータフレームとシリーズを見比べてみると、両者のindexが一致していません。
このようなデータでは、代入の場合においても結合のような挙動になっていることがわかりますね。
回避策
np.arrayとして渡すことで回避が可能です。
df.assign(new_col=new_series.values)
補足:公式ドキュメントを見る限り、values
メソッドよりto_numpy
メソッドを使用する方が推奨されるとのことです。
pandasの0.24にて追加されたExtensionArary
をはっきりと区別するためにこのようになっているみたいですね。
なぜこうなるのか
まず、pd.DataFrame.assign
メソッドは、渡した値がcallableでない場合は、冒頭に示した1の処理が内部で呼び出されているだけです。したがって、今回のような現象はいずれの方法においても起こります。
(ちなみに「渡した値がcallableである」ときというのは、lambda
式などでデータフレーム自身の列を呼び出す場合などが該当します。)1
df['X'] = hogehoge
のように代入しようとすると、pd.DataFrame.__setitem__()
が呼び出されます。コードをたどっていくと、以下のようなdocstringを発見しました。2
"""
Add series to DataFrame in specified column.
If series is a numpy-array (not a Series/TimeSeries), it must be the
same length as the DataFrames index or an error will be thrown.
Series/TimeSeries will be conformed to the DataFrames index to
ensure homogeneity.
"""
つまり、渡したデータが、
- データフレームとサイズの等しいnumpy-arrayである
- そのままの並びで追加される
- Seriesである
- そのindexがデータフレームのものと一貫性を保つように追加される
というように処理されると述べられています。
さらにコードを追ってみると、データが追加される前にデータフレームのindexに沿うように並び替えられていることがわかります。3
今回の現象は、シリーズを配列と同じように考えてしまっていたのが原因でした。
下記のように、そもそもシリーズのサイズは加えるデータフレームのレコードと一致させる必要すらなく、リストや配列と全く別物であることもわかりました。
df.assign(
x=pd.Series([3], index=[2])
)
# a b c x
# 1 1 2 3 NaN
# 2 4 5 6 3.0
# 3 7 8 9 NaN
まとめ
同じサイズの1次元データだからといって、リストや配列と同じようにデータフレームに追加しないようにしましょう。
シリーズをデータフレームの列に代入する場合は、サイズが異なる場合でもエラーにならずに処理が進んでしまうため、気づかぬうちにバグを生んでしまっている危険性が高まりそうです。必ずnumpy-arrayに変換して代入処理を行おうと自戒しました。
※この検証はpandas 1.0.3で行っていますが、以前のバージョンでも同様の挙動でした。