カテゴリカルデータ処理#

カテゴリカルデータとは?#

項目やラベルを区別するために与えられる、文字列または数値の集合を カテゴリ と呼びます。

原則的にカテゴリ値は有限の集団であり、これを解析に適用するために通常何らかの手法で数値データへの置き換えを考えます。しかし通常カテゴリには大小関係はありません(より専門的には名義尺度と言います)。このようなデータは 非順序データ (non-ordinal data) と呼ばれます。

非順序データに対して、大小の区分のあるカテゴリカルデータも存在します。例えば、大相撲の番付には明確な階級があります(例えば横綱、大関、関脇、小結、前頭、これらはさらに幕内というカテゴリに属します。また幕内の下に十両が存在します)。これは 順序データ(ordinal data) と呼ばれ、順序データでは階級に対して適当な「重み」が設けられる場合があります。

カテゴリは「どれだけ違うか」ではなく「値が異なる」ことが重要な判断基準となります。例えば、観客数5000人と観客数1000人はその差を求めることができ、その差の値にも意味があるため、カテゴリとして扱うより数値として扱うべきということになります。一方で、一見して数値のように扱えそうな郵便番号などは数値として扱うことは避けるべきです。これらは数字を用いていても大小関係や差の値が一般に意味を持たないからです。

カテゴリはときに大規模になります。顧客データを例にとると、ユーザーを識別するIDはサービスを利用するユーザ数が増えれば増えるほど大きくなり、計算コストが高くなってしまいます。そうした場合はより多くのカテゴリ数に対応できる 特徴量エンジニアリング が求められます。

本項では、以上の様な性質をもつカテゴリカルデータに対する、基本的な前処理手法を紹介します。

データの読み込み#

import warnings

warnings.filterwarnings("ignore")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

pd.set_option("display.max_rows", 10)
pd.set_option("display.max_columns", 10)

fpath = "../data/2020413_raw.csv"
df = pd.read_csv(fpath, index_col=0, parse_dates=True, encoding="shift-jis")

今回はカテゴリカルデータの前処理を行うので、数値型以外の要約統計量を確認することとします。

df.describe(include=["object"])
気化器圧力_MODE 気化器液面レベル_MODE 気化器温度_PV 気化器ヒータ熱量_MODE 反応器外殻温度_MODE ... 原材料O2供給量_MODE セパレータ蒸気排出量_MODE (Fixed) アブソーバスクラブ流量_MODE (Fixed) アブソーバ還流流量_MODE (Fixed) 気化器液体流入量_MODE (Fixed)
count 86401 86401 86401 86401 86401 ... 86401 86401 86401 86401 86401
unique 1 1 86391 2 2 ... 2 1 2 1 1
top AUT AUT None AUT AUT ... AUT AUT AUT MAN AUT
freq 86401 86401 11 86346 86398 ... 86245 86401 86394 86401 86401

4 rows × 28 columns

ここで要約統計量の情報から、下記の様に少し注意が必要な列が存在することがわかります。

  • 気化器温度_PV, 熱交換器出口温度(リサイクルガス)_PV :おそらく元々数値型だったと思われるので数値型に変換

    • None(文字列?)が混入して悪さしてるっぽい

    • カテゴリカルデータでないので対象列は削除する

  • unique = 1:終始同じ状態(=同じカテゴリ)ということ

    • 値の変化が無いので対象列は削除する

  • unique > 1:前処理の対象とする

# 気化器温度_PV, 熱交換器出口温度(リサイクルガス)_PVの列をdropする
df = df.drop(columns=["気化器温度_PV", "熱交換器出口温度(リサイクルガス)_PV"])

# unique = 1のobject列をdropする
drop_list = list(df.describe(include=["object"]).loc["unique"] == 1)
df = df.drop(columns=df.select_dtypes(include="object").columns[drop_list])

また、今回の記事では数値型データは取り扱わないので、すべてdropします。
(この処理は記事の簡単化のために行うもので、当たり前ですが通常はするべきではありません。念のため。)

df = df.drop(columns=df.describe().columns)

再度要約統計量を確認します。適切に不要な列をdropできていることがわかります。

