0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python初心者の備忘録 #23 ~機械学習入門編09~

Last updated at Posted at 2025-06-21

はじめに

今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #22 ~機械学習入門編08~
次の記事:Python初心者の備忘録 #24 ~機械学習入門編10~

今回はEDA前処理(preprocessing) についてまとめております。

■学習に使用している資料

Udemy:【本番編】米国データサイエンティストがやさしく教える機械学習超入門【Pythonで実践】

■データサイエンスの流れ

データサイエンスにおいては大体下記のような流れで処理が進んでいく
image.png

  1. EDAでそのデータがどのようなデータなのかを表にしたり、グラフにしたりして把握する
  2. データクリーニングで不必要なデータを削除したりして、目的に対して扱いやすいデータ群に加工する
  3. 前処理特徴量エンジニアリング特徴量選択ハイパーパラメーターチューニングを繰り返して、モデルを作成する
  4. 作成したモデルをアンサンブルしたりして、最後の評価する

■EDA(Exploratory data analysis)

▶EDAとは

  • データの分布やサマリーを可視化して、データの特徴を把握する作業
    • データ(構造)の確認:カラム数、行数、データの意味、データの粒度...etc
    • 欠損地の確認:N/A、0、""...etc
    • 外れ値の確認
    • データのパターンを確認:時系列、季節性、相関...etc
    • ...etc

EDAが間違っていると、その後の作業が全て意味をなさなくなるので、とても重要な工程といえる。

PythonでEDA

今回使用しているデータは下記から参照してください。
https://github.com/Yushin-Tati/Learning_machine_learning
./vgsales.csv

  1. データロード
  2. サンプルデータの表示、カラム名
  3. 欠損値の確認
  4. Object type(カテゴリカル or 数値?)
  5. 各カラムの統計量(min、max、mean...etc)
  6. 各カラム同士の散布図とそれぞれのカラムのヒストグラム
  7. それぞれのカテゴリのレコード数
  8. 外れ値の確認
  9. カラム間の相関
    ...etc
EDA
import pandas as pd
import seaborn as sns
import numpy as np

# データロード
df = pd.read_csv('vgsales.csv')

# データの表示、カラム名
df.head(3)

image.png

# 欠損値確認、Object type
df.info()

★YearとPublisherに欠損値があることがわかる
image.png

# 各カラムの統計量を確認
df.describe()

image.png

# カラム間の分布確認
sns.pairplot(df)

★外れ値がないか、どのようにアクションすればいいのかを把握できる
今回であれば、離れた場所にRankが低くくSalesが低いデータがあるので、それが外れ値かどうか検証する。
C__Users_ytati_Desktop%E3%82%AB%E3%83%A1%E3%81%95%E3%82%93%E8%AC%9B%E5%BA%A7_Python%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E5%85%A5%E9%96%80_%E6%9C%AC%E7%95%AA%E7%B7%A8_EDA_eda.html (1).png

# 最もRankが低いデータ
df[df['Rank']==16599]

image.png

# 最もGlobal_Salesが低いデータと高いデータ
df.sort_values('Global_Sales')

★RankとSalesが低いデータは複数ある
image.png
※Salesが低いデータは全て除外してもいいし、扱ってもいい。やりたいことによって、判別する。

# Publisher単位でGlobal_Salesの合計が高い順
df.groupby('Publisher').sum().sort_values('Global_Sales', ascending=False)

image.png

# Top10を可視化
%matplotlib inline
df.groupby('Publisher').sum().sort_values('Global_Sales', ascending=False)[:10].plot.bar(y='Global_Sales')

image.png

# Yearのユニークな値とそれぞれの値に対するレコード数
# Yearに変な値が入っていないか,またデータセットのYearの傾向を確認
df['Year'].value_counts().sort_index()

image.png
2015年に比べて2016年以降は不自然に数が少ないので、2016年の途中までのデータということも予測できる。

# なぜか記録されている2017と2020のデータを確認
df.sort_values('Year', ascending=False)[:10]

image.png
不自然なので、本当に正しい記録なのか確認するなども必要になってくる。

