pandas の concat における注意点
pandas の concat における注意点

pandas の concat における注意点

はじめに

LGBM を本番に持って行く際にパフォーマンスのボトルネックになってしまった pandas の使い方とその改善についてのメモです

やりたい事

LGBM を api 化する際に、全ての特徴量を api の parameter として貰うのではなく、key となる id だけをもらって api 側で持っている data から LGBM の入力を作って推論を行いたいとします

例えば user id と item id をもらって、LGBM への推論を行いたい下記のような場合を考えてみます

DataFrame を連結して LGBM へ入力可能なデータを作る
DataFrame を連結して LGBM へ入力可能なデータを作る

サンプルとなるデータは以下のようなコードで作れます

item_features = pd.DataFrame({'A': ['A1', 'A2', 'A3'],
				                      'B': ['B1', 'B2', 'B3'],
				                      'C': ['C1', 'C2', 'C3']})

user_features = pd.DataFrame({'D': ['D2'],
                              'E': ['E2']})

解決方法

1. user feature の次元を item feature に合わせる

以下の図のように user feature の次元を item feature に合わせる事で目的の DataFrame を作る事が可能です

user feature の次元を先に item feature に合わせる
user feature の次元を先に item feature に合わせる

この方法は以下のようなコードで実現できます

user_features = pd.concat([user_features] * len(item_features), ignore_index=True)
df = pd.concat([item_features, user_features], axis=1)

結果の DataFrame は欲しい形になりますが、パフォーマンスが非常に悪く、item の件数に応じて使い物にならないくらいパフォーマンスが悪化してしまう可能性があります

実際に10万件のデータで動作させてみると 7 sec 程度かかりました

item_features = pd.DataFrame([ [f"i-{i}-1", f"i-{i}-2", f"i-{i}-3"] for i in range(100000)])
user_features = pd.DataFrame([['u-1', 'u-2']])

user_features = pd.concat([user_features] * len(item_features), ignore_index=True)
df = pd.concat([item_features, user_features], axis=1)

2. dummy となる key を付与して pandas の merge を使う

図にすると以下のようになります

dummy key を付与して merge する方法
dummy key を付与して merge する方法

コードでは以下のようになります

item_features.assign(key=1).merge(user_features.assign(key=1), on='key').drop('key',axis=1)

一見すると 1 よりも面倒な事をやっているように見えますが、パフォーマンスはこちらの方が圧倒的に良く、特に item feature の数が増えてくると顕著になります

実際に動作確認すると、1 と同じ条件の処理が 0.06 sec 程度で終わりました

item_features = pd.DataFrame([ [f"i-{i}-1", f"i-{i}-2", f"i-{i}-3"] for i in range(100000)])
user_features = pd.DataFrame([['u-1', 'u-2']])

item_features.assign(key=1).merge(user_features.assign(key=1), on='key').drop('key',axis=1)

3. iloc を使う

この記事を書いた後で更に早い方法を弊社エンジニアに教えてもらったので追記です

概念としては 1 の方法をpandas の iloc を使うとさらに高速になりました

user_features = user_features.iloc[np.repeat(0, len(item_features))].reset_index(drop=True)
df = pd.concat([item_features, user_features], axis=1)
概念 1 を iloc を使って実装する

動作確認では、1 と同じ条件で処理が 0.01 sec 程度になりました

item_features = pd.DataFrame([ [f"i-{i}-1", f"i-{i}-2", f"i-{i}-3"] for i in range(100000)])
user_features = pd.DataFrame([['u-1', 'u-2']])

user_features = user_features.iloc[np.repeat(0, len(item_features))].reset_index(drop=True)
df = pd.concat([item_features, user_features], axis=1)

まとめ

pandas で LGBT の入力を作る場合などで覚えておきたいテクニックを紹介しました

結果は 3 → 2 → 1 の手法の順にパフォーマンスが良く、特に 1 は非常に遅いためプロダクションでの利用などは注意が必要です

item feature が 10 万件の際の処理時間

Name結果
1. user feature の次元を item feature に合わせる
~ 7 sec
2. dummy となる key を付与して pandas の merge を使う
~ 0.06 sec
3. iloc を使う
~ 0.01 sec