df.describe(include=["object"])
気化器ヒータ熱量_MODE 反応器外殻温度_MODE コンプレッサーヒーター熱量_MODE 有機生成物流量_MODE.1 原材料O2供給量_MODE アブソーバスクラブ流量_MODE (Fixed)
count 86401 86401 86401 86401 86401 86401
unique 2 2 2 2 2 2
top AUT AUT AUT AUT AUT AUT
freq 86346 86398 86315 86399 86245 86394
df
気化器ヒータ熱量_MODE 反応器外殻温度_MODE コンプレッサーヒーター熱量_MODE 有機生成物流量_MODE.1 原材料O2供給量_MODE アブソーバスクラブ流量_MODE (Fixed)
2020-04-13 00:00:00 AUT AUT AUT AUT AUT AUT
2020-04-13 00:00:01 AUT AUT AUT AUT AUT MAN
2020-04-13 00:00:02 AUT AUT AUT AUT AUT AUT
2020-04-13 00:00:03 AUT AUT AUT AUT AUT AUT
2020-04-13 00:00:04 AUT AUT AUT AUT AUT AUT
... ... ... ... ... ... ...
2020-04-13 23:59:56 AUT AUT AUT AUT AUT AUT
2020-04-13 23:59:57 AUT AUT AUT AUT AUT AUT
2020-04-13 23:59:58 AUT AUT AUT AUT AUT AUT
2020-04-13 23:59:59 AUT AUT AUT AUT AUT AUT
2020-04-14 00:00:00 AUT AUT AUT AUT AUT AUT

86401 rows × 6 columns

カテゴリ型への変換#

通常csvファイルを pandas.DataFrame に読み込んだのみでは、データ型はobject型となっています。

df.dtypes
気化器ヒータ熱量_MODE               object
反応器外殻温度_MODE                object
コンプレッサーヒーター熱量_MODE          object
有機生成物流量_MODE.1              object
原材料O2供給量_MODE               object
アブソーバスクラブ流量_MODE (Fixed)    object
dtype: object

pandas.DataFrame ではカテゴリカルデータのためのcategory 型が用意されているので、基本的にはこの型に変換すると便利です。
object 型で前処理を頑張ることももちろん可能ですが、category 型を用いる方が何かと簡単なのでオススメします。

# カテゴリ型に変換
df = df.astype("category")
df.dtypes
気化器ヒータ熱量_MODE               category
反応器外殻温度_MODE                category
コンプレッサーヒーター熱量_MODE          category
有機生成物流量_MODE.1              category
原材料O2供給量_MODE               category
アブソーバスクラブ流量_MODE (Fixed)    category
dtype: object

category 型に変換することで、例えば以下のメソッドが使えたりします。

# インデックスデータはcat.codesに格納されている
df["気化器ヒータ熱量_MODE"].cat.codes
2020-04-13 00:00:00    0
2020-04-13 00:00:01    0
2020-04-13 00:00:02    0
2020-04-13 00:00:03    0
2020-04-13 00:00:04    0
                      ..
2020-04-13 23:59:56    0
2020-04-13 23:59:57    0
2020-04-13 23:59:58    0
2020-04-13 23:59:59    0
2020-04-14 00:00:00    0
Length: 86401, dtype: int8
# マスターデータはcat.categoriesに格納されている
df["気化器ヒータ熱量_MODE"].cat.categories
Index(['AUT', 'MAN'], dtype='object')

ダミー変数化#

あらかじめ category 型に変換しておくことで、カテゴリカルデータを簡単に ダミー変数 (dummy variable) に変換することもできます。

ダミー変数とはカテゴリカルデータを適切に「0」と「1」の数値の組に変換したものです。カテゴリカルデータは数値でないのでそのままだと回帰モデルや分類モデルの説明変数として用いることができないのですが、ダミー変数に変換することでそれが可能になります。

二値のカテゴリに対応するダミー変数を作成する場合#

カテゴリのどちらか一方を「0」、もう片方を「1」と変換してダミー変数を作ります。例えば

  • はい → 1、いいえ → 0

  • 含まれる → 1、含まれない → 0

  • 男 → 1、女 → 0

などです。

作ったダミー変数に名前をつけておくと分かりやすくなります。計量経済学の分野では「○○ダミー」という名前がよく使われています。

多値のカテゴリに対応するダミー変数を作成する場合#

この場合、取りうるカテゴリの数と同じだけのダミー変数を作ることで変換可能です。例えば、曜日をダミー変数に変換する場合であれば

  • 月曜日ダミー : 月曜日を1、その他の曜日を0とした数列

  • 火曜日ダミー : 火曜日を1、その他の曜日を0とした数列

  • 水曜日ダミー : 水曜日を1、その他の曜日を0とした数列

  • 木曜日ダミー : 木曜日を1、その他の曜日を0とした数列

  • 金曜日ダミー : 金曜日を1、その他の曜日を0とした数列

  • 土曜日ダミー : 土曜日を1、その他の曜日を0とした数列

  • 日曜日ダミー : 日曜日を1、その他の曜日を0とした数列