# カテゴリカル変数の確認
# Genreのユニークな値一覧とそれぞれのGenreのレコード数確認
df['Genre'].value_counts()

image.png
変なものもないので問題なさそう、誤字やおかしなものがあれば修正やデータの信憑性を疑う必要が出てくる

# Platformのユニークな値一覧とそれぞれのPlatformのレコード数確認
df['Platform'].value_counts()

image.png
変なものもないので問題なさそう

# カラム間の相関をヒートマップで確認
sns.heatmap(df.corr(), annot=True)

image.png

# Global_Salesは他のSales値の合計値なのか,別で取ってきた値なのか確認
df['NA_EU_JP_Other'] = df['NA_Sales'] + df['EU_Sales'] + df['JP_Sales'] + df['Other_Sales']
df['|Global_Sales - NA_EU_JP_Other|'] = np.abs(df['Global_Sales'] - df['NA_EU_JP_Other'])
df.sort_values('|Global_Sales - NA_EU_JP_Other|', ascending=False)

image.png
Global_Salesと全体の合計値に最大0.02の差分が出ている。そのことからGlobal_Salesは別でデータを取得してきていると予想できる。
-> 端数まで考慮すると合計値(Global_Sales)に近づくのでは...?

▶データクリーニング

  • EDAの一部として扱われることが多い
    • 不要なカラムの削除:重複カラム、相関係数が1、欠損が多いデータ...etc
    • 不要なデータの削除:重複データ、外れ値、欠損が多いデータ...etc
    • 類似カテゴリの統一:ex.["excellent", "outstanding", "extremely good"]
      ...etc

■前処理(preprocessing)

  • 欠損地対応(EDAの一部として扱われることもある)やカテゴリカル変数のエンコード、特徴量スケーリングなどがある

▶欠損値の対応

  • なぜ欠損があるのかを考える
    • どこまでランダム性があるの
    • 人為的なのかシステム的なのか
  • 欠損値にも様々な種類があることに注意("", ?, 0, -, N/A, nan...etc)
    ※欠損値のデータを落とすと貴重な学習データが減ってしまう
  • 欠損値の対応法
    • 数値カラム:欠損値を代表値(平均値、中央値)で置き換える
    • カテゴリカラム:N/Aを新たなカテゴリとして扱う
    • 欠損値を予測値で代入
      ※欠損値を代入しても、欠損があったことを表すフラグを残しておくのも有効

Pythonで欠損値代入

  • それぞれのカラムの欠損値の数を確認する
  • カテゴリカルカラムの欠損を新たなカテゴリとして扱う
  • 欠損値に代表値を代入する
    • 数値カラム:中央値
    • カテゴリカラム:最頻値
欠損値代入
import pandas as pd
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.model_selection import train_test_split, RepeatedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler, OrdinalEncoder
from sklearn.neighbors import KNeighborsRegressor
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer, make_column_selector
# !pip install category_encoders
from category_encoders import TargetEncoder
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

# データロード
df = pd.read_csv('vgsales.csv')
df.info()

★YearとPublisherに欠損値があることがわかる
image.png

# Yearの欠損数
len(df[df['Year'].isna()])  # -> 271

# Publisherが欠損しているレコードのindex
pub_na_idx = df[df['Publisher'].isna()].index

# Yearが欠損しているレコードのindex
year_na_idx = df[df['Year'].isna()].index

# Publisherの欠損を"NaN"で埋める
# df[['Publisher']] = df[['Publisher']].fillna("NaN")

# 複数のカラムを同時に埋めることも可能
df.fillna({'Publisher': "NaN", 'Year': df['Year'].median()}, inplace=True)
df.iloc[year_na_idx][:4]

image.png

# PublisherにはUnknownという値もあり、これを欠損値として扱っていいのか確認する
# Publisherの欠損とUnkownの分布の違いを確認
pub_nan_df = df[df['Publisher']=='NaN']
pub_unknown_df = df[df['Publisher']=='Unknown']
pub_missing_df = pd.concat([pub_nan_df, pub_unknown_df])
sns.pairplot(pub_missing_df, hue='Publisher')

