PandasのDataFrameとScikit-Learnを用いたテーブルデータの前処理を紹介します。
具体的には、以下の処理について順番に紹介していきます。
テーブルの結合: pd.DataFrame.merge
pd.DataFrame.mergeメソッドでは特定の列をキーとして、基準となるDataFrameに別のDataFrameを結合します。
ここでは、具体例として売上履歴データ(sales_history)と商品カテゴリデータ(categories)を用い、2つのテーブルデータを結合します。
# 売上履歴データ: sales_history
type(sales_history) # pandas.core.frame.DataFrame
print(sales_history)
# 日付 店舗ID 商品ID 商品カテゴリID 商品価格 売上個数
# 0 2021-10-01 1 1 1 10000 2.0
# 1 2021-10-01 1 2 1 20000 NaN
# 2 2021-10-01 1 3 1 15000 1.0
# 3 2021-10-01 1 4 2 1000 3.0
# 4 2021-10-01 1 5 2 5000 9999.0
# 5 2021-10-01 1 6 3 3000 2.0
# 6 2021-10-01 1 7 4 5000 1.0
# 7 2021-10-01 1 8 5 2000 3.0
# 商品カテゴリデータ: categories
type(categories) # pandas.core.frame.DataFrame
print(categoris)
# 商品カテゴリID 商品カテゴリ名
# 0 1 ギフト - ゲーム機
# 1 2 ギフト - 記念品
# 2 3 映画 - DVD
# 3 4 映画 - ブルーレイ
# 4 5 本 - 教育用
結合列の指定: on引数
商品カテゴリIDをキーとして、売上履歴データに商品カテゴリ名を追加します。
キーは引数onで指定できます。
df_data = sales_history.merge(categories, on='商品カテゴリID')
# pd.merge関数を使った以下の記述も可能
# df_data = pd.merge(sales_history, categories, on='商品カテゴリID')
print(df_data)
# 日付 店舗ID 商品ID 商品カテゴリID 商品価格 売上個数 商品カテゴリ名
# 0 2021-10-01 1 1 1 10000 2.0 ギフト - ゲーム機
# 1 2021-10-01 1 2 1 20000 NaN ギフト - ゲーム機
# 2 2021-10-01 1 3 1 15000 1.0 ギフト - ゲーム機
# 3 2021-10-01 1 4 2 1000 3.0 ギフト - 記念品
# 4 2021-10-01 1 5 2 5000 9999.0 ギフト - 記念品
# 5 2021-10-01 1 6 3 3000 2.0 映画 - DVD
# 6 2021-10-01 1 7 4 5000 1.0 映画 - ブルーレイ
# 7 2021-10-01 1 8 5 2000 3.0 本 - 教育用
売上履歴データに商品カテゴリ名を統合したテーブルデータが作成できました。
結合方法の指定: how引数
引数howでテーブルの結合方法を指定できます。結合方法には、互いのテーブルでカラムが一致する部分を取得する内部結合(inner)と、一致しない部分も取得する外部結合(left, right, outer)、直積(cross)があります。
ここではサンプルデータを用いて、内部結合と外部結合の違い確認していきます。
# サンプルデータ
ages = pd.DataFrame([['Taro', 25], ['Jiro', 20], ['Hanako', 18]], columns=['名前', '年齢'])
birthplace = pd.DataFrame([['Taro', 'Tokyo'], ['Jiro', 'Osaka'], ['Saburo', 'Fukuoka']], columns=['名前', '出身'])
print(ages)
# 名前 年齢
# 0 Taro 25
# 1 Jiro 20
# 2 Hanako 18
print(birthplace)
# 名前 出身
# 0 Taro Tokyo
# 1 Jiro Osaka
# 2 Saburo Fukuoka
内部結合: inner
print(ages.merge(birthplace, on='名前', how='inner'))
# 名前 年齢 出身
# 0 Taro 25 Tokyo
# 1 Jiro 20 Osaka
on引数でキーとして指定した「名前」が、双方のテーブルで一致する「Taro」「Jiro」の行が結合されています。
外部結合: left, right, outer
# 外部結合left
print(ages.merge(birthplace, on='名前', how='left'))
# 名前 年齢 出身
# 0 Taro 25 Tokyo
# 1 Jiro 20 Osaka
# 2 Hanako 18 NaN
外部結合leftでは、agesのキーに該当する値が、birthplaceから追加されます。
そのため、"名前"カラムにはagesテーブルにある要素が表示され、Hanakoの出身はbirthplaceテーブルにないためNaNとなってります。
print(ages.merge(birthplace, on='名前', how='right'))
# 名前 年齢 出身
# 0 Taro 25.0 Tokyo
# 1 Jiro 20.0 Osaka
# 2 Saburo NaN Fukuoka
外部結合rightでは、birthplaceのキーに該当する値が、agesから追加されます。
そのため、"名前"のカラムにはbirthplaceテーブルにある要素が表示され、Saburoの年齢はagesテーブルにないためNaNとなっています。
print(ages.merge(birthplace, on='名前', how='outer'))
# 名前 年齢 出身
# 0 Taro 25.0 Tokyo
# 1 Jiro 20.0 Osaka
# 2 Hanako 18.0 NaN
# 3 Saburo NaN Fukuoka
外部結合outerでは、agesもしくはbirthrateに存在する要素をキーとして、統合します。
そのため、"名前"のカラムにはいずれかのテーブルに存在する要素が表示され、年齢、出身の情報がない箇所はNaNとなります。
カラムの並べ替え
テーブルデータを統合した後にカラムの並びを変更したい場合は、DataFrameにカラム名のリストを渡せばリストの順番で並べ替わります。
var_xs = ['日付', '店舗ID', '商品ID', '商品カテゴリID', '商品カテゴリ名', '商品価格', '売上個数']
df_data = df_data[var_xs]
print(df_data)
# 日付 店舗ID 商品ID 商品カテゴリID 商品カテゴリ名 商品価格 売上個数
# 0 2021-10-01 1 1 1 ギフト - ゲーム機 10000 2.0
# 1 2021-10-01 1 2 1 ギフト - ゲーム機 20000 NaN
# 2 2021-10-01 1 3 1 ギフト - ゲーム機 15000 1.0
# 3 2021-10-01 1 4 2 ギフト - 記念品 1000 3.0
# 4 2021-10-01 1 5 2 ギフト - 記念品 5000 9999.0
# 5 2021-10-01 1 6 3 映画 - DVD 3000 2.0
# 6 2021-10-01 1 7 4 映画 - ブルーレイ 5000 1.0
# 7 2021-10-01 1 8 5 本 - 教育用 2000 3.0
欠損値の確認/置換/削除
この章では、結合したテーブルデータの概要を確認するメソッドを紹介したのち、欠損値の置換/削除を行います。
統計情報の確認: describe
describeメソッドでは、カラム単位の要素数、平均値、最大/最小値といった統計情報を確認できます。
df_data.describe()
# 店舗ID 商品ID 商品カテゴリID 商品価格 売上個数
# count 8.0 8.00000 8.000000 8.000000 7.000000
# mean 1.0 4.50000 2.375000 7625.000000 1430.142857
# std 0.0 2.44949 1.505941 6802.048011 3778.510925
# min 1.0 1.00000 1.000000 1000.000000 1.000000
# 25% 1.0 2.75000 1.000000 2750.000000 1.500000
# 50% 1.0 4.50000 2.000000 5000.000000 2.000000
# 75% 1.0 6.25000 3.250000 11250.000000 3.000000
# max 1.0 8.00000 5.000000 20000.000000 9999.000000
リファレンス: pandas.DataFrame.describe
簡潔なサマリーを出力: info
infoメソッドでは、カラム名、欠損していないエントリ数、dtypeといった簡潔なサマリーを確認できます。
df_data.info()
# Int64Index: 8 entries, 0 to 7
# Data columns (total 7 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 日付 8 non-null object
# 1 店舗ID 8 non-null int64
# 2 商品ID 8 non-null int64
# 3 商品カテゴリID 8 non-null int64
# 4 商品カテゴリ名 8 non-null object
# 5 商品価格 8 non-null int64
# 6 売上個数 7 non-null float64
infoメソッドの結果から、テーブルデータは8エントリ(行)あるのに、売上個数は7 non-nullとなっているので、欠損値が1つあることがわかります。
欠損値の確認/欠損率の取得
infoメソッドを用いることで、カラム毎の欠損値数を確認できましたが、
isnullメソッドを用いると、要素毎に欠損値かどうかを判定した結果を得ることができます。
# 欠損値の確認
df_data.isnull()
# 日付 店舗ID 商品ID 商品カテゴリID 商品価格 売上個数 商品カテゴリ名
# 0 False False False False False False False
# 1 False False False False False True False
# 2 False False False False False False False
# 3 False False False False False False False
# 4 False False False False False False False
# 5 False False False False False False False
# 6 False False False False False False False
# 7 False False False False False False False
これを利用し、カラム毎の欠損率を計算した関数calc_missing_rateを作りました。
任意のDataFrameを渡すとカラム毎の欠損率を降順で返します。
# 欠損率を確認する関数
def calc_missing_rate(df: pd.DataFrame):
""" データフレームの列毎の欠損率を降順で出力 """
print((df.isnull().sum()/len(df)).sort_values(ascending=False))
calc_miss_rate(df_data)
# 売上個数 0.125
# 日付 0.000
# 店舗ID 0.000
# 商品ID 0.000
# 商品カテゴリID 0.000
# 商品カテゴリ名 0.000
# 商品価格 0.000
欠損値の削除: dropna
dropnaメソッドでは、欠損値を含む行/列を削除できます。
- 引数
- axis:{0 or ‘index’, 1 or ‘columns’}, default 0
欠損値を含む行を削除するか、列を削除するかを指定 - how: {‘any’, ‘all’}, default ‘any’
削除条件を指定。行もしくは列に1つ以上欠損値を含む(any)か、全てが欠損値(all)か - thresh: int
欠損値ではない要素数を閾値として指定。要素数が閾値以下の場合に削除
- axis:{0 or ‘index’, 1 or ‘columns’}, default 0
# 欠損値を1つ以上含む行を削除
df_data.dropna()
# 日付 店舗ID 商品ID 商品カテゴリID 商品価格 売上個数 商品カテゴリ名
# 0 2021-10-01 1 1 1 10000 2.0 ギフト - ゲーム機
# 2 2021-10-01 1 3 1 15000 1.0 ギフト - ゲーム機
# 3 2021-10-01 1 4 2 1000 3.0 ギフト - 記念品
# 4 2021-10-01 1 5 2 5000 9999.0 ギフト - 記念品
# 5 2021-10-01 1 6 3 3000 2.0 映画 - DVD
# 6 2021-10-01 1 7 4 5000 1.0 映画 - ブルーレイ
# 7 2021-10-01 1 8 5 2000 3.0 本 - 教育用
リファレンス: pandas.DataFrame.dropna
欠損値の置換: fillna
fillnaメソッドでは、欠損値を置換できます。
- 引数
- value: scalar, dict, Series, or DataFrame
置換する値を指定。scalar(数値や文字)を指定すると全ての欠損値がその値で置換される。
要素毎に置換する値を変更する場合は、辞書やSeriesで指定できる。 - method: {‘backfill’, ‘bfill’, ‘pad’, ‘ffill’, None}, default None
欠損値の置換方法決める。'backfill', 'bfill'ではindexの順番に見ていき直前の値で置換する。'pad', 'ffill'ではindexの直後の要素で置換する。
- value: scalar, dict, Series, or DataFrame
fillnaメソッドを用いて、欠損値を0で置換する方法とカラム毎の中央値で置換する方法は次の通りです。
# 欠損値を0で置換
df_data.fillna(0)
# 欠損値を要素毎の中央値で置換
df_data.fillna(df_data.median())
リファレンス: pandas.DataFrame.fillna
dtypeの変換(キャスト): astype, pandas.to_datetime
astypeメソッドを用いることで、dtypeを変換できます。
また、pandas.to_datetimeを用いると日付データをdatetime型に変換できます。datetime型にしておくと日数計算が容易になり、不当な日付が入らなくなるため便利です。
# np.int64型への変換
df_data['売上個数'] = df_data['売上個数'].astype(np.int64)
# datetime型への変換
df_data['日付'] = pd.to_datetime(df_data['日付'])
df_data.dtypes
# 日付 datetime64[ns]
# 店舗ID int64
# 商品ID int64
# 商品カテゴリID int64
# 商品価格 int64
# 売上個数 int64
# 商品カテゴリ名 object
datetime型では、年月日や曜日の取得、datetime同士の演算など、日付に関する便利な機能が利用できます。
date = df_data['日付'][0]
print(date)
# 2021-10-01 00:00:00
# %% 年月日時分秒の取得
print(date.year)
print(date.month)
print(date.day)
print(date.hour)
print(date.minute)
print(date.second)
# 2021
# 10
# 1
# 0
# 0
# 0
# 曜日の取得 0(月曜)~6(日曜)
date.weekday()
# 4
datetime型の使い方や便利な機能については別途まとめて記事にする予定です。
カテゴリカル変数の分割
テーブルデータを確認すると「商品カテゴリ名」にはメインカテゴリ(ギフト、英語、本)とサブカテゴリ(ゲーム機、記念品など)が含まれています。
テーブルデータでは、1つのカラムに複数の要素が含まれていることはよくあるため、カラムを分割する関数add_split_categoryを作成しました。
def add_sub_categories(input, main_category, sub_categories, sep='-'):
""" カテゴリカル変数を複数のサブカテゴリへ分割する
Args:
input (DataFrame): 入力
main_category (str): 分割対象のカラム名
sub_categories (list): 分割後のカラム名
sep (str, optional): 区切り文字. Defaults to '-'.
Returns:
[DataFrame]: 分割後のカラムを追加したDataFrame
"""
buf = input[main_category].str.split(sep, expand=True)
buf.columns = sub_categories
input = input.join(buf)
return input
df_data = add_sub_categories(df_data, '商品カテゴリ名', ['メインカテゴリ', 'サブカテゴリ'], sep=' - ')
df_data
# 日付 店舗ID 商品ID 商品カテゴリID 商品カテゴリ名 商品価格 売上個数 メインカテゴリ サブカテゴリ
# 0 2021-10-01 1 1 1 ギフト - ゲーム機 10000 2 ギフト ゲーム機
# 1 2021-10-01 1 2 1 ギフト - ゲーム機 20000 0 ギフト ゲーム機
# 2 2021-10-01 1 3 1 ギフト - ゲーム機 15000 1 ギフト ゲーム機
# 3 2021-10-01 1 4 2 ギフト - 記念品 1000 3 ギフト 記念品
# 5 2021-10-01 1 6 3 映画 - DVD 3000 2 映画 DVD
# 6 2021-10-01 1 7 4 映画 - ブルーレイ 5000 1 映画 ブルーレイ
# 7 2021-10-01 1 8 5 本 - 教育用 2000 3 本 教育用
- add_sub_categories関数
- DataFrameにおける特定のカラムを分割する
- 引数
- input (pd.DataFrame): 分割対象のカラムを含むDataFrame
- main_category(str): 分割対象のカラム名
- sub_categories(list): 分割後のカラム名
- sep (str, optional): 区切り文字. デフォルトは'-'.
- 返り値
- (pd.DataFrame): 分割後のカラムを追加したDataFrame
add_sub_categories関数では、入力したDataFrameに、分割したカラムを追加して値を返します。
上の例では、df_dataのカラム「商品カテゴリ名」を対象に、区切り文字「' - '」で分割し、分割後のカラム名を「'メインカテゴリ', 'サブカテゴリ'」としています。
カテゴリカル変数を数値へ
カテゴリカル変数を数値に変換する様々な手法が提案されていますが、ここではLabelEncoderとTargetEncoderについて紹介します。
LabelEncoder: sklearn.preprocessing.LabelEncoder
LabelEncoderは文字列のラベルを、[0 ~ クラス数-1]の数値へ変換します。
scikit-learnのLabelEncodeを利用すると簡単に実現でき、その手順は次のとおりです。
- LabelEncoderのインスタンス生成: LabelEncoder()
- 変換対象のラベルの種類を定義: fit()メソッド
- fitメソッドで変換テーブルが作成されます。'ギフト'は'0'にするなど
- 変換テーブルに従って、ラベルを数値に変換: transform()メソッド
- 変換テーブルにないラベルを入力するとValueErrorします。
リファレンス: sklearn.preprocessing.LabelEncoder
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder() #
le.fit(df_data['メインカテゴリ']) # ラベルの一覧を定義
encoded_label = le.transform(df_data['メインカテゴリ'])
### LabelEncode前
print(df_data['メインカテゴリ'].values.tolist())
# ['ギフト', 'ギフト', 'ギフト', 'ギフト', '映画', '映画', '本']
### LabelEncode後
print(encoded_label)
# [0 0 0 0 1 1 2]
ギフト = 0、映画 = 1、本 = 2とそれぞれエンコードされたことが分かります。
LabelEncoderを機械学習に用いる場合の注意点として、学習データと評価データで同じ具変換されるように「ラベルの一覧」には互いのラベルを含める必要があります。
変換結果を元のDataFrameに代入すれば、LabelEncodeは完了です。
def apply_le(series:pd.Series, labels=[]):
""" LabelEncoderの適用
Args:
input (pd.Series): 変換対象のラベル
labels (list, optional): ラベルの種類。指定しない場合はinputから生成する。
Returns:
[list]: 変換後の値
"""
le = LabelEncoder()
if not labels:
labels = series.unique()
le.fit(labels)
encoded_label = le.transform(series)
return encoded_label
df_data['メインカテゴリ'] = apply_le(df_data['メインカテゴリ'])
df_data['サブカテゴリ'] = apply_le(df_data['サブカテゴリ'])
print(df_data)
# 日付 店舗ID 商品ID 商品カテゴリID 商品カテゴリ名 商品価格 売上個数 メインカテゴリ サブカテゴリ
# 0 2021-10-01 1 1 1 ギフト - ゲーム機 10000 2 0 1
# 1 2021-10-01 1 2 1 ギフト - ゲーム機 20000 0 0 1
# 2 2021-10-01 1 3 1 ギフト - ゲーム機 15000 1 0 1
# 3 2021-10-01 1 4 2 ギフト - 記念品 1000 3 0 4
# 5 2021-10-01 1 6 3 映画 - DVD 3000 2 1 0
# 6 2021-10-01 1 7 4 映画 - ブルーレイ 5000 1 1 2
# 7 2021-10-01 1 8 5 本 - 教育用 2000 3 2 3
メインカテゴリおよびサブカテゴリがそれぞれ数値に変換しているのが分かります。
TargetEncoder
Target EncoderはCategory _encodersライブラリで実装されています。利用するにはまず、category_encodersライブラリをpipを使用してインストールします。
$ pip install category_encoders
次に、以下はcategory_encoders
を使用してターゲットエンコーディングを実装する例です。
import pandas as pd
import category_encoders as ce
# サンプルデータ
data = pd.DataFrame({
'Category': ['A', 'B', 'A', 'C', 'B', 'A'],
'Target': [1, 2, 1, 3, 2, 3]
})
# ターゲットエンコーダーの初期化
encoder = ce.TargetEncoder(cols=['Category'])
# データの適合と変換
encoded_data = encoder.fit_transform(data['Category'], data['Target'])
print(encoded_data)
エンコードした出力結果は次のとおりです。同じカテゴリは同じ値に変換されているのがわかります。
元のデータ(Category)をエンコードデータで置き換えたい場合は、data['Category'] = encoded_dataで置換できます。
Category
0 1.948512
1 2.000000
2 1.948512
3 2.130108
4 2.000000
5 1.948512
終わりに
今回紹介した機能はテーブルデータを扱う際には必ずと言って良いほど利用するので、必要な際に参照できるようにしておくと良いと思います。
また、テーブルデータ向けの定番アルゴリズムの使い方はこちらの記事で、
機械学習モデル構築までの流れはこちらの記事でまとめていますので、併せて参考にしてください。
コメント