skip to content
barorin&

pandas前処理まとめ|結合・エンコーディング・欠損の右詰め・単語頻度

/ 7 min read

Table of Contents

はじめに

pandasの前処理について書いてきたメモを、1本にまとめ直しました。

掲載しているコードは pandas 3.0 / scikit-learn 1.9 で実際に動かして確認しています。あわせて、昔よく見かけた書き方がpandas 3で動かなくなる点も補足しました。

表の結合:mergehow

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 の形の列を作ります。key1key2 は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.

titletext をつないで正規化し、ストップワードを除去するまでの流れです。

import nltk
import 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 happy
1 fuga
2 test hello world
3 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 に寄せておくと版差を吸収できます。

この記事を書いた人

barorinのプロフィール画像

barorin公認会計士

会計とITの二足のわらじで働く公認会計士です。Pythonを中心に、会計・監査の実務で実際に使っているコードや、Ubuntu・Raspberry Piの設定メモを書いています。