★ヒストグラムを見ると大体そろっているが、Yearに限っては少しずれているように見えるので、別のカテゴリとして扱った方がよさそう(他にも欠損値がある場合は判断が変わってくる)
preprocessing.png

# SimpleImputerでも欠損値代入が可能
# Yearに中央値,Publisherには最頻値を入れる例
df = pd.read_csv('vgsales.csv')
imputer = SimpleImputer(strategy='median')
df['Year'] = imputer.fit_transform(df[['Year']])
imputer = SimpleImputer(strategy='most_frequent')
df['Publisher'] = imputer.fit_transform(df[['Publisher']])

# 上では特に気にせずYearの欠損値を中央値で埋めたが、Platformから考えるとおかしいものもある(GBAが2007年とは考えにくい)
# Platform別にYearの中央値を計算し,その値で欠損値を埋める
df = pd.read_csv('vgsales.csv')
platform_year_dict = df.groupby('Platform').median()['Year'].to_dict()
df['Year'] = df.apply(lambda row: platform_year_dict[row['Platform']] if np.isnan(row['Year']) and row['Platform'] in platform_year_dict else row['Year'], axis=1)

# それぞれのPlatformに対応してYearの値が入っている
df.iloc[year_na_idx]

image.png

# 学習データとテストデータに分けて、それぞれで欠損値を代入する
# 代表値による欠損代入する場合,代表値は学習データを使用して計算する
df = pd.read_csv('vgsales.csv')

# Global_Salesがあると簡単にJP_Salesを計算できてしまうため、先に落としておく
df.drop('Global_Sales', inplace=True, axis=1) 
target = 'JP_Sales'
X = df.drop(target, axis=1)
y = df[target]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
platform_year_dict = X_train.groupby('Platform').median()['Year'].to_dict()                           
X_train['Year'] = X_train.apply(lambda row: platform_year_dict[row['Platform']] if np.isnan(row['Year']) and row['Platform'] in platform_year_dict else row['Year'], axis=1)

# テストデータにも同様にplatform_year_dictを使用する
X_test['Year'] = X_test.apply(lambda row: platform_year_dict[row['Platform']] if np.isnan(row['Year']) and row['Platform'] in platform_year_dict else row['Year'], axis=1)

image.png

▶kNNを使った欠損値代入

kNNの復習は下記を参照してください。
▶kNN(k Nearest Neighbor:k最近傍法)

  • kNNを使用して欠損値を予測する方法があり、テストデータに対しても同様に学習データを使って予測する
    ※kNNの学習時には目的変数($X_n$)を含まない

image.png

PythonでkNNを使った欠損値代入

  • vgsales.csvの"Year"の欠損値をkNNで予測して埋める
    • sklearn.neighbors.KNeighborsRegressorクラスでkNNを構築し、欠損値を埋める
    • sklearn.impute.KNNImputer(n_neighbors)クラスで欠損値を埋める
  • 交差検証(Cross Validation)やhold-outの前段階で実施する必要があることに注意
kNNで欠損値代入(KNeighborsRegressor)
# データ準備
df = pd.read_csv('vgsales.csv')
df[['Publisher']] = df[['Publisher']].fillna('NaN')
# NameはYearの推測に関係なく、カテゴリが多いので処理に影響するので落としておく
df.drop("Name", axis=1, inplace=True)

# Yearカラムの欠損をKNNで代入する
target = "Year"
X = df.drop(target, axis=1)
y = df[target]

# 数値カラムのリスト取得(標準化の対象)
num_cols = X.select_dtypes(include=np.number).columns.to_list()

# ダミー変数
X = pd.get_dummies(X, drop_first=True)

# 標準化
X[num_cols] = StandardScaler().fit_transform(X[num_cols])

# YearがNaNのデータはテストデータ,そうでなければ学習データ
test_indexes = df[df['Year'].isna()].index
train_indexes = df[~df['Year'].isna()].index
X_train, X_test = X.iloc[train_indexes], X.iloc[test_indexes]
y_train, y_test = y.iloc[train_indexes], y.iloc[test_indexes]

