scikit learnで特徴量生成に役立ちそうな処理

概要

scipyとかscikit-learnとかに機能があるのに気づかずに独自実装して無駄に時間を使ってたみたいなことをしなくて済むように、整形したデータを分類器とかに回す前段階でやる処理でお手頃そうなものをまとめておく。

LabelEncoder

文字列をIDに変換したい場合に利用できる。

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(['tokyo', 'osaka', 'nagoya', 'tokyo', 'yokohama', 'osaka'])

le.classes_
  #=> array(['nagoya', 'osaka', 'tokyo', 'yokohama'], dtype='<U8')

le.transform(['tokyo', 'osaka', 'nagoya', 'tokyo', 'yokohama', 'osaka'])
  #=> array([2, 1, 0, 2, 3, 1])

le.inverse_transform([2, 1, 0, 2, 3, 1])
  #=> array(['tokyo', 'osaka', 'nagoya', 'tokyo', 'yokohama', 'osaka'], dtype='<U8')

生成したLabelEncoderを一旦保存して他でも使いまわしたい場合は、StackOverFlowに載ってるように一部を保存しておくか、下記のように単純にpickleで取っておくか。

pickle.dump(le, 'foo.txt')
with open('foo.p', 'wb') as f:
  pickle.dump(le, f)
with open('foo.p', 'rb') as f:
  le2 = pickle.load(f)

le2.inverse_transform([2, 1, 0, 2, 3, 1])
  #=> array(['tokyo', 'osaka', 'nagoya', 'tokyo', 'yokohama', 'osaka'], dtype='<U8')

LabelBinarizer

LabelEncoderでは文字列を数値に直したけど、こうした値はたいてい1つの要素としてではなく複数の要素として扱われる。

例えば[tokyo, osaka, nagoya]という3つの値が入っていた場合、[1, 2, 3]ではなく[[1, 0, 0], [0, 1, 0]. [0, 0, 1]]になって欲しい。

下記、イメージ。

# こうなって欲しいのではなく。
pd.DataFrame([{'prefecture': 1}, {'prefecture': 2}, {'prefecture': 3}])
  #=> prefecture
  #=> 0 1
  #=> 1 2
  #=> 2 3

# こうなって欲しい。
 pd.DataFrame([
  {'tokyo': 1, 'osaka': 0, 'nagoya': 0},
  {'tokyo': 0, 'osaka': 1, 'nagoya': 0},
  {'tokyo': 0, 'osaka': 0, 'nagoya': 1}])
  #=> nagoya osaka tokyo
  #=> 0 0 0 1
  #=> 1 0 1 0
  #=> 2 1 0 0

こうした変換はLabelBinarizerを使うと可能。

from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()

lb.fit(['tokyo', 'osaka', 'nagoya'])
lb.transform(['tokyo', 'nagoya', 'osaka'])
  #=> array([[0, 0, 1],
  #=> [1, 0, 0],
  #=> [0, 1, 0]])

書いてる途中で気づいたけど、nagoyaはprefectureではなかった。まあいいや、1871年のデータですとか書いとけば。

pandasのDataFrameのカラムに対して実行して、結果を同じDataFrameに入れたい場合。

df = pd.DataFrame([{'prefecture': 'tokyo'}, {'prefecture': 'osaka'}, {'prefecture': 'nagoya'}, {'prefecture': 'tokyo'}])
lb.fit(df.prefecture)
pd.concat([df, pd.DataFrame(lb.transform(df.prefecture), columns=lb.classes_)], axis=1)
  #=> prefecture nagoya osaka tokyo
  #=> 0 tokyo 0 0 1
  #=> 1 osaka 0 1 0
  #=> 2 nagoya 1 0 0
  #=> 3 tokyo 0 0 1

上記では3つしかカラムがないのでdenseなDataFrameでも問題ないけど、これが値が増えてくるとsparseにしないと持たなくなる。

LabelBinarizerの初期化時にsparse_outputを設定しておくと、出力結果がsparseになる。

from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer(sparse_output=True)

lb.fit(['tokyo', 'osaka', 'nagoya'])
lb.transform(['tokyo', 'nagoya', 'osaka'])
  #=> <3x3 sparse matrix of type '' with 3 stored elements in Compressed Sparse Row format>

MultiLabelBinarizer

LabelBinarizerだと1つのカラムに対して1つしか1が立たない。

例えば映画のジャンルを利用したい場合に、[action, horror]とか[romance, commedy]のように複数のジャンルが紐づく場合がある。

そんな時はMultiLabelBinarizerで配列を渡すと、良い感じに変換してくれる。

from sklearn.preprocessing import MultiLabelBinarizer
mlb = MultiLabelBinarizer()
mlb.fit_transform([['action', 'adventure'], ['action'], ['action', 'war', 'commedy'], []])
  #=> array([[1, 1, 0, 0],
  #=>        [1, 0, 0, 0],
  #=>        [1, 0, 1, 1],
  #=>        [0, 0, 0, 0]])