概要
- そこまでメジャーではない(?)
- けど、覚えておくと実装時間やコードの行数を大幅削減できる!
という便利な技をご紹介します!
「そういえばpandas
ってあんなこともできたような気がするな。」
「自力で実装する前に調べてみようかな?」
と気付けると、時短 & コード量削減できる可能性が生まれます。
ではでは、お楽しみください!!
Environment
以下の環境で動作確認を行いました。
項目 | version など |
---|---|
OS | Ubuntu 22.04.3 LTS (Google Colaboratory) |
Python | 3.10.12 |
pandas | 1.5.3 |
便利技たち!
0. 一覧
下に行けば行くほど、珍しく、使い道がよくわからなくなっていきます😂
これ見たことないな〜、っていうところだけでも見ていっていただけると嬉しいです^^
※「レア度」は、星が多くなるほど珍しい、もしくは難解な処理であることを意味しています。(独断と偏見により設定)
No | レア度 | メソッド |
---|---|---|
1 | ★ | df.isin() |
2 | ★ | df.columns = ["列名", ..] |
3 | ★ | df.between( , ) |
4 | ★★ | df.to_dict(orient="records") |
5 | ★★ | pd.to_numeric(df["列名"], errors="coerce").notna() |
6 | ★★ | df.apply(func, axis=1) |
7 | ★★★ | df.groupby()["列名"].size() |
8 | ★★★ | df.groupby()["列名"].head(n) |
9 | ★★★ | df.filter(like="部分列名") |
10 | ★★★★ | set_index(["列名"], append=True) |
11 | ★★★★ | df.groupby()["列名"].rank() |
12 | ★★★★ | df.groupby()["列名"].shift() |
13 | ★★★★ | df.groupby()["列名"].transform() |
14 | ★★★★★ | df["列名"].apply(pd.Series) |
15 | ★★★★★ | df.explode() |
16 | ★★★★★ | df.unstack() |
1. レア度:★
- ごく簡単に実装できるもの
- もしくは、頻出メソッド
1-1. isin - 複数候補に対する完全一致判定
動作確認のために、以下のようなサンプルデータを用意します。
df = pd.DataFrame({
"item_name": [
"松屋サイダー",
"pakemon card",
"パリウス",
"あいぽん12",
"イーロン茶"
],
"price": [
140,
350,
4_000_000,
150_000,
120
]
})
df
# 結果
item_name price
0 松屋サイダー 140
1 pakemon card 350
2 パリウス 4000000
3 あいぽん12 150000
4 イーロン茶 120
あなたは今、上記 df
という DataFrame
の中から飲み物 ("イーロン茶"、"松屋サイダー"、"QQレモン" のうちいずれか) が欲しいとします。
こういう時に isin
を使うのです!
drink_names = ["イーロン茶", "松屋サイダー", "QQレモン"]
# 複数の候補に対して、完全一致判定をする
drink_index = df["item_name"].isin(drink_names)
df_extracted: pd.DataFrame = df[drink_index]
df_extracted
# 見事「飲み物」をゲットした!
item_name price
0 松屋サイダー 140
4 イーロン茶 120
この結果は、0行目と4行目だけが ["イーロン茶", "松屋サイダー", "QQレモン"]
のどれかと一致したということ。
ちなみに drink_index
の中身はこうなってる。
drink_index
# 以下の通り
0 True
1 False
2 False
3 False
4 True
Name: column_name1, dtype: bool
True
行だけが DataFrame
から抽出される仕組みなりぃ。
関連記事
1-2. df.columns = - カラム名を一気にrename
今度は、 DataFrame の列名をまとめて全部書き換えるときに使えるワザです。
df = pd.DataFrame({
"item_name": [
"松屋サイダー",
"pakemon card",
"パリウス",
"あいぽん12",
"イーロン茶"
],
"price": [
140,
350,
4_000_000,
150_000,
120
]
})
df
# 結果
item_name price
0 松屋サイダー 140
1 pakemon card 350
2 パリウス 4000000
3 あいぽん12 150000
4 イーロン茶 120
さっきのこいつの、列名を書き換えてしまいましょう。
# えいっ!と代入
df.columns = ["商品名", "価格"]
df
# こうなる
商品名 価格
0 松屋サイダー 140
1 pokemon card 350
2 パリウス 4000000
3 あいぽん12 150000
4 イーロン茶 120
真面目に1列ずつ df.rename(columns={"item_name": "商品名", "price": "価格"})
してあげても良いんですけどね。 columns
に直代入した方が楽。
1-3. df.between( , ) - 2つの引数でレコードを絞り込む
別に、日付のデータである必要はないのですが、わかりやすいので日付の文字列でサンプルを示します。
# サンプルデータ
df: pd.DataFrame = pd.DataFrame(
{
"date": [
"2023-12-03",
"2023-12-04",
"2023-12-05",
"2023-12-06",
"2023-12-07",
"2023-12-08",
"2023-12-09",
]
}
)
これを、 between
で絞り込んでみよう!
# between の両端に挟まれるデータを抽出
df[df['date'].between("2023-12-05", "2023-12-08")]
# 実行結果
date
2 2023-12-05
3 2023-12-06
4 2023-12-07
5 2023-12-08
ちなみに、単純に between
を実行した直後はこうなってるよ。
df['date'].between("2023-12-05", "2023-12-08")
# 実行結果
0 False
1 False
2 True
3 True
4 True
5 True
6 False
Name: date, dtype: bool
この結果を見ると、引数に指定した値と完全一致するレコードも True
で返ってくるみたいですね。
2. レア度:★★
- まぁまぁ簡単に使えるけど、あんまり見かけない。
2-1. to_dict(orient="records") - DataFrame に対する loop を高速化
今は numpy
や pandas
がありますし、できれば loop
せずにベクトル演算、ブロードキャスト演算したいですね。
でも、どうしても大量の loop を回したい時はこうします。
from typing import List
dict_data: List[dict] = df.to_dict(orient="records")
for dict in dict_data:
# loop しないといけない処理
# from_dict で DataFrame に戻す
df_converted: pd.DataFrame = pd.from_dict(dict_data)
こうしないと、 DataFrame
の loop は結構遅いんですよ|・`ω・´)チラリッ
(なんと400倍もの速度差になったりします)
関連記事
2-2. to_numeric(series , errors="coerce") - 数値型に変換できない値を回避
たとえば以下のような DataFrame の "numeric?"
列を float 型に変換したいとします。
df = pd.DataFrame(
{
"numeric?": [1, 2, "3", "4", "5.0", ".1", "l"],
"alpha": ["a", "b", "c", "d", "e", "f", "g"]
}
)
# 実行結果
numeric? alpha
0 1 a
1 2 b
2 3 c
3 4 d
4 5.0 e
5 .1 f
6 l g
実行すると、最終行の "l" (エル) が float 型に変換できない値だったため、エラーになります。
df["numeric?"].astype(float)
# 実行結果
ValueError: could not convert string to float: 'l'
このような大量のデータがある時に、 to_numeric
を使うと、うまく float に変換できる値だけを取り出すことができます。
こんな感じ!
pd.to_numeric(df["numeric?"], errors="coerce").notna()
# 結果
0 True
1 True
2 True
3 True
4 True
5 True
6 False
Name: numeric?, dtype: bool
これで6行目が数値に変換できないとわかったので、除外するのも簡単ですね(*´v`)
# あとはこうする
astypable_indices = pd.to_numeric(df["numeric?"], errors="coerce").notna()
df.loc[astypable_indices, "numeric?"].astype(float)
# 実行結果
0 1.0
1 2.0
2 3.0
3 4.0
4 5.0
5 0.1
Name: numeric?, dtype: float64
関連記事
2-3. apply(func, axis=1) - 1行ごとに関数を実行
DataFrame 内の1行1行を、1つのSeriesとして関数の引数に渡して処理したいときに使います。
たとえば、以下のような関数に1つの Series を渡してみます。
import pandas as pd
# apply メソッドのサンプル
def sample_concat_str(series: pd.Series) -> str:
"""
pd.Series を受け取り、 series 内の文字列を全て連結して返却
Example
------
>>> sample_series = pd.Series(["123", "456", "789"])
>>> sample_concat_str(sample_series)
"123-456-789"
"""
return "-".join(list(series["list"]))
df: pd.DataFrame = pd.DataFrame({
"dummy": [1, 2, 3],
"list": [
["abc", "def"],
["123", "456", "789"],
["hoge"]
]
})
series: pd.Series = df.iloc[1]
series
# series の中身
dummy 2
list [123, 456, 789]
Name: 1, dtype: object
この series
を関数に渡すと以下のようになります。
sample_concat_str(series)
# 結果
'123-456-789'
これを、Series 1個だけではなくて、 DataFrame にある分全部に対して実行させたい場合に以下のようにします。
df.apply(sample_concat_str, axis=1)
# 実行結果
0 abc-def
1 123-456-789
2 hoge
dtype: object
apply
をもっと幅広く応用したい方には、以下の記事もおすすめ( *¯ ꒳¯*)✨
3. レア度:★★★
- 見かけることははっきりいってほとんどないですが、比較的動作が理解しやすい / イメージしやすい処理
- 覚えておくと、活用できるチャンスはきっと廻ってくる(気がする
3-1. groupby と size のコンボ
SQL で言うところの、 GROUP BY
結果に対する COUNT(<列名>)
が実行できます!
普通に使う機会があると思うので、覚えておいて損はないです。
df.groupby(["GROUP BY の対象列", "対象列2", ...])["COUNT したい列名"].size()
# Example
# 「各ユーザーが各商品を何回買ったのか」を知りたい時...
df.groupby(["username", "購入商品"])["購入商品"].size()
実際の使い方はこんな感じ。
df = pd.DataFrame(
{
"username": [
"たけち",
"マティス",
"かつみ",
"マティス",
"ありか",
"ありか",
"たけち",
"たけち",
"ありか",
"マティス",
],
"購入商品": [
"キズサソリ",
"おいしいおゆ",
"アドバンテージボール",
"おいしいおゆ",
"おいしいおゆ",
"たべかけ",
"キズサソリ",
"キズサソリ",
"たべかけ",
"アドバンテージボール"
]
}
)
df
# 実行結果
username 購入商品
0 たけち キズサソリ
1 マティス おいしいおゆ
2 かつみ アドバンテージボール
3 マティス おいしいおゆ
4 ありか おいしいおゆ
5 ありか たべかけ
6 たけち キズサソリ
7 たけち キズサソリ
8 ありか たべかけ
9 マティス アドバンテージボール
ここで、誰が何を何個買ったかを調べるとき、これでOK!
df.groupby(["username", "購入商品"])["購入商品"].size()
# 実行結果
username 購入商品
ありか おいしいおゆ 1
たべかけ 2
かつみ アドバンテージボール 1
たけち キズサソリ 3
マティス おいしいおゆ 2
アドバンテージボール 1
Name: 購入商品, dtype: int64
3-2. groupby().head(n) - グループ別に先頭から n 件データ抽出
以下のようなマラソンの順位表を考えます。
df: pd.DataFrame = pd.DataFrame(
[
{"名前": "桔梗", "性別": "female", "time": "04:58"},
{"名前": "雅人", "性別": "male", "time": "05:02"},
{"名前": "真由", "性別": "female", "time": "05:15"},
{"名前": "慎二", "性別": "male", "time": "05:32"},
{"名前": "達人", "性別": "male", "time": "05:41"},
{"名前": "祥平", "性別": "male", "time": "06:12"}
]
)
df
# 実行結果
名前 性別 time
0 桔梗 female 04:58
1 雅人 male 05:02
2 真由 female 05:15
3 慎二 male 05:32
4 達人 male 05:41
5 祥平 male 06:12
この順位表から、性別ごとに(男女別々に)タイムの短かった方を2人ずつ抜き出したいとしましょう。
これで行けます!
df.groupby(["性別"])[["名前", "性別", "time"]].head(2)
# 実行結果
名前 性別 time
0 桔梗 female 04:58
1 雅人 male 05:02
2 真由 female 05:15
3 慎二 male 05:32
この応用として、例えば「地区」列などがあったときに、「地区ごと」かつ「性別ごと」に上位 n 件を抜き出すこともできるので、結構応用範囲は広いです。
関連記事
3-3. filter - カラム名に対する部分一致検索
こいつは超便利です!
絶対に頭の片隅に入れておいた方がいい!
こんな DataFrame があったとき、
df = pd.DataFrame({
"feat_1": np.arange(0, 5),
"feat_2": np.arange(0, 5),
"feat_3": np.arange(0, 5),
"feat_4": np.arange(0, 5),
"feat_5": np.arange(0, 5),
"label_1": np.arange(0, 5),
"label_2": np.arange(0, 5),
"label_3": np.arange(0, 5)
})
df
# 実行結果
feat_1 feat_2 feat_3 feat_4 feat_5 label_1 label_2 label_3
0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 2
3 3 3 3 3 3 3 3 3
4 4 4 4 4 4 4 4 4
ここから、列名に "feat_" が入っている列だけを取得したいとしましょう。
愚直に df[["feat_1", "feat_2", ...]]
と書いてもいいのですが、以下のようにするともっと簡単に取得できます。
df.filter(like="feat_")
# 実行結果
feat_1 feat_2 feat_3 feat_4 feat_5
0 0 0 0 0 0
1 1 1 1 1 1
2 2 2 2 2 2
3 3 3 3 3 3
4 4 4 4 4 4
これ、覚えておいて損ないですよね?(*´▽`*)
これぞ時短&バグの削減ですよ。
関連記事
4. レア度:★★★★
- 初見だと、何が起こるか全くイメージできない
- 理解して使えるようになっておくと、処理を自分で実装しなくて済むので、非常に時短になる
- 使うチャンスはもしかしたらないかもしれないけど、、、もしその時が来たらかなり役に立つ
4-1. set_index([], append=True) - MultiIndex化
- MultiIndex って、作り方あんまりイメージが沸かないんですけど、、、これはそこそこ覚えやすいです。
-
set_index
はみなさん使ってると思うんですけど、ポイントはappend=True
です。
df.set_index(["index に追加したい column 名"], append=True)
MultiIndex
に良く出くわす方には、ぜひ覚えておいてほしい珠玉の一品(?)です。
実際の使い方はこんな感じです(サンプルデータがしょぼくてすみません。゚・(´^ω^`)・゚。)
df = pd.DataFrame({
"date": ["2023-12-31", "2023-01-01", "2023-01-02"],
"腕立て伏せの回数": [197, 256, 0],
})
df
# 実行結果
date 腕立て伏せの回数
0 2023-12-31 197
1 2023-01-01 256
2 2023-01-02 0
この DataFrame の date 列を index に追加して、 MultiIndex な DataFrame を作りましょう。
これでOK!
df.set_index(["date"], append=True)
# 実行結果
腕立て伏せの回数
date
0 2023-12-31 197
1 2023-01-01 256
2 2023-01-02 0
4-2. groupby と rank のコンボ
グループごとに、特定の列の値の順位を返却してくれます。
df.groupby(["GROUP BY の対象列", "対象列2", ...])["順位を知りたい列名"].rank()
# Example
# 「各ユーザー×商品のグループごとの、購入個数ランキング」が欲しい時...
df.groupby(["user_id", "商品名"])["購入個数"].rank(ascending=False)
更に具体的にを見てみましょう。
df = pd.DataFrame(
{
"username": [
"たけち",
"ありか",
"ありか",
"たけち",
"たけち",
"ありか",
],
"購入商品": [
"キズサソリ",
"おいしいおゆ",
"おいしいおゆ",
"おいしいおゆ",
"キズサソリ",
"おいしいおゆ",
],
"購入個数": [
12,
1,
10,
6,
4,
2,
]
}
)
df
# 出力
username 購入商品 購入個数
0 たけち キズサソリ 12
1 ありか おいしいおゆ 1
2 ありか おいしいおゆ 10
3 たけち おいしいおゆ 6
4 たけち キズサソリ 4
5 ありか おいしいおゆ 2
このようなデータに対して適用すると、以下のように順位を出力してくれます。
df.groupby(["username", "購入商品"])["購入個数"].rank(method="first", ascending=False)
# 出力
0 1.0
1 3.0
2 1.0
3 1.0
4 2.0
5 2.0
Name: 購入個数, dtype: float64
何が嬉しいのかちょっとわかりにくいので、元のデータフレームに代入してみましょう。
こうなります。
df["購入数順位"] = (
df.groupby(["username", "購入商品"])["購入個数"]
.rank(method="first", ascending=False)
)
df.sort_values(by=["username", "購入数順位"])
# 出力結果
username 購入商品 購入個数 購入数順位
2 ありか おいしいおゆ 10 1.0
5 ありか おいしいおゆ 2 2.0
1 ありか おいしいおゆ 1 3.0
3 たけち おいしいおゆ 6 1.0
0 たけち キズサソリ 12 1.0
4 たけち キズサソリ 4 2.0
sort_values
で、グループ化した列名と、ランキングが入力されている列を指定すると、人目にも理解しやすい結果が出力されますよ(*´v`)
どなたかの役に立てれば嬉しいです。
これって、SQLだと何に当たるのかな...?
関連記事
4-3. groupby() と shift() - グループごとに n 行ずらす
これも使いどころがちょっと難しいのですが、たとえば、先程のような商品購入ログであれば、特定ユーザが同名商品を「前回購入した日」をリストに追加したいときなどに使えます。
サンプルデータはこんな感じ。
df = pd.DataFrame(
{
"username": [
"たけち",
"ありか",
"ありか",
"たけち",
"たけち",
"ありか",
],
"購入商品": [
"キズサソリ",
"おいしいおゆ",
"おいしいおゆ",
"おいしいおゆ",
"キズサソリ",
"おいしいおゆ",
],
"購入日": [
"2023-12-01",
"2023-12-03",
"2023-12-05",
"2023-12-05",
"2023-12-07",
"2023-12-10",
]
}
)
df
# 出力
username 購入商品 購入日
0 たけち キズサソリ 2023-12-01
1 ありか おいしいおゆ 2023-12-03
2 ありか おいしいおゆ 2023-12-05
3 たけち おいしいおゆ 2023-12-05
4 たけち キズサソリ 2023-12-07
5 ありか おいしいおゆ 2023-12-10
このデータに対して、「前回購入日」列を追加します。
df["前回購入日"] = (
df.groupby(["username", "購入商品"])["購入日"]
.shift(1)
)
df.sort_values(by=["username", "購入商品", "購入日"])
# 出力
username 購入商品 購入日 前回購入日
1 ありか おいしいおゆ 2023-12-03 NaN
2 ありか おいしいおゆ 2023-12-05 2023-12-03
5 ありか おいしいおゆ 2023-12-10 2023-12-05
3 たけち おいしいおゆ 2023-12-05 NaN
0 たけち キズサソリ 2023-12-01 NaN
4 たけち キズサソリ 2023-12-07 2023-12-01
こんな感じで、前回購入した日付を一気に出力できます。
shift
は、通常の DataFrame に対する shift と同じで負の値や2以上の値も入れられるので、もしかしたらもっと活用できる価値があるかも。
関連記事
以下の記事によると、処理速度的にも申し分ない早さみたいですよ(*´v`)
4-4. groupby() と transform()
groupby
がめっちゃ出てきているのでついでに紹介するぜ!(*´▽`*)
しかし、これも使いどころが難しい。。。
以下の記事がわかりやすいので、これはこの記事に解説をお願いしたい(o*。_。)o
5. レア度:★★★★★
- ハッキリ言って、いつ使うのか全くイメージが沸かない...( ˘·ω·˘ ).。oஇ
- でも、自分で実装しようとしたら大変手間のかかる処理を、関数で一発で実現できるので大助かり!
- 使い方は忘れてもいいけど、「そういえば、
pandas
で用意されているメソッドであんなことできたはずだよね...」って思い出せれば、かなりアドバンテージになります!
5-1. df["列名"].apply(pd.Series)
Series の 1要素内に list型 が入っている際に、それらを展開してくれる。
たとえば、こんな困ったデータがあったとする。
df = pd.DataFrame({
"a": [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]],
"b": [["a", "b", "c", "d", "e"], ["f", "g", "h", "i", "j"]]
})
# 出力
a b
0 [1, 2, 3, 4, 5] [a, b, c, d, e]
1 [6, 7, 8, 9, 10] [f, g, h, i, j]
"a" 列にも "b" 列にも、1要素にまるまるリスト型が入り込んでいる。
これをなんとかして、1要素につき値は一つだけ、という形に変換したい。
そんな時にこうする。
df["b"].apply(pd.Series)
# 実行結果
0 1 2 3 4
0 a b c d e
1 f g h i j
上記は "b" 列を展開した場合の話。 "a" 列も展開したければ、別途 .apply(pd.Series)
してあげるとよい。
関連記事
「5-1.」の補足 (2024/09/12 追記)
よく考えたら、.apply
ってデータ量が大きいと遅いんですよね。。。
以下のやり方の方がおそらく早いのでおすすめです。
# データの準備
df_dummy = pd.DataFrame({
"a": [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]],
"b": [["a", "b", "c", "d", "e"], ["f", "g", "h", "i", "j"]]
})
# 横へカラム内の配列を複数カラムに分散
pd.DataFrame(df_dummy["a"].tolist(), index=df_dummy.index)
# 実行結果
0 1 2 3 4
0 1 2 3 4 5
1 6 7 8 9 10
5-2. explode - 爆ぜろ!
こちらも、Series の 1要素内に list型 が入っている際に、それらを展開してくれる。
df["列名"].apply(pd.Series)
との大きな違いは、各配列の要素数が異なる(ジャグ配列の場合)でも上手く処理してくれるところだろうか。
df = pd.DataFrame({'A': [[0, 1, 2], 'foo', [], [3, 4]],
'B': [5, 1, 1, 6],
'C': ['a', np.nan, [], 'd']})
df
# 実行結果
A B C
0 [0, 1, 2] 5 a
1 foo 1 NaN
2 [] 1 []
3 [3, 4] 6 d
爆発(して縦に分裂)します。
# "A"列に入っている配列を、行方向に分割 (爆発?)
df.explode('A')
# 実行結果
A B C
0 0 5 a
0 1 5 a
0 2 5 a
1 foo 1 NaN
2 NaN 1 []
3 3 6 d
3 4 6 d
explode
メソッドの引数に指定した列以外は、単純に複製される。
メソッド名も動作も楽しいけど、使い所が難しい。。。
関連記事
5-3. unstack - 潰れろ!
MultiIndex
な Series
のいずれかの Index を、 DataFrame の列名へと変換してくれる。
サンプルデータはこちら。
鬼と桃太郎の腕立て伏せ競争?の結果データを考える()
index = pd.MultiIndex.from_tuples([
('ももたろ', '2023-03-04'), ('ももたろ', '2023-03-05'), ('ももたろ', '2023-03-06'),
('鬼', '2023-03-04'), ('鬼', '2023-03-06')
])
series = pd.Series(
[591, 528, 580, 1209, 1221],
index=index,
name="腕立て伏せの回数"
)
series
# 出力
ももたろ 2023-03-04 591
2023-03-05 528
2023-03-06 580
鬼 2023-03-04 1209
2023-03-06 1221
Name: 腕立て伏せの回数, dtype: int64
ここで、後ろから数えて1つめ(level=-1
)の Index を新たな列名として DataFrame にすると、こうなる。
series.unstack(level=-1)
# 出力
2023-03-04 2023-03-05 2023-03-06
ももたろ 591.0 528.0 580.0
鬼 1209.0 NaN 1221.0
これ、いつ使うんじゃ?という感じ。
でも私は仕事で使うことになった。頭の片隅に入れておいて損はない。
関連記事
公式サイトだけど、実行例も載ってるよ
あとがき
こういったメソッドを覚えておくと、難解な処理を自力で実装する手間が省けます。
その結果、以下のような効果が見込まれます!
- ソース量が減る
- 多重ネストが減る
- バグが減る
- 作業工数が減る
- 引継ぎが少しスムーズになる???(これは微妙か?)
というわけで...みなさま、是非是非ご活用くださいませ(*´v`)
p.s.
もう多重ネストのコードは読みたくない...
if
と for
だけで力技で処理を書くのは、どうかどうか辞めてほしい。。。
スマートに行きましょう..._(-ω-`_)⌒)_