前処理 - redultimate/utility GitHub Wiki

欠損値

  • 何もしない.
    • GBDTなど, 大小関係のみが重要な場合.
  • 変な値で置き換える.
    • 正の場合は-1を入れたり, とても大きい値を入れたりする.
  • 代表値で置き換える.
    • 平均値など. データ数が少ない場合には, Bayesian averageという方法がある.
  • 他の変数から予測する.
    • 欠損値予測モデルを前段階として挟む. 過去のコンペでこれが効いた例があったらしい.
  • 欠損しているかどうかのフラグや欠損数などを新たに特徴量として足す.

数値データ

  • GBDTでは, 大小関係が保存される変換はあまりモデル向上にはつながらない.

規格化

  • standardization
    • trainとtestは必ず同じ変換をする. 変換パラメータをtrainから得るか, train+testから得るかは状況次第.
    • こちらより.
from sklearn.preprocessing import StandardScaler

# 学習データに基づいて複数列の標準化を定義
scaler = StandardScaler()
scaler.fit(train_x[num_cols])

# 変換後のデータで各列を置換
train_x[num_cols] = scaler.transform(train_x[num_cols])
test_x[num_cols] = scaler.transform(test_x[num_cols])

# または

# 学習データとテストデータを結合したものに基づいて複数列の標準化を定義
scaler = StandardScaler()
scaler.fit(pd.concat([train_x[num_cols], test_x[num_cols]]))

# 変換後のデータで各列を置換
train_x[num_cols] = scaler.transform(train_x[num_cols])
test_x[num_cols] = scaler.transform(test_x[num_cols])
  • Min-Max scaling
    • standardizationが普通, 最大最小が決まっている場合にはこちらがいい.
    • こちらより.
from sklearn.preprocessing import MinMaxScaler

# 学習データに基づいて複数列のMin-Maxスケーリングを定義
scaler = MinMaxScaler()
scaler.fit(train_x[num_cols])

# 変換後のデータで各列を置換
train_x[num_cols] = scaler.transform(train_x[num_cols])
test_x[num_cols] = scaler.transform(test_x[num_cols])

非線形変換

  • 対数
np.log(x)
  • log(x+1)
    • 0が含まれる場合.
np.log1p(x)
  • 絶対値の対数
    • 負の値が含まれる場合.
np.log(np.abs(x)) * np.sign(x)
  • Box-Cox変換
    • テイルの長い分布を正規分布に変換する.
    • 正の値のみ?
    • こちらより.
  • Yeo-Johnson変換
    • テイルの長い分布を正規分布に変換する.
    • こちらより.
from sklearn.preprocessing import PowerTransformer

# 学習データに基づいて複数列のYeo-Johnson変換を定義
pt = PowerTransformer(method='yeo-johnson')
pt.fit(train_x[num_cols])

# 変換後のデータで各列を置換
train_x[num_cols] = pt.transform(train_x[num_cols])
test_x[num_cols] = pt.transform(test_x[num_cols])
  • generalized log transformation
  • 絶対値
np.abs(x)
  • 平方根
  • n乗
  • binary化
  • round

Clipping

  • 上限と下限を設定する.
  • こちらより.
# 列ごとに学習データの1%点、99%点を計算
p01 = train_x[num_cols].quantile(0.01)
p99 = train_x[num_cols].quantile(0.99)

# 1%点以下の値は1%点に、99%点以上の値は99%点にclippingする
train_x[num_cols] = train_x[num_cols].clip(p01, p99, axis=1)
test_x[num_cols] = test_x[num_cols].clip(p01, p99, axis=1)

Binning

  • 要はヒストグラム化. カテゴリ変数として扱えるようになる.
  • こちらより.
# binの数を指定する場合
binned = pd.cut(x, 3, labels=False)
print(binned)
# [0 2 1 1 2 0] - 変換された値は3つのbinのどれに入ったかを表す

# binの範囲を指定する場合(3.0以下、3.0より大きく5.0以下、5.0より大きい)
bin_edges = [-float('inf'), 3.0, 5.0, float('inf')]
binned = pd.cut(x, bin_edges, labels=False)
print(binned)
# [0 2 1 1 2 0] - 変換された値は3つのbinのどれに入ったかを表す

Rank

  • 大小関係を保持して順位付けする. その上で最大最小を固定(規格化).
  • こちらより.
