DataFrameでreplaceされない要素を平均値で埋めたいけど冗長になってしまったあなたへ


はじめに

データフレームはデータ分析をPythonでやるときには必須になります。
しかし、少し複雑な処理になると冗長なコードになりがちでもっとうまい処理の仕方はないかと思います。
私の勤める企業ではPythonを利用している人がかなり少ないため、そのような処理を書くときにリファクタリングについて聞く当てがありません。(残念ながらエンジニアの知り合いも少ないです)

そんな状況でデータ分析をしているときに、「この処理うまく書けないか?」と思いました。

最近はITに関する質問をすることができるteratailStackOverFlowなどがあり、そこでは達人が多くの質問に早く回答してくれます。

私は処理を効率よくする(調べるだけではみつけずらい)方法を学ぶために上記のサービスを利用して学ぶようにしています。(terateilのほうが回答が返ってきやすいです)

そして今回もスマートにコードが書けないかとteratailを利用しましたので、まとめたいと思います。

問題

以下のようなデータフレームがあるとします。

index 都道府県 市区町村
1 東京 渋谷
2 東京 新宿
3 東京 池袋
4 埼玉 大宮
5 千葉 新浦安
6 神奈川 横浜

そして、市区町村を辞書(dic)を利用してreplaceします。

# データフレームの準備
pref = ["東京", "東京", "東京", "埼玉", "千葉", "神奈川"]
value = ["渋谷", "新宿", "池袋", "大宮", "新浦安", "横浜"]
df = pd.DataFrame({'都道府県':pref, '市区町村': value})

# 辞書
dic = {"渋谷":100, "新宿":90, "大宮":50, "新浦安":45, "横浜":80}

# 辞書で市区町村列を置き換え
df["市区町村"] = df["市区町村"].replace(dic)

すると、市区町村の池袋がないために、置き換わらず以下のようなデータフレームになります。

index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 池袋
3 埼玉 50
4 千葉 45
5 神奈川 80

ここで、replaceできなかった池袋を「東京」の平均値で埋められる処理を考えます。

(100 + 90) / 2 = 95

池袋を95で埋めたいのですが、コードが冗長であったので質問をしました。

解決方法1

mapを利用することでうまく処理することができます。
replaceだと辞書にないものはそのままになりますが、mapを利用するとNaNとなるためfloat型で扱いやすくなります。

またmapのほうが高速に動くそうです。

1.mapを利用して辞書で置換をする

import pandas as pd
# データフレームの準備
pref = ["東京", "東京", "東京", "埼玉", "千葉", "神奈川"]
value = ["渋谷", "新宿", "池袋", "大宮", "新浦安", "横浜"]
df = pd.DataFrame({'都道府県':pref, '市区町村': value})

# 辞書
dic = {"渋谷":100, "新宿":90, "大宮":50, "新浦安":45, "横浜":80}

# 置換
df["市区町村"] = df["市区町村"].map(dic)

するとdfは以下のようになります。

index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 NaN
3 埼玉 50
4 千葉 45
5 神奈川 80

処理速度は以下のようになり、mapの方が高速であることがわかります。

%timeit df["市区町村"].replace(dic)
# 1000 loops, best of 5: 865 µs per loop

%timeit df["市区町村"].map(dic)
# 1000 loops, best of 5: 478 µs per loop

2. 平均値で補完する

df['市区町村'] = df['市区町村'].groupby(df['都道府県']).transform(lambda s: s.fillna(s.mean()))

県ごとにまとめて、処理をしています。
applytransformerは同じような使い方になりますが、少し違いがありますので以下を参照してください。

Pandas の transform と apply の基本的な違い

そのあとfillna()を利用してNaNに該当するところを県の平均で補完します。

index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 95
3 埼玉 50
4 千葉 45
5 神奈川 80

また1行でこの処理を書くことも可能です。

df['市区町村'] = df['市区町村'].map(dic).groupby(df['都道府県']).transform(lambda s: s.fillna(s.mean()))

解決方法2

手順を追ってやることも可能です。

1. 辞書を使って置換する

問題で行っていた手順をまず行います。

# データフレームの準備
pref = ["東京", "東京", "東京", "埼玉", "千葉", "神奈川"]
value = ["渋谷", "新宿", "池袋", "大宮", "新浦安", "横浜"]
df = pd.DataFrame({'都道府県':pref, '市区町村': value})

# 辞書
dic = {"渋谷":100, "新宿":90, "大宮":50, "新浦安":45, "横浜":80}

# 辞書で市区町村列を置き換え
df["市区町村"] = df["市区町村"].replace(dic)
index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 池袋
3 埼玉 50
4 千葉 45
5 神奈川 80

2.市区町村ごとに平均値をもとめる

# 市区町村の要素を整数に変換する
tempdf["市区町村"] = tempdf["市区町村"].astype(int)

# 市区町村ごとに平均値をもとめる
mean_df = tempdf.groupby('都道府県').mean()
index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 95
3 埼玉 50
4 千葉 45
5 神奈川 80

3. 平均値のデータフレームを辞書にする

mean_dict = mean_df.to_dict()['市区町村']

4.辞書でデータフレームを置換する

df['市区町村']  = df.apply(lambda row: row['市区町村'] if type(row['市区町村'])==int else mean_dict[row['都道府県']], axis=1)
index 都道府県 市区町村
0 東京 100
1 東京 90
2 東京 95
3 埼玉 50
4 千葉 45
5 神奈川 80

おわりに

replaceを2回利用して希望通りの結果になりました。
私は置換を2回行うところまでは同じでしたが、applyを利用したうまい処理ができず、困っていました。
詳しい人の技術を盗むことも成長するには必要ですね。

参考文献

teratail
StackOverFlow
DataFrameのreplaceで置換されなかった要素の処理について
Pandas の transform と apply の基本的な違い