# kNNのモデルを作って予測値を代入する
knn = KNeighborsRegressor(n_neighbors=3).fit(X_train, y_train)
y_pred = knn.predict(X_test)

# 一つ目のテストデータのkNNのYear予測を確認
df.iloc[X_test.head(1).index]
y_pred[0]  # -> 2004.3333333333333

image.png

# neighborとして使用されたデータを確認する
neighbors = knn.kneighbors(X_test.head(1))

# neighbors[1][0]には,予測に使用されたデータのindexが格納されているが,このindexは学習データX_trainのindexであるため,
# 一度reset_index()でindexを振り直して対象データにアクセスし,['index']カラムから元のdfのindexを取得する
df.loc[X_train.reset_index().loc[neighbors[1][0]]['index']] 

image.png

kNNで欠損値代入(KNNImputer(n_neighbors))
# kNNImputerを使う
imputer = KNNImputer(n_neighbors=3)
imputer.set_output(transform='pandas')

# ダミー変数
df = pd.get_dummies(df, drop_first=True)

# 標準化
df[num_cols] = StandardScaler().fit_transform(df[num_cols])
df_imputed = imputer.fit_transform(df)
df_imputed.iloc[test_indexes]

image.png

# kNNImputerの結果↑とKNeighborsRegressor↓が等しいことを確認
y_pred
出力結果
KNeighborsRegressorの結果
array([2004.33333333, 2005.        , 2008.66666667, 2003.33333333,
       1980.66666667, 2009.33333333, 1990.66666667, 2009.        ,
       2009.66666667, 2009.        , 2010.66666667, 2009.66666667,
       1999.        , 2010.66666667, 2008.        , 1982.33333333,
       1982.        , 2002.33333333, 2003.33333333, 2008.        ,
       1998.66666667, 1997.66666667, 2013.66666667, 1982.        ,
       2005.        , 2010.66666667, 2014.33333333, 2002.        ,
       2001.        , 2009.66666667, 2014.33333333, 2014.66666667,
       2006.66666667, 2003.66666667, 2010.33333333, 2009.        ,
       2009.33333333, 2001.66666667, 2011.33333333, 2008.        ,
       2004.        , 2011.        , 2006.66666667, 2006.        ,
       2002.33333333, 2009.        , 2007.33333333, 2003.33333333,
       2004.33333333, 2004.33333333, 2003.        , 1983.66666667,
       2012.33333333, 2010.        , 1983.33333333, 2009.        ,
       1982.33333333, 2003.33333333, 1982.33333333, 2011.66666667,
       1985.        , 2007.33333333, 2009.66666667, 2011.        ,
       2013.66666667, 2007.33333333, 2011.        , 2002.33333333,
       2008.        , 1984.66666667, 2009.        , 2003.        ,
       2007.66666667, 1995.        , 2009.33333333, 2012.33333333,
       2008.        , 1999.        , 2003.66666667, 2007.        ,
       1982.66666667, 2004.33333333, 2003.66666667, 1982.        ,
       2009.33333333, 1998.33333333, 2013.        , 2005.        ,
       2002.33333333, 1997.        , 2004.33333333, 2004.66666667,
       2004.33333333, 1995.33333333, 2004.        , 1989.66666667,
       2008.        , 1982.        , 2009.        , 2009.66666667,
       2010.        , 2002.66666667, 2003.33333333, 2011.66666667,
       2009.        , 2010.33333333, 2010.33333333, 2008.33333333,
       2009.33333333, 2004.        , 2004.        , 2008.33333333,
       2012.        , 2006.33333333, 1984.33333333, 1985.33333333,
       2003.66666667, 2011.        , 2009.66666667, 1997.        ,
       2011.66666667, 2010.33333333, 2009.33333333, 2007.33333333,
       2009.        , 2008.66666667, 1994.        , 2010.33333333,
       2002.66666667, 1994.66666667, 2008.        , 2001.66666667,
       2004.33333333, 2013.66666667, 1999.        , 2010.66666667,
       2000.66666667, 1981.        , 2006.        , 2004.66666667,
       2008.66666667, 1992.33333333, 2010.66666667, 2008.66666667,
       2004.33333333, 1995.        , 2011.66666667, 2003.33333333,
       2009.        , 2009.66666667, 2008.66666667, 2010.33333333,
       2006.        , 2003.66666667, 2003.33333333, 2008.33333333,
       2011.33333333, 2007.33333333, 2010.33333333, 2002.        ,
       2011.        , 2010.33333333, 2012.        , 2008.66666667,
       2009.66666667, 2010.66666667, 2010.33333333, 2010.        ,
       2009.33333333, 2007.        , 2007.66666667, 1996.        ,
       2002.66666667, 2009.33333333, 2009.33333333, 2010.66666667,
       2009.        , 2010.        , 2012.33333333, 2010.66666667,
       2012.33333333, 2006.66666667, 2002.66666667, 2010.66666667,
       2007.        , 2005.66666667, 2005.33333333, 2012.        ,
       2010.66666667, 2009.        , 2010.66666667, 2004.66666667,
       2010.66666667, 2011.66666667, 2010.        , 2009.33333333,
       2006.        , 2003.66666667, 2009.33333333, 2010.66666667,
       2009.66666667, 2005.        , 2006.33333333, 2007.33333333,
       2010.66666667, 2003.33333333, 2011.33333333, 2002.66666667,
       2009.33333333, 2009.66666667, 2014.33333333, 2003.33333333,
       2011.33333333, 1999.        , 2008.        , 2005.        ,
       2006.66666667, 2011.        , 2010.33333333, 2007.        ,
       2009.66666667, 2011.        , 2010.66666667, 2002.33333333,
       2013.33333333, 2009.33333333, 2010.66666667, 2011.        ,
       2005.66666667, 2008.        , 2009.        , 2004.66666667,
       2002.66666667, 2010.66666667, 2004.66666667, 2009.33333333,
       2002.66666667, 2013.66666667, 2009.33333333, 2010.        ,
       2009.33333333, 2009.33333333, 2010.33333333, 2011.66666667,
       2009.66666667, 2009.        , 2005.33333333, 2009.        ,
       2007.66666667, 2007.33333333, 2004.66666667, 2008.        ,
       2008.33333333, 2008.33333333, 2007.33333333, 2010.66666667,
       2007.66666667, 2009.        , 2006.        , 2008.33333333,
       2011.66666667, 2007.66666667, 2010.66666667, 2009.33333333,
       2011.66666667, 2009.33333333, 2003.33333333, 2009.66666667,
       2014.        , 2009.33333333, 2013.33333333])