x = [10, 20, 30, 0, 40, 40]

# pandasのrank関数で順位に変換する
rank = pd.Series(x).rank()
print(rank.values)
# はじまりが1、同順位があった場合は平均の順位となる
# [2. 3. 4. 1. 5.5 5.5]

# numpyのargsort関数を2回適用する方法で順位に変換する
order = np.argsort(x)
rank = np.argsort(order)
print(rank)
# はじまりが0、同順位があった場合はどちらかが上位となる
# [1 2 3 0 4 5]

RankGaus

  • さらに規格化の際にGaussianにした場合.
  • こちらより.
from sklearn.preprocessing import QuantileTransformer

# 学習データに基づいて複数列のRankGaussによる変換を定義
transformer = QuantileTransformer(n_quantiles=100, random_state=0, output_distribution='normal')
transformer.fit(train_x[num_cols])

# 変換後のデータで各列を置換
train_x[num_cols] = transformer.transform(train_x[num_cols])
test_x[num_cols] = transformer.transform(test_x[num_cols])

カテゴリデータ

one-hot encoding

  • 各カテゴリについて0, 1に変換する.
  • pandasを使う場合 (こちらより)
# 学習データとテストデータを結合してget_dummiesによるone-hot encodingを行う
all_x = pd.concat([train_x, test_x])
all_x = pd.get_dummies(all_x, columns=cat_cols)

# 学習データとテストデータに再分割
train_x = all_x.iloc[:train_x.shape[0], :].reset_index(drop=True)
test_x = all_x.iloc[train_x.shape[0]:, :].reset_index(drop=True)
  • scikit-learnを使う場合 (こちらより)
from sklearn.preprocessing import OneHotEncoder

# OneHotEncoderでのencoding
ohe = OneHotEncoder(sparse=False, categories='auto')
ohe.fit(train_x[cat_cols])

# ダミー変数の列名の作成
columns = []
for i, c in enumerate(cat_cols):
    columns += [f'{c}_{v}' for v in ohe.categories_[i]]

# 生成されたダミー変数をデータフレームに変換
dummy_vals_train = pd.DataFrame(ohe.transform(train_x[cat_cols]), columns=columns)
dummy_vals_test = pd.DataFrame(ohe.transform(test_x[cat_cols]), columns=columns)

# 残りの変数と結合
train_x = pd.concat([train_x.drop(cat_cols, axis=1), dummy_vals_train], axis=1)
test_x = pd.concat([test_x.drop(cat_cols, axis=1), dummy_vals_test], axis=1)

label encoding

  • カテゴリをそれぞれ数字にする.
  • 大小に意味はないので, 決定木ではないモデルではちゃんと扱わないといけない.
  • scikit-learnを使った実装 (こちらより)
from sklearn.preprocessing import LabelEncoder

# カテゴリ変数をループしてlabel encoding
for c in cat_cols:
    # 学習データに基づいて定義する
    le = LabelEncoder()
    le.fit(train_x[c])
    train_x[c] = le.transform(train_x[c])
    test_x[c] = le.transform(test_x[c])

feature hashing

  • ハッシュ関数を用いて少ないカテゴリで表せるよう変換する.

frequency encoding

  • 出現頻度で置き換える.

target encoding

  • 目的変数を用いて数値に変換する.
  • 上級者向け.
  • 自己相関係数に通じるものがある.
  • 各カテゴリにおける目的変数の平均で置換する.
  • 時系列で使う時は要注意. 「頻度情報」を使っているので.
  • 実装でもリークをしないようにしないといけない.
    • ある学習データのtarget encodingの値はそれ以外の学習データの目的変数の平均. (fold数は4から10くらいがいい)
    • テストデータのtarget encodingは学習データ全体の目的変数の平均.
    • さらにcross validationを行う場合にはそれ用のデータは別に避けておく必要がある.
  • 実装例はこちらより.
from sklearn.model_selection import KFold

