by 『前処理大全9章』
- 補足:
- 数値型のデータもカテゴリを付与することで、カテゴリ型にできる。具体的には、年齢を10歳未満、10代、20代、30代・・・と分けることで、元々数値型である年齢をカテゴリ値として扱える。
- カテゴリ型は非線形な変化を表現できるが、数値をカテゴリ化した時は、カテゴリ値間の関係性のデータは表現されていない。
- カテゴリ型は、データサイズを減らす方法としても有効
- 内容:
- 9-1 カテゴリ型への変換
- 9-2 ダミー変数化
- 9-3 カテゴリ値の集約
- 9-4 カテゴリ値の組み合わせ
- 9-5 カテゴリ型の数値化
- 9-6 カテゴリ型の補完
9-1 カテゴリ型への変換
Python, Rにはカテゴリ型とブール型があるが、SQLにはブール型しかない。
例)ホテルの予約レコードで、顧客テーブルの性別(sex)をブール型とカテゴリ型に変換
- SQL
- SQLでは、ブール型はBooleanとして提供されているが、カテゴリ型は提供されていない。ブール型に変換するには、CASE文の条件にTrueとなるときの条件式を設定する。
- コード例1
SELECT
CASE WHEN sex = 'man' THEN TRUE ELSE FALSE END AS sex_is_man
FROM
work.customer_tb; - また、データの値の種類ごとにIDを付与することで擬似的にカテゴリ型を実現できる。
- コード例2
-- SEXのカテゴライズマスタを生成
WITH sex_mst AS(
SELECT
sex,
ROW_NUMBER() OVER() AS sex_mst_id
FROM
work.customer_tb
GROUP BY
sex
)
SELECT
base.*,
s_mst.sex_mst_id
FROM
work.customer_tb base
INNER JOIN sex_mst s_mst
ON base.sex = s_mst.sex;
上記では、新たにカテゴリ値とカテゴリIDの対応を示すマスタテーブルを作成し、データ値をカテゴリIDに変換することで擬似的にカテゴリ型を表現している。 - Python
# sexがmanのときにTrueとするブール型を追加
# この真下のコードは、astypeしなくても、条件式なのでブール値が返ってくるがわかりやすくするために、書いたらしい。
customer_tb[['sex_is_man']] = (customer_tb[['sex']] == 'man').astype('bool')
# sexをカテゴリ型に変換
customer_tb['sex_c'] = \
pd.Categorical(customer_tb['sex'], categories=['man', 'woman'])
# astype関数でも変換可能
customer_tb['sex_c'] = customer_tb['sex_c'].astype('category')
# インデックスデータはcodesに格納されている
customer_tb['sex_c'].cat.codes
# マスタデータはcategoriesに格納されている
customer_tb['sex_c'].cat.categories
※カテゴリ型の方が、メモリの節約になるので、大量データを扱う時は、カテゴリ型に変換したほうが良い!
9-2 ダミー変数化
ダミー変数は、カテゴリの種類数と同じだけ生成され、機械学習でモデルを作る時は、だめー変数を一つ減らせる。しかし、減らさない方が良い場合もある。例えば、あるサービスの利用料金が年代別にどの程度の影響を与えるか、機械学習モデルの予測モデルにより分析したいについて考える。ダミー変数を減らさずに分析すれば、各角ダミー変数の重要度(重回帰モデルの各ダミー変数の係数など)により、各年代の利用料金への影響度が簡単にわかる。しかし、ダミー変数を1つ減らしてしまうと、減らしたダミー変数の影響が残っているダミー変数に反映されるので、各年代の利用料金への影響度がわかりにくくなる。
このように、各カテゴリ値の影響度を分析したい場合は、ダミー変数を減らす方法を選択しない方が良い。
例)ホテルの予約レコードにおいて、顧客テーブルの性別(sex)をダミー変数化
- SQL
- CASE文を利用してカテゴリ値の種類ごとにフラグ化することでダミー変数化できるが、カテゴリ値の種類が増えるたびにCASE文を追加する必要があるので、データ内容に依存したコードになってしまうので、あまりおすすめでない。
SELECT
-- 男性フラグを生成
CASE WHEN sex = 'man' THEN TRUE ELSE FALSE END AS sex_is_man,
-- 女性フラグを生成
CASE WHEN sex = 'woman' THEN TRUE ELSE FALSE END AS sex_is_woman
FROM
work.customer_tb;- Python
- get_dummies関数を利用
# ダミー変数化する前にカテゴリ型に変換
customer_tb['sex'] = pd.Categorical(customer_tb['sex'])
# get_dummies関数によりsexをダミー変数化
# drop_firstをFalseにすると、カテゴリ値の全種類の値のダミーフラグを生成
dummy_vars = pd.get_dummies(customer_tb['sex'], drop_firtst=False)
補:drop_first=Trueで、最初のダミーフラグを落として、フラグ数を1つ減らす。
9-3 カテゴリ値の集約
該当するデータ数が少ないカテゴリ値が存在すると、数少ないデータからカテゴリ値の特性を学習してしまい、過学習に陥りやすくなる。なので、データ数が極端に少ないカテゴリ値は、他のカテゴリ値とまとめる(集約する)ことがある。
集約の際には、もしカテゴリ間に関係性がある場合は、なるべく近い関係性のカテゴリにまとめる方がうまく傾向が表れ、予測精度が上がることが多い。
例)ホテルの予約レコードにおいて、顧客テーブルの年齢(age)を10歳区切りでカテゴリ型に変換し、さらに60歳以上の場合は"60歳以上"というカテゴリ値に変換する。
- SQL
- CASE文を活用して、カテゴリ型の集約を実現する。
WITH customer_tb_with_age_rank AS(
SELECT
*,
-- 年齢を10歳区切りでカテゴリ化( FLOOR()は、小数点切り下げする関数)
CAST(FLOOR(age / 10) * 10 AS TEXT) AS age_rank
FROM
work.customer_tb
)
SELECT
customer_id,
age,
sex,
home_latitude,
home_longitude,
-- カテゴリ値を集約
CASE WHEN age_rank = '60' OR age_rank = '70' OR age_rank = '80'
THEN '60歳以上' ELSE age_rank END AS age_rank
FROM
customer_tb_with_age_rank; - Python
- category型でカテゴリ値を集約するときは、マスタデータの更新が必要になる。category型のマスタデータの更新には、add_categories関数とremove_unused_categories関数を用いる。
# pd.Categoricalにより、category型に変換
customer_tb['age_rank'] = \
pd.Categorical(np.floor(customer_tb['age'] / 10) * 10)
# マスタデータに '60歳以上' を追加
customer_tb['age_rank'].cat.add_categories(['60歳以上'], inplace=True)
# 集約するデータを書き換え
# category型は、= または != の判定のみ可能なので、isin関数を利用する
customer_tb.loc[customer_tb['age_rank'].isin([60.0, 70.0, 80.0]), 'age_rank'] \
= '60歳以上'
# 利用されていないマスタデータを削除
customer_tb['age_rank'].cat.remove_unused_categories(inplace=True)
補:remove_unused_categories関数はcategory型のマスタデータを削除する関数。関数の呼び出し元のcategory型のマスタデータから利用されていないマスタデータを削除する。
9-4 カテゴリ値の組み合わせ
カテゴリ値を組み合わせることで、カテゴリ値の種類を増やす。例えば、性別と年代のカテゴリ値を組み合わせ、男性20代、女性50代といった新たなカテゴリ値に拡張する。これにより、男女によって同じ年代で大きく傾向が異なっていても、カテゴリ値により非線形な変化を表現できる。
※組み合わせると、ダミー変数の数がどんどん増えてしまうので、データ量に気をつけること。
例)ホテルの予約レコードにおいて、顧客テーブルの性別(sex)と年齢(age)の10歳区切りのカテゴリ値を組み合わせ、性別/年代のカテゴリ値を生成する。
- SQL
- カテゴリ型がないので、文字列として結合することでカテゴリ値の組み合わせを擬似的に実現する。
SELECT
*,
-- sexと年齢の10歳区切りのカテゴリ値を文字列として間に"_"を加えて結合
sex || '_' || CAST(FLOOR(age / 10) * 10 AS TEXT) AS sex_and_age
FROM
work.customer_tb;
|| は、文字列を連結できる。 - Python
- format関数で、年齢と性別のカテゴリ値を文字列として、間に _ を挟み連結し、カテゴリ値の組み合わせを実現
customer_tb['sex_and_age'] = pd.Categorical(
# 連結する列を抽出
customer_tb[['sex', 'age']]
# lambda関数内でsexと10代区切りのageを _ を挟んで文字列とし連結
.apply(lambda x: '{}_{}'.format(x[0], np.floor(x[1] / 10) * 10),
axis=1)
)
9-5 カテゴリ型の数値化
学習データ量が少ない場合に利用することが多い。しかし、過学習を引き起こしたり、データ本来の意味を失ったりすることがあるので、数値化には注意が必要。
- 数値化の方法:
- 該当するカテゴリ値ごとの指標やカテゴリ値に対応する極値/代表値/バラツキ具合を利用することが多い。
- 数値化の3つの例:製造レコードの製造物の品種の数値化
- 製造物の品種ごとにレコード内の出現回数をカウントし、カテゴリ値の代わりに利用
- 製造物の品種ごとに製造障害率(障害が発生した割合)を計算し、カテゴリ値の代わりに利用
- カテゴリ値ごとの製造障害率を基準に、カテゴリ値ごとの障害発生率の高い順に順位を計算し、カテゴリ値の代わりに利用
※上記のような変換時に、データリークを起こしやすいので注意!
例)製造レコードにおいて、製品種別(type)を製品種別ごとの平均障害率に変換する。ただし、平均障害率の計算は自身のレコードを除いて計算する。
注:製品種別ごとの平均障害率を利用して障害予測モデルを学習する場合、自身のレコードを除くほうがモデルの精度が上げられることが多い。なぜなら、全レコードの製品種別ごとの平均障害率には予測すべき値の情報が含まれてしまっているため。その結果、弱いリークによる過学習を引き起こしてしまう。ただし、自身のレコードの影響がほとんどない程度に平均障害率を算出する元のレコード数が多ければ影響はない。また、機械学習モデルを学習したあとの運用時には、製品種別ごとの平均障害率を利用した障害予測モデルを活用して予測を行うときには、製品種別ごとの平均障害率は学習データ全体の製品種別ごとの平均障害率を利用する。
- SQL
- with句を利用して、製品種別ごとの製造数と障害数を計算してから、製品種別ごとの平均障害率を計算/付与する。
-- 製品種別ごとの製造数と障害数の計算
WITH type_mst AS(
SELECT
type,
-- 製造数
COUNT(*) AS record_cnt,
-- 障害数
SUM(CASE WHEN fault_flg THEN 1 ELSE 0 END) AS fault_cnt
FROM
work.prduction_tb
GROUP BY
type
)
SELECT
base.*,
-- 自身のレコードを除いた製品種別ごとの平均障害率
CAST(t_mst.fault_cnt - (CASE WHEN fault_flg THEN 1 ELSE 0 END) AS FLORT) /
(t_mst.record_cnt - 1) AS type_fault_rate
FROM
work.production_tb base
INNER JOIN type_mst t_mst
ON base.type = t_mst.type;- Python
# 製品種別ごとの障害数
fault_cnt_per_type = production \
.query('fault_flg') \
.groupby('type')['fault_flg'] \
.count()
# 製品種別ごとの製造数
type_cnt = production.groupby('type')['fault_flg'].count()
production['type_fault_rate'] = production[['type', 'fault_flg']] \
.apply(lambda x:
(fault_cnt_per_type[x[0]] - int(x[1])) / type_cont[x[0]] - 1),
axis=1)
9-6 カテゴリ型の補完
カテゴリ型のデータが欠損している場合、どのような欠損なのかを確認した後に、適切な対処方法を選択する必要がある。
- 補完方法:
- 固定値による補完:
任意の値を指定し、欠損値の補完値として利用する。カテゴリ型の場合、「その他」や「欠損」といったカテゴリ値を新たに作り、欠損値を新たなカテゴリ値として利用するのが一般的。本来は存在しないカテゴリ値を作ってしまうことになり、ありもしない傾向を学んでしまうことがあるので、非推奨。 - 集計値による補完:
欠損していないデータから最頻値を計算し、欠損値の補完値として利用する。たとえば、顧客の性別で男性の方が多ければ、性別不明の顧客の性別を男性として補完する。ただし、指定した最頻値のデータが極端に増えてしまうので、欠損値が多い場合は非推奨。 - 欠損していないデータに基づく予測値により補完:
欠損していない列の値と一部欠損している列の値の関係から、欠損している値を予測して補完する方法。予測するための関係は、機械学習モデルなどによって表現される。また、欠損していない列を1列利用することもあれば、複数列利用することもある。
例えば、一部の年収ランクのデータが欠けていた場合に、年収ランクと年齢や職業の関係を分析し、年齢や職業から欠損している年収ランクを予測し、補完する。数値と同様に、多重代入法を使うことも多い。 - 時系列の関係から補完
欠損しているデータの前後のデータから欠損値を予測して補完する方法。カテゴリ型であまり利用することはない。例えば、2016年の居住地が不明で、2015年と2017年の居住地が同じであればその値で補完し、2015年と2017年の居住地が異なればどちらかの値をランダムに選択して補完する。 - 多重代入法
欠損値の補完方法について(数値型への前処理2)を参照 - 最尤法
欠損値の補完方法について(数値型への前処理2)を参照
例)KNNによる補完
対象データは、fault_flgに欠損が存在する製造レコード。fault_flgが欠損していないデータを用いた予測結果から、欠損しているfault_flgを補完する。予測にはKNNを用いる
- Python
from sklearn.neighbors import KNeighborsClassifier
# replace関数により、Noneをnanに変換
production_missc_tb.replace('None', np.nan, inplace=True)
# 欠損していないデータの抽出
train = production_missc_tb.dropna(subset=['type'], inplace=False)
# 欠損しているデータの抽出 ( index.difference()は、引数のindex(行番号)と異なるindexを返す関数。)
test = production_misc_tb.loc[production_missc_tb.index.difference(train.index), :]
#k=3で、knnモデル生成
kn = KNeighborsClassifier(n_neighbors=3)
# knnモデル学習
kn.fit(train[['length', 'thickness']], train['type'])
# knnモデルによって予測値を計算し、typeを補完
test['type'] = kn.predict(test[['length', 'thickness']])
0コメント