▶各種欠損値代入手法を比較

次で使用されているデータは下記から取得してください。
https://github.com/Yushin-Tati/Learning_machine_learning
./penguins_size.csv

データの説明
image.png

以下のケースで精度を比較する

  • 欠損値を落としたケース
    • .dropna()
  • 欠損値を新しいカテゴリ(数値は中央値を代入)としたケース
    • sklearn.impute.SimpleImputer().fit_transform()
  • 欠損値をkNNで予測してケース(カテゴリカラムは最頻値で代入)
    • sklearn.impute.KNNImputer(n_neighbors).fit_transform()

その他条件

  • それぞれのカラムから'species'を予測するモデルを作成する
  • モデルは何でもいいが、今回はロジスティック回帰を使用する
  • 5 fold × 3で評価
    • PipelineやColumnTransformerクラスを使用する
  • 評価指標は何でもいいが、今回はloglossを使用

データ準備とEDA

df = pd.read_csv('penguins_size.csv')

# 欠損値の確認
df.info()

image.png

df.describe()

image.png

%matplotlib inline
sns.pairplot(df, hue='species')

★大きくずれた値はないので外れ値の心配はしなくてよさそう
preprocessing. (1).png

sns.heatmap(df.corr(), annot=True)

image.png

# カテゴリカルカラムのユニークな値とそれぞれの値にレコード数
cat_cols = df.select_dtypes(exclude=np.number).columns.to_list()
for cat_col in cat_cols:
    print(f"======{cat_col}======")
    print(df[cat_col].value_counts())

image.png

