業務で集計したデータを「実数だけではなく割合でも出して欲しい」というオーダーを受けることはよくあります。そんな時に、PandasのDataFrameのデータを列ごとや、行ごとの割合に変換する方法のまとめです。
DataFrameの話に入る前に、まずSeries型でやってみましょう。これは非常に簡単で、元のデータをその合計で割るだけです。
import pandas as pd
# 元のデータ
sr = pd.Series([30, 0, 40, 30, 10])
# 合計で割ると割合になる
print(sr/sr.sum())
"""
0 0.272727
1 0.000000
2 0.363636
3 0.272727
4 0.090909
dtype: float64
"""
続いて、DataFrame のデータを列ごとに、その列の値の和に占める割合に変換する方法を見ていきます。実はこれも簡単でDataFrameにたいしてsum()メソッドを実行すると列ごとの和が得られ、元のDataFrameをその和で割るといい感じにブロードキャストされて望む結果が得られます。
ブロードキャストについてはこちらも参照ください。今回の例で言えば、型が(5, 3) と (3,) なのでブロードキャストされます。
参考: NumPyのブロードキャストで変換できる型
# 元のデータを生成する
df = pd.DataFrame(
{
"col1": [0, 60, 80, 60, 0],
"col2": [10, 80, None, 20, 40],
"col3": [30, 0, 40, 30, 10],
}
)
print(df)
"""
col1 col2 col3
0 0 10.0 30
1 60 80.0 0
2 80 NaN 40
3 60 20.0 30
4 0 40.0 10
"""
# sum() すると列ごとの和が得られる
print(df.sum())
"""
col1 200.0
col2 150.0
col3 110.0
dtype: float64
"""
print(df/df.sum())
"""
col1 col2 col3
0 0.0 0.066667 0.272727
1 0.3 0.533333 0.000000
2 0.4 NaN 0.363636
3 0.3 0.133333 0.272727
4 0.0 0.266667 0.090909
"""
ここからがこの記事の本題です。
列ごとに割合に変換するのは簡単でしたが、行ごとに割合に変換するのはこのようにはうまくいきません。sum(axis=1) で各行ごとの和は出せますが、それで元のデータフレームを割ろうとすると適切にブロードキャストされないからです。なんか変な結果が戻ってきます。
print(df/df.sum(axis=1))
"""
col1 col2 col3 0 1 2 3 4
0 NaN NaN NaN NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN NaN NaN NaN
2 NaN NaN NaN NaN NaN NaN NaN NaN
3 NaN NaN NaN NaN NaN NaN NaN NaN
4 NaN NaN NaN NaN NaN NaN NaN NaN
"""
対処法はいくつかあると思います。一つは、「列ごとの処理は簡単で行ごとの処理が難しいなら行列入れ替えればいい」という発想に基づくものです。単純に転置して割合に変換した後もう一回転置します。
# 転置したDataFrameを作る
df_t = df.T
print((df_t/df_t.sum()).T)
"""
col1 col2 col3
0 0.000000 0.250000 0.750000
1 0.428571 0.571429 0.000000
2 0.666667 NaN 0.333333
3 0.545455 0.181818 0.272727
4 0.000000 0.800000 0.200000
"""
もう一つ、applyメソッドをaxis=1を指定して使い行ごとのSeriesに対して、割合に変換する方法もあります。個人的にはこちらの方が若干スマートに思えます。
(ちなみに、axis=0 (デフォルト)で実行すると列ごとに割合に変換してくれます)
print(df.apply(lambda x: x/x.sum(), axis=1))
"""
col1 col2 col3
0 0.000000 0.250000 0.750000
1 0.428571 0.571429 0.000000
2 0.666667 NaN 0.333333
3 0.545455 0.181818 0.272727
4 0.000000 0.800000 0.200000
"""
さて、 lambda 式の中で、 x.sum() ってメソッドが出てきますが、ここがちょっとしたコツです。ここで呼び出されるsum()はSeriesオブジェクトのメソッドのsum()なのですが、これはNaNを無視して和をとってくれます。そのため、index 2 の行(3行目)は、元の値が[80, NaN, 40] ですが、これの和を120として、元の値を割ってくれているわけです。
ここで、x.sum() とせずに、 sum(x)と、Python組み込みメソッドのsum()を呼び出すと結果が変わります。これはNaNを無視せず、NaNが含まれる和はNaNにしてしまうのです。そのため、sum(x)を使うと次のような結果になります。
print(df.apply(lambda x:x/sum(x), axis=1))
"""
col1 col2 col3
0 0.000000 0.250000 0.750000
1 0.428571 0.571429 0.000000
2 NaN NaN NaN
3 0.545455 0.181818 0.272727
4 0.000000 0.800000 0.200000
"""
index 2 の行が全部 NaNになってしまいましたね。元のデータにNaNがなければ気にしなくて良い違いなのですが、うっかりしていると見落としがちな性質なので気をつけましょう。
当然ですが、Series型のデータに対しても、もし元のデータがNaNを含んでいたら、sum(sr)で割るのと、sr.sum()で割るのは結果が変わります。
sr = pd.Series([30, None, 40, 30, 10])
print(sr/sum(sr))
"""
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
dtype: float64
"""
print(sr/sr.sum())
"""
0 0.272727
1 NaN
2 0.363636
3 0.272727
4 0.090909
dtype: float64
"""