【テーブルデータ】pythonにおける定番の前処理【欠損値/型変換/結合】

python_table_preprocess_summary TECHNOLOGY

PandasのDataFrameとScikit-Learnを用いたテーブルデータの前処理を紹介します。
具体的には、以下の処理について順番に紹介していきます。

テーブルの結合: pd.DataFrame.merge

pd.DataFrame.mergeメソッドでは特定の列をキーとして、基準となるDataFrameに別のDataFrameを結合します。

リファレンス:pandas.DataFrame.merge

ここでは、具体例として売上履歴データ(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つあることがわかります。

リファレンス: pandas.DataFrame.info

欠損値の確認/欠損率の取得

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
      欠損値ではない要素数を閾値として指定。要素数が閾値以下の場合に削除
# 欠損値を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の直後の要素で置換する。

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のカラム「商品カテゴリ名」を対象に、区切り文字「' - '」で分割し、分割後のカラム名を「'メインカテゴリ', 'サブカテゴリ'」としています。

カテゴリカル変数を数値へ

カテゴリカル変数を数値に変換する様々な手法が提案されていますが、ここではLabelEncoderTargetEncoderについて紹介します。

LabelEncoder: sklearn.preprocessing.LabelEncoder

LabelEncoderは文字列のラベルを、[0 ~ クラス数-1]の数値へ変換します。
scikit-learnのLabelEncodeを利用すると簡単に実現でき、その手順は次のとおりです。

  1. LabelEncoderのインスタンス生成: LabelEncoder()
  2. 変換対象のラベルの種類を定義: fit()メソッド
    • fitメソッドで変換テーブルが作成されます。'ギフト'は'0'にするなど
  3. 変換テーブルに従って、ラベルを数値に変換: 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

終わりに

今回紹介した機能はテーブルデータを扱う際には必ずと言って良いほど利用するので、必要な際に参照できるようにしておくと良いと思います。
また、テーブルデータ向けの定番アルゴリズムの使い方はこちらの記事で、

機械学習モデル構築までの流れはこちらの記事でまとめていますので、併せて参考にしてください。

コメント

タイトルとURLをコピーしました