の合計7つのダミー変数を作ります。すなわち、ダミー変数化ではuniqueなカテゴリの数だけ新たに列が増えることになります。

pandasでは get_dummies() 関数を用いることで category 型の列をダミー変数に変換できます。

df_dummy = pd.get_dummies(df)
df_dummy
気化器ヒータ熱量_MODE_AUT 気化器ヒータ熱量_MODE_MAN 反応器外殻温度_MODE_AUT 反応器外殻温度_MODE_MAN コンプレッサーヒーター熱量_MODE_AUT ... 有機生成物流量_MODE.1_MAN 原材料O2供給量_MODE_AUT 原材料O2供給量_MODE_MAN アブソーバスクラブ流量_MODE (Fixed)_AUT アブソーバスクラブ流量_MODE (Fixed)_MAN
2020-04-13 00:00:00 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 00:00:01 1 0 1 0 1 ... 0 1 0 0 1
2020-04-13 00:00:02 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 00:00:03 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 00:00:04 1 0 1 0 1 ... 0 1 0 1 0
... ... ... ... ... ... ... ... ... ... ... ...
2020-04-13 23:59:56 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 23:59:57 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 23:59:58 1 0 1 0 1 ... 0 1 0 1 0
2020-04-13 23:59:59 1 0 1 0 1 ... 0 1 0 1 0
2020-04-14 00:00:00 1 0 1 0 1 ... 0 1 0 1 0

86401 rows × 12 columns

このとき 気化器ヒータ熱量_MODE 列には AUTMAN の2つのカテゴリが存在し、 get_dummies() をすることで 気化器ヒータ熱量_MODE_AUT 列( AUT → 1 , MAN → 0)と 気化器ヒータ熱量_MODE_MAN 列( AUT → 0, MAN → 1)が作成されます。

なお、これ以降の分析においては、作成したそれぞれのダミー変数の組のうち「取りうるカテゴリの数から1つ少ない」ダミー変数のみを使用すべき点に注意です。

というのも、一つのカテゴリカルデータから作ったダミー変数を全て用いると完全な多重共線性が発生してしまい、以降の分析結果が不正確なものになってしまうためです。具体的な多重共線性の問題については「次元削減」のページも参考にしてください。

多重共線性の発生を確認するには 気化器ヒータ熱量_MODE_AUT 列と 気化器ヒータ熱量_MODE_MAN 列を散布図にプロットしてみても良いのですが、ここでは簡単に相関係数を計算してみることとします。

df_dummy[["気化器ヒータ熱量_MODE_AUT", "気化器ヒータ熱量_MODE_MAN"]].corr()
気化器ヒータ熱量_MODE_AUT 気化器ヒータ熱量_MODE_MAN
気化器ヒータ熱量_MODE_AUT 1.0 -1.0
気化器ヒータ熱量_MODE_MAN -1.0 1.0

当然の結果ですが、unique=2の場合は相関係数が-1.0となり、これをそのまま用いると以降の分析は多重共線性の影響で壊滅必至でしょう。

したがって、通常は一つのカテゴリカルデータから作ったダミー変数の組ごとに1列削除したものを用います。すなわちunique-1個の列を使用します。pandasでは、 get_dummies() のキーワード引数を指定することでこの処理を簡単に行うことができます。

df_dummy = pd.get_dummies(df, drop_first=True)
df_dummy
気化器ヒータ熱量_MODE_MAN 反応器外殻温度_MODE_MAN コンプレッサーヒーター熱量_MODE_MAN 有機生成物流量_MODE.1_MAN 原材料O2供給量_MODE_MAN アブソーバスクラブ流量_MODE (Fixed)_MAN
2020-04-13 00:00:00 0 0 0 0 0 0
2020-04-13 00:00:01 0 0 0 0 0 1
2020-04-13 00:00:02 0 0 0 0 0 0
2020-04-13 00:00:03 0 0 0 0 0 0
2020-04-13 00:00:04 0 0 0 0 0 0
... ... ... ... ... ... ...
2020-04-13 23:59:56 0 0 0 0 0 0
2020-04-13 23:59:57 0 0 0 0 0 0
2020-04-13 23:59:58 0 0 0 0 0 0
2020-04-13 23:59:59 0 0 0 0 0 0
2020-04-14 00:00:00 0 0 0 0 0 0

86401 rows × 6 columns

カテゴリ値の集約#

状況によっては、いくつかのカテゴリ値をひとまとめにしたい場合もあるかもしれません。

本稿で読み込んでいるデータでは各列のカテゴリ値が高々2つしかないので、例示のため ['AUT', 'CAS', 'MAN'] という3つのカテゴリ値を持つ 架空の制御量_MODE という列を用意します。

