12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LIFULLAdvent Calendar 2021

Day 25

細かすぎて伝わらないpandasテクニック集

Last updated at Posted at 2021-12-24

LIFULLアドベントカレンダーのトリは、クリスマスよりも明日の有馬記念が大事、そんな@kazuktymがお届けします。

パンダジスタへの道

機械学習によるデータの前処理など、大量のデータ操作を簡単に実現するためにpandasをよく利用します。なるべくならPythonプログラミングに頼らず、pandasのDataFrame上でデータ処理を完結したいとパンダジスタの誰もが思っていることでしょう。(パンダジスタという言葉は一般用語ではありませんので、用法・用量を守って正しくお使いください)

今回は、利用するケースがそうそう無いかもしれない、しかし知っているときっと助かるはず、という細かすぎて伝わらないpandasの活用例を紹介します。

今回使用するDataFrame

import pandas as pd

df = pd.DataFrame({
    'レース番号': [1, 1, 1, 1, 1, 2, 2, 2, 2],
    '馬名': ['アアアウィーク', 'イイイテイオー', 'ウウウブラック', 'エエエキャップ', 'オオオシャトル', 'カカカワンダー', 'キキキグルーヴ', 'クククブルボン', 'ケケケシャワー'],
    '生年月日': [20180311, 20180301, 20180221, 20180211, 20180201, 20180131, 20180121, 20180111, 20180101],
    '走破タイム': [2000, 1580, 2005, 1585, 1590, 2010, 1595, 2015, 2015],
    '着順':  [4, 1, 5, 2, 3, 2, 1, 3, 4]
})
レース番号 馬名 生年月日 走破タイム 着順
1 アアアウィーク 20180311 2000 4
1 イイイテイオー 20180301 1580 1
1 ウウウブラック 20180221 2005 5
1 エエエキャップ 20180211 1585 2
1 オオオシャトル 20180201 1590 3
2 カカカワンダー 20180131 2010 2
2 キキキグルーヴ 20180121 1595 1
2 クククブルボン 20180111 2015 3
2 ケケケシャワー 20180101 2015 4

競馬の予想モデルを作るイメージで、上記のDataFrameを使用していきます。

グループ化したデータをゴニョゴニョしたい時

pandasではgroupbyを活用する場面が何かと多いと思います。今回の例で言うと、レースごとにグループ化してデータを加工する際に重宝します。groupbyしたデータをもっと柔軟に操作できたらいいのにという場面によく出くわすので、そんな時に役立つ事例をまとめてみました。

グループごとにソート順で先頭n件抽出する

早くもちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。

df = df.sort_values('走破タイム', ascending=True)
df.groupby('レース番号').head(3)

各レースごとに走破タイムの早い順に3頭抜き出したいケースがあったときに、上記のようにDataFrame全体をまずは走破タイムの昇順でソートして、groupbyの結果をheadして先頭n件取り出します。

レース番号 馬名 生年月日 走破タイム 着順
1 イイイテイオー 20180301 1580 1
1 エエエキャップ 20180211 1585 2
1 オオオシャトル 20180201 1590 3
2 キキキグルーヴ 20180121 1595 1
2 カカカワンダー 20180131 2010 2
2 クククブルボン 20180111 2015 3

ただしこの方法には一つ問題があります。指定したn件のデータがインデックス順にきっちり抽出されてしまうため、同じ走破タイムの馬が複数いた場合、拾えないレコードが存在します。(今回の例で言うとケケケシャワーが該当します)

上記の方法は、ソート対象の値が重複しないと分かっているケースで使用するとよいでしょう。重複するレコードも抽出したい場合は、以下の方法で実現することが可能です。