# "."は欠損値扱いにする
df.loc[df[df['sex']=='.'].index, 'sex'] = np.nan
df[df['sex'].isna()]

★3と339はほとんどデータを持っていないので、実業務では落としてもよさそう
image.png

# それぞれの結果を格納するディクショナリー
results = {}

欠損値を落とすケース

df.dropna(inplace=True)
target = 'species'
X = df.drop(target, axis=1)
y = df[target]

# 前処理
# ダミー変数
X = pd.get_dummies(X, drop_first=True)

# CV
k = 5
n_repeats = 3
cv = RepeatedKFold(n_splits=k, n_repeats=n_repeats, random_state=0)

# Pipeline
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipeline = Pipeline(steps=[('scaler', StandardScaler()), ('model', LogisticRegression())])
scores = cross_val_score(pipeline, X, y, cv=cv, scoring='neg_log_loss')
results['drop'] = -np.mean(scores)

欠損を新カテゴリーとする(カテゴリカル変数) and 中央値で代入(数値変数)

df = pd.read_csv('penguins_size.csv')
# "."は欠損値扱いにする
df.loc[df[df['sex']=='.'].index, 'sex'] = np.nan
target = 'species'
X = df.drop(target, axis=1)
y = df[target]

# CV
k = 5
n_repeats = 3
cv = RepeatedKFold(n_splits=k, n_repeats=n_repeats, random_state=0)

# ダミー変数生成クラスを自作(Pipelineに組み込むため)
class GetDummies(BaseEstimator, TransformerMixin):
    
    def __init__(self):
        self.columns = None

    """
    学習データとテストデータに分けた際に学習データにはあるが、
    テストデータにはカテゴリがないということが発生しうる。
    例:maleとfemaleがあったときに学習データにfemaleが全て偏ってしまい、テストデータにfemaleがない

    その対処として、カテゴリリストを取得しておき、.trasform()でカテゴリを補完する形にしておく。
    ※逆のテストデータにはあるが、学習データにないパターンは問題なし
    """
    def fit(self, X, y=None):
        self.columns = pd.get_dummies(X).columns
        return self

    def transform(self, X):
        X_new = pd.get_dummies(X)
        return X_new.reindex(columns=self.columns, fill_value=0)

# Columns Transformer (imputer)
num_cols = X.select_dtypes(include=np.number).columns.to_list()
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
ct = ColumnTransformer([('imputer_cat', SimpleImputer(strategy='constant', fill_value='NaN'), cat_cols),
                   ('imputer_num', SimpleImputer(strategy='median'), num_cols)])

# デフォルトだとColumnTransformerの結果がNumPyArrayになるが,後続処理で問題になることがあるのでDataFrameにする
ct.set_output(transform='pandas')

# Pipeline (dummy + scaler + model)
pipeline = Pipeline(steps=[('ct', ct),
                ('dummy', GetDummies()),
                ('scaler', StandardScaler()),
                ('model', LogisticRegression())])
scores = cross_val_score(pipeline, X, y, cv=cv, scoring='neg_log_loss')
results['median'] = -np.mean(scores)

欠損をkNNで予測したケース(カテゴリカルカラムは最頻値)

df = pd.read_csv('penguins_size.csv')
# "."は欠損値扱いにする
df.loc[df[df['sex']=='.'].index, 'sex'] = np.nan
target = 'species'
X = df.drop(target, axis=1)
y = df[target]

# CV
k = 5
n_repeats = 3
cv = RepeatedKFold(n_splits=k, n_repeats=n_repeats, random_state=0)

# ダミー変数生成クラスを自作(Pipelineに組み込むため)
class GetDummies(BaseEstimator, TransformerMixin):
    
    def __init__(self):
        self.columns = None

    def fit(self, X, y=None):
        self.columns = pd.get_dummies(X).columns
        return self
    
    def transform(self, X):
        X_new = pd.get_dummies(X)
        return X_new.reindex(columns=self.columns, fill_value=0)