df_concat = df.copy()

# ['AUT', 'CAS', 'MAN'] の3カテゴリがある列を追加
df_concat["架空の制御量_MODE"] = np.random.choice(
    ["AUT", "CAS", "MAN"], size=(len(df),)
)
df_concat["架空の制御量_MODE"] = df_concat["架空の制御量_MODE"].astype("category")

このうち ['CAS', 'MAN'] をひとまとめにして、新しく ['Except_AUT'] というカテゴリ値にすることを考えます。

df_concat["架空の制御量_MODE"]
2020-04-13 00:00:00    AUT
2020-04-13 00:00:01    MAN
2020-04-13 00:00:02    CAS
2020-04-13 00:00:03    AUT
2020-04-13 00:00:04    CAS
                      ... 
2020-04-13 23:59:56    AUT
2020-04-13 23:59:57    AUT
2020-04-13 23:59:58    CAS
2020-04-13 23:59:59    CAS
2020-04-14 00:00:00    MAN
Name: 架空の制御量_MODE, Length: 86401, dtype: category
Categories (3, object): [AUT, CAS, MAN]
# マスタデータに'Except_AUT'を追加
df_concat["架空の制御量_MODE"].cat.add_categories(["Except_AUT"], inplace=True)

# 集約するデータを書き換え['CAS', 'MAN']→['Except_AUT']
# category型は==または!=の判定のみ可能なので、isin関数を利用すると良い
df_concat.loc[
    df_concat["架空の制御量_MODE"].isin(["CAS", "MAN"]), "架空の制御量_MODE"
] = "Except_AUT"

# 利用されていないマスターデータ['CAS', 'MAN']の削除
df_concat["架空の制御量_MODE"].cat.remove_unused_categories(inplace=True)

以上の操作で、 ['CAS', 'MAN']['Except_AUT'] に変換し、マスターデータも置き換えることができました。

df_concat["架空の制御量_MODE"]
2020-04-13 00:00:00           AUT
2020-04-13 00:00:01    Except_AUT
2020-04-13 00:00:02    Except_AUT
2020-04-13 00:00:03           AUT
2020-04-13 00:00:04    Except_AUT
                          ...    
2020-04-13 23:59:56           AUT
2020-04-13 23:59:57           AUT
2020-04-13 23:59:58    Except_AUT
2020-04-13 23:59:59    Except_AUT
2020-04-14 00:00:00    Except_AUT
Name: 架空の制御量_MODE, Length: 86401, dtype: category
Categories (2, object): [AUT, Except_AUT]

カテゴリ型の数値化#

カテゴリカルデータを数値データに変換する方法としては、すでにダミー変数化を紹介しました。

ここではダミー変数化以外の方法として、解析者が適当に特徴量エンジニアリングを行い、それを数値として表現する方法を紹介します。

シンプルな方法として、例えば各カテゴリ値の出現頻度(割合)をそのまま数値データとして利用してみることが考えられます。

df_numeric = df.copy()

# 各列ごとカテゴリ値の出現頻度を辞書型として格納
frequency = {
    column: dict(df_numeric[column].value_counts() / len(df_numeric))
    for column in df_numeric.columns
}

# 格納した辞書型をそのまま置換ルールとする
df_numeric = df_numeric.replace(frequency)
df_numeric
気化器ヒータ熱量_MODE 反応器外殻温度_MODE コンプレッサーヒーター熱量_MODE 有機生成物流量_MODE.1 原材料O2供給量_MODE アブソーバスクラブ流量_MODE (Fixed)
2020-04-13 00:00:00 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 00:00:01 0.999363 0.999965 0.999005 0.999977 0.998194 0.000081
2020-04-13 00:00:02 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 00:00:03 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 00:00:04 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
... ... ... ... ... ... ...
2020-04-13 23:59:56 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 23:59:57 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 23:59:58 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-13 23:59:59 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919
2020-04-14 00:00:00 0.999363 0.999965 0.999005 0.999977 0.998194 0.999919

86401 rows × 6 columns

df_numeric.dtypes
気化器ヒータ熱量_MODE               float64
反応器外殻温度_MODE                float64
コンプレッサーヒーター熱量_MODE          float64
有機生成物流量_MODE.1              float64
原材料O2供給量_MODE               float64
アブソーバスクラブ流量_MODE (Fixed)    float64
dtype: object

この辺の数値型への変換方針は、いわゆる特徴量エンジニアリングに関わる話で、個別具体的な状況に応じて柔軟に行う必要があります。
直感的には、本項で取り上げたように同一のカテゴリ値は同一の数値に対応させるべきですが、時系列データの時間発展や画像データのピクセル位置を考慮したい場合などは、冒頭でも述べた様に適当な重みをつけてみても良いかもしれません。