# クロスバリデーションのfoldごとにtarget encodingをやり直す
kf = KFold(n_splits=4, shuffle=True, random_state=71)
for i, (tr_idx, va_idx) in enumerate(kf.split(train_x)):

    # 学習データからバリデーションデータを分ける
    tr_x, va_x = train_x.iloc[tr_idx].copy(), train_x.iloc[va_idx].copy()
    tr_y, va_y = train_y.iloc[tr_idx], train_y.iloc[va_idx]

    # 変数をループしてtarget encoding
    for c in cat_cols:
        # 学習データ全体で各カテゴリにおけるtargetの平均を計算
        data_tmp = pd.DataFrame({c: tr_x[c], 'target': tr_y})
        target_mean = data_tmp.groupby(c)['target'].mean()
        # バリデーションデータのカテゴリを置換
        va_x.loc[:, c] = va_x[c].map(target_mean)

        # 学習データの変換後の値を格納する配列を準備
        tmp = np.repeat(np.nan, tr_x.shape[0])
        kf_encoding = KFold(n_splits=4, shuffle=True, random_state=72)
        for idx_1, idx_2 in kf_encoding.split(tr_x):
            # out-of-foldで各カテゴリにおける目的変数の平均を計算
            target_mean = data_tmp.iloc[idx_1].groupby(c)['target'].mean()
            # 変換後の値を一時配列に格納
            tmp[idx_2] = tr_x[c].iloc[idx_2].map(target_mean)

        tr_x.loc[:, c] = tmp

    # 必要に応じてencodeされた特徴量を保存し、あとで読み込めるようにしておく

テキストデータ

embedding

  • 単語やカテゴリ変数を実数ベクトルに変換する.
  • Word2Vec
  • GloVe
  • fastText

bag-of-words

n-gram

tf-idf

画像データ

日付・時刻データ

  • カテゴリ変数化する.
    • 曜日, 休日・祝日, 「特別な日」など.
  • 期日がある場合には, 0から1に変換する.
  • 通し番号にする.
  • 時間差にする.

時系列データ

ワイド-ロングフォーマット変換

  • ワイドtoロング: DataFrameのstack
  • ロングtoワイド: DataFrameのpivot
  • こちらが参考になる.

MultiIndexの扱い

  • 行: DataFramemのreset_index
  • 列: MultiIndexのto_flat_index
  • こちらが参考になる.

ラグ特徴量

  • 目的変数のラグ特徴量.
    • ただし, test dataが時間的に分割されている場合には, test dataの後の期間ほど直前の目的変数などの情報は使えない. その場合は期間別モデルを作るなどして対応する.
x_lag1 = x.shift(1)
x_avg = x.shift(1).rolling(window=3).mean()

累積和

  • 過去の一定期間の何かしらの累積量.
  • 累積和についてはここなど.

変数の組み合わせ

  • 変数間で「意味のある」計算をして別の変数を作り出す.
  • GBDT的には乗除の特徴量は捉えづらいので, 加えるなら乗除.
  • カテゴリ変数同士を掛け合わせてより多くのカテゴリを作る.
    • この際にtarget encodingを用いると有効である場合が多い. より細かい分類ベースの目的変数の平均が得られるから.

他のテーブルの結合

  • 特に, 1対1対応していない場合.

集計

  • 行ごとに集計した統計量を加える.
  • 列ごとに集計した統計量を加える.
  • カウント, ユニーク数.
  • 何かのbool
  • 合計, 平均, 割合, 最大, 最小, 標準偏差, 中央値, 分位点, 尖度, 歪度.
  • 直近の情報.
  • 間隔, 頻度.
  • Aの次はB, AとCは同時などの情報.

その他のテクニック(未分類)

  • 教師なし学習を利用する.
  • バーコード情報の復元.
  • データを圧縮してその圧縮率を特徴にする.
  • フィッティングしてその差分.
  • Ridge回帰でのトレンド推定.
  • 過去の戦績からレーティングを計算 (自己相関係数のアイディアに近い?).
  • 同じ値が存在するかどうか.
  • ひたすらそのデータに関係する登場人物の気持ちになる(当たり前). 本当は実際に潜って体験のが一番いい. でも現実的ではないのでできる限り背景を調べて想像する.
  • 科学を使う (因果関係から特徴量を推測する).
  • 相対値に注目する.
  • 位置情報に注目する.
  • 自然言語処理の方法を, それ以外のデータにも適用してみる.
  • 画像特徴量を使う.
    • NNの出力層近くの値.
    • SIFT, EXIFタグ
  • decision tree feature transformation
  • 匿名化されたデータの変換前の値を推測する.
    • 自分だったらどうやって匿名化するか?
    • 例えばここなど.
  • データの誤りの修正.

参考文献