# Columns Transformer (imputer)
# ↓2行の処理はmake_column_selector()でも実行可能
# num_cols = X.select_dtypes(include=np.number).columns.to_list()
# cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
num_pipeline = Pipeline([('scaler', StandardScaler()), ('imputer_num', KNNImputer())])
ct = ColumnTransformer([('imputer_cat', SimpleImputer(strategy='most_frequent'), make_column_selector(dtype_exclude=np.number)),
                        ('scaler+imputer_num', num_pipeline, make_column_selector(dtype_include=np.number))])
ct.set_output(transform='pandas')

# Pipeline (dummy + scaler + model)
pipeline = Pipeline(steps=[('ct', ct),
                ('dummy', GetDummies()),
                ('scaler', StandardScaler()),
                ('model', LogisticRegression())])
scores = cross_val_score(pipeline, X, y, cv=cv, scoring='neg_log_loss')
results['knn'] = -np.mean(scores)

それぞれの結果(低い方が精度がいいことを表している)
-> 今回は単純に欠損値を落とした方が精度がいいが、必ずしもそうなるとは限らないので、データによって、うまくやり方をくみあわせるひつようがある。

{'drop'  : 0.030380026442624677,
 'median': 0.03241178769196222,
 'knn'   : 0.03171966664515755}

▶質的変数(カテゴリカル変数)の処理

  • そのまま扱うことはできないので、エンコードが必要
    • label encoding
    • one-hot encoding(ダミー変数)
    • target encoding
    • embedding encoding
      ...etc

▶Label Encoding

  • カテゴリの値を数字(0, 1, 2...)に変換する方法で、カテゴリカル変数を順序尺度に変換していることになる
    • 順序関係がないカテゴリカル変数に対して使用する際には注意
    • 順序関係があってもサイクル関係にある場合は注意
      例:曜日、月、季節...etc
  • 決定木では順序尺度でなくても分岐を繰り返すことで対応することができる
    例:3以上 -> 4未満という2段階の分岐で3を抽出できる
    ※木が深くなることに注意

PythonでLabel Encoding

  • sklearn.preprocessing.OrdinalEncoderクラスを使用する
    • .fit.transformで変換
    • デフォルトではNumPyArrayが返ってくるので、.set_output(transform="pandas")でDFで処理できるようにする
LabelEncoding
df = pd.read_csv('penguins_size.csv')

# インスタンス生成
oe = OrdinalEncoder()

# DFで出力するように設定
oe.set_output(transform='pandas')
cat_cols = df.select_dtypes(exclude=np.number).columns.to_list()
df[cat_cols] = oe.fit_transform(df[cat_cols])

image.png

▶One-hot Encoding

▶Target Encoding(TS:target statistics)

  • 目的変数の統計量で代用(または新たな特徴量として使用)する方法
  • 目的変数が数値(回帰)の場合も同様に平均を取る

image.png

  • テストデータを含めてEncodingを行うと過学習(target leakage)する可能性があるので、必ずFold作成後に実施をする
    • テストデータに対しては手持ちの学習データのすべての平均、もしくはCVの平均を使用する

image.png

  • 多クラス問題の場合は目的変数(target)をone-hotにしてから平均を取る

image.png

Pythonでtarget encoding

  • category_encoders.TargetEncoder()クラスを使用
    1. インスタンス生成
    2. .fit(X, y)
    3. .transform(X)
      .fitの際にテストデータを含まないように注意

(下記例では、学習データやテストデータは特に気にしていない)

target encoding
# データロード
df = sns.load_dataset('titanic')
df.dropna(inplace=True)

# adult_maleのデータタイプをobjectに変更し,target encodingの対象とする(実際にはaloneも同様に行う
df['adult_male'] = df['adult_male'].astype('object')
df.info()

image.png

encoder = TargetEncoder()
encoder.fit(df, df['survived'])
encoder.transform(df)

image.png

Multi target
# マルチクラスのケース
df = pd.read_csv('penguins_size.csv')

# 1つだけある'.'データをNaNで置換する
df.loc[df[df['sex']=='.'].index[0], 'sex'] = np.nan

# 複数ターゲットなので、one-hotする必要がある
targets = df['species'].unique()
for target in targets:
    target_y = df['species'] == target
    encoder = TargetEncoder()
    df['encoded_island_'+target] = encoder.fit_transform(df['island'], target_y)