カテゴリ型の補完#

カテゴリカルデータにも欠損値が存在する場合があります。
これらの欠損値は、当該列の欠損していないカテゴリ値、あるいはそれ以外の列の情報から、良しなに推定して補完することが多いです。

補完方法はさまざま提案されていますが、ここではその中でも \(K\)-近傍法 ( \(K\)-neighbors; \(K\)-最近傍法, \(K\)-nearest neighbors, KNNとも) を紹介します。

本稿で読み込んでいるデータには、幸いにも欠損値が含まれていないので、例示のため コンプレッサーヒーター熱量_MODE の列を10%程無作為に欠損させたデータを用意します。

df_lack = df.copy()

# 欠損対象行のインデックスを10%程無作為抽出
lack_index = df_lack.sample(frac=0.1).index

# 欠損対象行の['コンプレッサーヒーター熱量_MODE']を答えとして記録しておく
answer = df_lack.loc[lack_index, "コンプレッサーヒーター熱量_MODE"]

# 欠損対象行の['コンプレッサーヒーター熱量_MODE']をnanとする
df_lack.loc[lack_index, "コンプレッサーヒーター熱量_MODE"] = np.nan

このデータから、 sklearn に含まれている KNeighborsClassifier を用いて欠損値補完を行います。

# KNeighborsClassifierをsklearnライブラリから読み込み
from sklearn.neighbors import KNeighborsClassifier

# 欠損していないデータの抽出
train = df_lack.dropna(subset=["コンプレッサーヒーター熱量_MODE"], inplace=False)

# 欠損しているデータの抽出
test = df_lack.loc[df_lack.index.difference(train.index)]

# knnモデル生成、n_neighborsはknnのkパラメータ
# 例えばn_neighbors=3だと、標本の近傍上位3点に基づいて標本のクラスを決定する
# 特にn_neighbors=1の場合は単に「最近傍法」と呼ばれている
kn = KNeighborsClassifier(n_neighbors=3)

# knnモデル学習
# このとき、説明変数の入力は数値型(あるいは数値型に変換できる文字型)である必要がある。
# ここでは先ほど「カテゴリ型の数値化」で用いた置換辞書frequencyを使用する。
kn.fit(
    train[train.columns.difference(["コンプレッサーヒーター熱量_MODE"])].replace(
        frequency
    ),
    train["コンプレッサーヒーター熱量_MODE"],
)

# knnモデルによって予測値を計算し、'コンプレッサーヒーター熱量_MODE'を補完
test["コンプレッサーヒーター熱量_MODE"] = kn.predict(
    test[test.columns.difference(["コンプレッサーヒーター熱量_MODE"])].replace(
        frequency
    )
)

以上の操作で欠損値が良しなに補完されていることが確認できます。
また正解率を計算すれば、約99.9%が真のカテゴリ値で補完できていることが見て取れます。

test["コンプレッサーヒーター熱量_MODE"]
2020-04-13 00:00:16    AUT
2020-04-13 00:00:17    AUT
2020-04-13 00:00:20    AUT
2020-04-13 00:00:29    AUT
2020-04-13 00:00:38    AUT
                      ... 
2020-04-13 23:58:17    AUT
2020-04-13 23:58:34    AUT
2020-04-13 23:58:47    AUT
2020-04-13 23:58:50    AUT
2020-04-13 23:59:37    AUT
Name: コンプレッサーヒーター熱量_MODE, Length: 8640, dtype: object
# 正解率
(test["コンプレッサーヒーター熱量_MODE"] == answer.sort_index()).sum() / len(answer)
0.999537037037037

小括#

本項ではカテゴリカルデータの基本的な前処理手法をいくつか紹介しました。

カテゴリカルデータの取り扱いは、特徴量エンジニアリングとも密に絡み合い、属人的なセンスが多少なりとも含まれますが、実案件の前処理としては避けては通れないものだと思います。
例えば生産プロセスのデータを想定すると、工場には多数の離散的な設定値(スイッチなど)が存在し、それらの切り替えと組み合わせで環境が変化すると考えるのが自然です。
重要なので再掲しますが、カテゴリカルデータを取り扱う際には多重共線性を常に意識してください。これを怠ると、特徴量エンジニアリング以前の問題として、そもそもの解析結果の信憑性にも重大な影響を残す可能性が出てきます。

※多重共線性は超重要なので、不安な方はこちら「次元削減」のページを参照してください。

参考文献#