indexes = df.groupby('レース番号')['走破タイム'].nsmallest(3, keep='all').index.values
df[df.index.isin(map(lambda x: x[1], indexes))]
レース番号 馬名 生年月日 走破タイム 着順
1 イイイテイオー 20180301 1580 1
1 エエエキャップ 20180211 1585 2
1 オオオシャトル 20180201 1590 3
2 キキキグルーヴ 20180121 1595 1
2 カカカワンダー 20180131 2010 2
2 クククブルボン 20180111 2015 3
2 ケケケシャワー 20180101 2015 4

nsmallest関数のkeep='all'オプションを使うことで、重複したレコードを全て抽出できます。ただし、戻り値が走破タイムのSeriesになるので、インデックスを取り出して、後続処理で元のDataFrameと突合します。中々複雑ですね。

2021/12/27加筆

rank関数というものがあり、同様の操作ができると指摘頂きました。(知らなかった...)

df[df.groupby('レース番号')['走破タイム'].rank() < 4]

めちゃくちゃ簡単ですね。

グループごとに条件にマッチする割合を算出する

続いてもちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。

df.groupby(['レース番号'])['走破タイム'].apply(lambda x: (x < 2000).sum() / x.count()).to_frame()
レース番号 走破タイム(割合)
1 0.60
2 0.25

こちらは、レースごとに走破タイムが2000(競馬では2分00.0秒を表します)を切る馬の割合を算出しています。apply関数を使用して、groupbyした結果に独自の関数を適用しています。

2021/12/27加筆

apply関数は処理が重いため、条件による抽出を先に実施してから、groupbyする方が処理効率が優れているようです。

(df['走破タイム'] < 2000).groupby(df['レース番号']).mean().to_frame()

グループにデータがn件以上存在しない場合は、DataFrameから除外する

こちらもなかなかマニアックな処理ですね。

df.groupby(['レース番号']).filter(lambda d: len(d) >= 5)
レース番号 馬名 生年月日 走破タイム 着順
1 アアアウィーク 20180311 2000 4
1 イイイテイオー 20180301 1580 1
1 ウウウブラック 20180221 2005 5
1 エエエキャップ 20180211 1585 2
1 オオオシャトル 20180201 1590 3

filter関数を使って、指定レコード数を満たすグループのみ抽出しています。

グループごとに特定カラムを区切り文字で結合する

例として、グループ内の馬名を連結します。

df.groupby('レース番号')['馬名'].apply('/'.join).to_frame()
レース番号 馬名(連結文字列)
1 アアアウィーク/イイイテイオー/ウウウブラック/エエエキャップ/オオオシャトル
2 カカカワンダー/キキキグルーヴ/クククブルボン/ケケケシャワー

CSVファイルからDataFrameを生成する際に、任意の関数で値を変換する

CSVファイルを読み込んで、DataFrameを生成することは往々にしてあります。その読み込み処理と同時に任意の関数を実行して値を変換することができます。

data.csv
レース番号,馬名,生年月日,走破タイム,着順
1,アアアウィーク,20180311,2000,4
1,イイイテイオー,20180301,1580,1
1,ウウウブラック,20180221,2005,5
1,エエエキャップ,20180211,1585,2
1,オオオシャトル,20180201,1590,3
2,カカカワンダー,20180131,2010,2
2,キキキグルーヴ,20180121,1595,1
2,クククブルボン,20180111,2015,3
2,ケケケシャワー,20180101,2015,4
#  競馬表記の走破タイムを実際の秒に変換
def cnv_second(val):
    min_to_sec = int(val[:1]) * 60
    decimal_sec = int(val[1:]) / 10
    return min_to_sec + decimal_sec

df = pd.read_csv('data.csv', converters={'生年月日':lambda x : str(x), '走破タイム': cnv_second})
レース番号 馬名 生年月日 走破タイム 着順
1 アアアウィーク 20180311 120.0 4
1 イイイテイオー 20180301 118.0 1
1 ウウウブラック 20180221 120.5 5
1 エエエキャップ 20180211 118.5 2
1 オオオシャトル 20180201 119.0 3
2 カカカワンダー 20180131 121.0 2
2 キキキグルーヴ 20180121 119.5 1
2 クククブルボン 20180111 121.5 3
2 ケケケシャワー 20180101 121.5 4