image.png

target encodingとone-hot encodingを比較する

[条件]

  • titanicデータセットを使用
  • 5 fold CV × 3
  • 欠損値代入
    • カテゴリカル:最頻値
    • 数値:中央値
  • 標準化
  • モデルは何でもいいが、今回はロジスティック回帰を使用
  • 評価指数も何でもいいが、今回はAccuracyを使用
target encoding vs one-hot encoding
# データ準備
df = sns.load_dataset('titanic')

# targetの'servived'と重複しているので、'alive'を落としておく
df.drop('alive', axis=1, inplace=True)

# adlut_maleとaloneをカテゴリカル変数として扱うための処理を書く
df[['adult_male', 'alone']] = df[['adult_male', 'alone']].astype('object')

# 特徴量と目的変数に分割
X = df.drop('survived', axis=1)
y = df['survived']

# それぞれの結果を格納
scores = {}

# 欠損値代入->カテゴリカル変数のEncoding->標準化->モデル学習
# 処理する対象が違うので,カテゴリカルカラムと数値カラムのリストを取得する
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
num_cols = X.select_dtypes(include=np.number).columns.to_list()

# 欠損値代入
cat_imputer = SimpleImputer(strategy='most_frequent')
num_imputer = SimpleImputer(strategy='median')
ct = ColumnTransformer([('cat_imputer', cat_imputer, cat_cols),
                   ('num_imputer', num_imputer, num_cols)])
ct.set_output(transform='pandas')

# target encoding
pipeline_te = Pipeline([('ct', ct),
          ('encoder', TargetEncoder()),
          ('scaler', StandardScaler()),
          ('model', LogisticRegression())])

# one hot encoding
class GetDummies(BaseEstimator, TransformerMixin):
    def __init__(self):
        self.columns = None
    
    def fit(self, X, y):
        self.columns = pd.get_dummies(X).columns
        return self
        
    def transform(self, X):
        X_new = pd.get_dummies(X)
        return X_new.reindex(columns=self.columns, fill_value=0)
    
pipeline_ohe = Pipeline([('ct', ct),
          ('encoder', GetDummies()),
          ('scaler', StandardScaler()),
          ('model', LogisticRegression())])

cv = RepeatedKFold(n_splits=5, n_repeats=3, random_state=0)
scores['target'] = cross_val_score(pipeline_te, X, y, cv=cv)
scores['onehot'] = cross_val_score(pipeline_ohe, X, y, cv=cv)

"""結果
{'target': array([0.83240223, 0.78089888, 0.82022472, 0.84831461, 0.83146067,
        0.79888268, 0.79775281, 0.89325843, 0.79213483, 0.83707865,
        0.82122905, 0.86516854, 0.79213483, 0.81460674, 0.81460674]),
 'onehot': array([0.83240223, 0.7752809 , 0.81460674, 0.84831461, 0.83146067,
        0.7877095 , 0.81460674, 0.87078652, 0.80337079, 0.81460674,
        0.80446927, 0.86516854, 0.78651685, 0.80898876, 0.8258427 ])}
"""
# 結果をboxplotで描画
sns.boxplot(data=[scores['target'], scores['onehot']])
plt.xticks([0, 1], ['target', 'onehot'])
plt.show()

image.png

# 中央値比較
print(np.median(scores['target']))  # -> 0.8202247191011236
print(np.median(scores['onehot']))  # -> 0.8146067415730337

# 平均値比較
print(np.mean(scores['target']))  # -> 0.8226769610612432
print(np.mean(scores['onehot']))  # -> 0.8189421044922898

★若干だがtarget encodingのほうがAccuracyが高い!

▶Embedding Encoding

  • 自然言語学習による表現でニューラルネットのembedding layerを使って学習する
  • 近いカテゴリ同士を近いものとして扱うことができる
  • one-hot encodingよりも精度が良くなることが多い
  • メモリ節約や高速化が期待できる

image.png

次の記事

Python初心者の備忘録 #24 ~機械学習入門編10~

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?