pandas前処理まとめ|結合・エンコーディング・欠損の右詰め・単語頻度
/ 7 min read
Table of Contents
はじめに
pandasの前処理について書いてきたメモを、1本にまとめ直しました。
掲載しているコードは pandas 3.0 / scikit-learn 1.9 で実際に動かして確認しています。あわせて、昔よく見かけた書き方がpandas 3で動かなくなる点も補足しました。
表の結合:merge の how
pd.merge(df1, df2, on="key", how="inner")| how | 説明 |
|---|---|
inner |
デフォルト。両方のDataFrameに含まれるキーだけを残して結合 |
left |
1つ目に渡したDataFrameのキーをすべて残して結合 |
right |
2つ目に渡したDataFrameのキーをすべて残して結合 |
outer |
すべてのキーを残して結合 |
キーが df1 に3件・df2 に3件・共通2件のとき、行数は inner 2件、left 3件、right 3件、outer 4件になります。
複合キーを作る
key1_key2 の形の列を作ります。key1 と key2 はint型を想定しています。
df["combine_key"] = df["key1"].astype(str) + "_" + df["key2"].astype(str)以前は df.apply(lambda x: "{}_{}".format(x["key1"], x["key2"]), axis=1) と書いていましたが、apply は行ごとにPythonの関数を呼ぶため遅くなります。列同士の文字列結合で書くほうが速く、読みやすいです。
カテゴリ変数のエンコーディング
ラベルエンコーディング(一括)
カテゴリ列をまとめて整数に置き換えます。
from sklearn.preprocessing import LabelEncoder
for col in df.select_dtypes(exclude="number").columns: df[col] = LabelEncoder().fit_transform(df[col])one-hotエンコーディング
学習データとテストデータで列がずれないよう、いったん結合してからエンコードし、あとで分割し直すのが定石です。
import pandas as pd
# 学習データとテストデータの結合n_train = len(df_train)combine = pd.concat([df_train, df_test])
# カテゴリ列を抽出して one-hot 化cat_cols = combine.select_dtypes(exclude="number").columns.tolist()combine = pd.get_dummies(combine, columns=cat_cols)
# 学習データとテストデータに再分割df_train = combine.iloc[:n_train, :].reset_index(drop=True)df_test = combine.iloc[n_train:, :].reset_index(drop=True)分割位置には学習データの行数(n_train)を使います。ここにテストデータの行数を書いてしまうと分割位置がずれるので注意してください。
なお、列を指定せずに pd.get_dummies(combine) と書けば、カテゴリ列は自動で選ばれます。
条件に合致する行だけ右詰めする
こういう表を、
| # | hoge | fuga | piyo |
|---|---|---|---|
| 1 | a | b | NaN |
| 2 | c | NaN | NaN |
| 3 | NaN | d | NaN |
| 4 | e | f | g |
こうしたい場合の話です。
| # | hoge | fuga | piyo |
|---|---|---|---|
| 1 | NaN | a | b |
| 2 | NaN | NaN | c |
| 3 | NaN | NaN | d |
| 4 | e | f | g |
一番右の piyo 列が埋まるまで、欠損している行だけを1つずつ右にずらします。
# piyo列の欠損がゼロになるまでループするwhile df["piyo"].isnull().sum() > 0: # piyo列が欠損している行だけを、1つ右にずらす mask = df["piyo"].isnull() df[mask] = df[mask].shift(axis=1)shift(axis=1) が横方向のシフトです。df[mask] で対象行に限定してから代入することで、条件に合う行だけが動きます。
テキストの前処理
こういうDataFrameを想定します。
| id | title | text |
|---|---|---|
| 0 | hoge | I am so happy. |
| 1 | fuga | NaN |
| 2 | test | Hello World!! |
| 3 | yo | He is 13 years old. |
title と text をつないで正規化し、ストップワードを除去するまでの流れです。
import nltkimport pandas as pd
nltk.download("stopwords")from nltk.corpus import stopwords
df = pd.read_csv("df.csv", header=0)
# 欠損を空文字で埋めるdf["text"] = df["text"].fillna("")
# titleとtextを結合してconcat列を作成df["concat"] = (df["title"] + " " + df["text"]).str.strip()
# 元の列を削除df = df.drop(["id", "title", "text"], axis=1)
# 小文字にするdf["concat"] = df["concat"].str.lower()
# 数字はすべてゼロにするdf["concat"] = df["concat"].str.replace(r"\d", "0", regex=True)
# 英数字と半角スペース以外を削除df["concat"] = df["concat"].str.replace(r"[^\w\s]", "", regex=True)
# ストップワード除去stop_words = set(stopwords.words("english"))df["concat_nostop"] = df["concat"].apply( lambda x: " ".join(w for w in x.split() if w not in stop_words))実行すると concat 列はこうなります。
0 hoge i am so happy1 fuga2 test hello world3 yo he is 00 years oldストップワードの照合は set にしておくと速くなります。リストのままだと1単語ごとに線形探索になります。
単語の出現頻度を出す
テキスト列を空白で分割して、出現回数を降順に集計します。
freq = df["text"].str.split().explode().value_counts()explode() で1単語1行に展開してから value_counts() を取るだけです。辞書に数え上げてソートするループを書かなくても、これで降順の頻度表になります。
上位20語だけ見たい場合はこうします。
freq.head(20)おわりに
前処理は毎回似たようなことを書くので、手癖を1箇所にまとめておくと楽になります。
特にpandas 3での文字列dtypeの変更は、古い記事のコードをコピーしたときに気づきにくいところです。select_dtypes に寄せておくと版差を吸収できます。