read_csv関数のconvertersオプションを使用します。サンプルでは、生年月日を文字列型に変換し、走破タイムを実際の秒数に変換しています。lambdaを使った無名関数でも、実際に定義した関数のどちらでも使用可能です。

特定の条件にマッチするレコードにフラグ値やカテゴリ変数を追加する

DataFrameで複雑な条件を扱いたい場合、先に対応するフラグ値やカテゴリ変数のカラムを作っておいた方が処理が楽になる場面もあります。

フラグ値を追加する例

走破タイムが2分未満かつ着順が3着以内のレコードにTrueを追加します。

df['要チェックフラグ'] = (df['走破タイム'] < 2000) & (df['着順'] <= 3)
レース番号 馬名 生年月日 走破タイム 着順 要チェックフラグ
1 アアアウィーク 20180311 2000 4 False
1 イイイテイオー 20180301 1580 1 True
1 ウウウブラック 20180221 2005 5 False
1 エエエキャップ 20180211 1585 2 True
1 オオオシャトル 20180201 1590 3 True
2 カカカワンダー 20180131 2010 2 False
2 キキキグルーヴ 20180121 1595 1 True
2 クククブルボン 20180111 2015 3 False
2 ケケケシャワー 20180101 2015 4 False

カテゴリ変数を追加する例

以下の条件に応じて、対応するカテゴリ値を追加します。

条件 カテゴリ値
走破タイムが2分未満かつ着順が3着以内 excellent
走破タイムが2分以上かつ着順が3着以内 good
着順が4着以下 bad
df.loc[(df['走破タイム'] < 2000) & (df['着順'] <= 3), 'レイティング'] = 'excellent'
df.loc[(df['走破タイム'] >= 2000) & (df['着順'] <= 3), 'レイティング'] = 'good'
df.loc[df['着順'] > 3, 'レイティング'] = 'bad'
レース番号 馬名 生年月日 走破タイム 着順 レイティング
1 アアアウィーク 20180311 2000 4 bad
1 イイイテイオー 20180301 1580 1 excellent
1 ウウウブラック 20180221 2005 5 bad
1 エエエキャップ 20180211 1585 2 excellent
1 オオオシャトル 20180201 1590 3 excellent
2 カカカワンダー 20180131 2010 2 good
2 キキキグルーヴ 20180121 1595 1 excellent
2 クククブルボン 20180111 2015 3 good
2 ケケケシャワー 20180101 2015 4 bad

特定カラムのソート順でDataFrameを指定の割合に分割する

最後までちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。

df = df.sort_values('生年月日', ascending=True)
ratio = round(len(df) * 0.7)
df_major = df[:ratio] # 7割
df_minor = df[ratio:] # 3割
レース番号 馬名 生年月日 走破タイム 着順
2 ケケケシャワー 20180101 2015 4
2 クククブルボン 20180111 2015 3
2 キキキグルーヴ 20180121 1595 1
2 カカカワンダー 20180131 2010 2
1 オオオシャトル 20180201 1590 3
1 エエエキャップ 20180211 1585 2
レース番号 馬名 生年月日 走破タイム 着順
1 ウウウブラック 20180221 2005 5
1 イイイテイオー 20180301 1580 1
1 アアアウィーク 20180311 2000 4

例では、生年月日順に7:3の割合でDataFrameを分割しています。機械学習では、学習データと評価データに分割して、モデル構築することが一般的ですが、時系列の概念がある場合は、時間を軸に上記の方法で分割すると簡単です。

さいごに

以上が現在の自分の引き出しにある、細かすぎて伝わらないpandasの活用例でした。私のように、データ加工処理をpandasで頑張って完結したい人の参考になれば幸いです。またノウハウが溜まったら第2弾を投稿したいと思います。

12
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?