StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas のデータを scikit-learn でうまいこと処理したい

はじめに

Python機械学習する場合、pandas で前処理したデータを scikit-learn で処理する、というケースが多いと思う。pandas, scikit-learn には それぞれ 簡単にできる / できない処理があるので、うまいこと連携できるとうれしい。

scikit-learn の各メソッドnumpy.ndarray に対する処理を前提にしているため、pandasデータ形式 (DataFrameSeries) を渡すと 内部で ndarray に変換して結果を返してくる。そのため、結果に対して 直接 pandas の処理を続けることはできない。

ndarray で処理すりゃいいじゃん、、というむきもあるが、自分はどうしても pandas で処理がしたいんだ。とりあえず、pandas のデータをできるだけ維持したい、というモチベーションがあるものとして処理のサンプルを書く。上述のとおり scikit-learn で何か処理をするたびに ndarray が返ってくるので、これを 都度 pandasデータ形式に戻してやる必要がある。

pandas / scikit-learn 標準での処理

このサンプルで行う処理の流れは、

  • データの準備: sklearn.datasets のデータセットを読み込み
  • 前処理: sklearn.preprocessing で説明変数を二値化し、sklearn.cross_validation でデータを訓練用 / テスト用に分割
  • 分類 / 評価: sklearn.svm で 分類し、sklearn.metrics で評価
# おまじない
import numpy as np
import pandas as pd
pd.options.display.max_rows = 10
pd.options.display.max_columns = 15
state = np.random.RandomState(1)

データの準備

scikit-learn 付属の手書き数字のデータセットdatasets.load_digits で読み込む。

import sklearn.datasets as datasets
digits = datasets.load_digits()
type(digits)
# <class 'sklearn.datasets.base.Bunch'>

# 説明変数
digits.data.shape
# (1797, 64)

# 目的変数
digits.target.shape
# (1797,)

# 目的変数を Series に変換
target = pd.Series(digits.target)
target
# 0    0
# 1    1
# 2    2
# ...
# 1794    8
# 1795    9
# 1796    8
# Length: 1797, dtype: int64

# 説明変数を DataFrame に変換
data = pd.DataFrame(digits.data)
data
#       0   1   2   3   4   5   6  ...  57  58  59  60  61  62  63
# 0      0   0   5  13   9   1   0 ...   0   6  13  10   0   0   0
# 1      0   0   0  12  13   5   0 ...   0   0  11  16  10   0   0
# 2      0   0   0   4  15  12   0 ...   0   0   3  11  16   9   0
# 3      0   0   7  15  13   1   0 ...   0   7  13  13   9   0   0
# 4      0   0   0   1  11   0   0 ...   0   0   2  16   4   0   0
# ...   ..  ..  ..  ..  ..  ..  .. ...  ..  ..  ..  ..  ..  ..  ..
# 1792   0   0   4  10  13   6   0 ...   0   2  14  15   9   0   0
# 1793   0   0   6  16  13  11   1 ...   0   6  16  14   6   0   0
# 1794   0   0   1  11  15   1   0 ...   0   2   9  13   6   0   0
# 1795   0   0   2  10   7   0   0 ...   0   5  12  16  12   0   0
# 1796   0   0  10  14   8   1   0 ...   1   8  12  14  12   1   0
# 
# [1797 rows x 64 columns]

前処理

まずは 説明変数を preprocessing.binarize で二値化して

import sklearn.preprocessing as pp
data = pd.DataFrame(pp.binarize(data.values, threshold=5))
data
#       0   1   2   3   4   5   6  ...  57  58  59  60  61  62  63
# 0      0   0   0   1   1   0   0 ...   0   1   1   1   0   0   0
# 1      0   0   0   1   1   0   0 ...   0   0   1   1   1   0   0
# 2      0   0   0   0   1   1   0 ...   0   0   0   1   1   1   0
# 3      0   0   1   1   1   0   0 ...   0   1   1   1   1   0   0
# 4      0   0   0   0   1   0   0 ...   0   0   0   1   0   0   0
# ...   ..  ..  ..  ..  ..  ..  .. ...  ..  ..  ..  ..  ..  ..  ..
# 1792   0   0   0   1   1   1   0 ...   0   0   1   1   1   0   0
# 1793   0   0   1   1   1   1   0 ...   0   1   1   1   1   0   0
# 1794   0   0   0   1   1   0   0 ...   0   0   1   1   1   0   0
# 1795   0   0   0   1   1   0   0 ...   0   0   1   1   1   0   0
# 1796   0   0   1   1   1   0   0 ...   0   1   1   1   1   0   0
# 
# [1797 rows x 64 columns]

cross_validation.train_test_split で 訓練用とテスト用のデータに分割する。

import sklearn.cross_validation as crv
train_data, test_data, train_target, test_target = crv.train_test_split(data.values, target.values, random_state=state)
 
train_data = pd.DataFrame(train_data)
test_data = pd.DataFrame(test_data)

train_target = pd.Series(train_target)
test_target = pd.Series(test_target)
train_data
#       0   1   2   3   4   5   6  ...  57  58  59  60  61  62  63
# 0      0   0   1   1   1   0   0 ...   0   1   1   1   0   0   0
# 1      0   0   0   1   1   0   0 ...   0   0   1   1   1   1   0
# 2      0   0   0   0   1   0   0 ...   0   0   0   1   1   0   0
# 3      0   0   0   1   1   1   1 ...   0   0   1   0   0   0   0
# 4      0   0   1   1   1   0   0 ...   0   0   1   1   1   0   0
# ...   ..  ..  ..  ..  ..  ..  .. ...  ..  ..  ..  ..  ..  ..  ..
# 1342   0   0   0   1   1   0   0 ...   0   0   1   1   1   0   0
# 1343   0   0   0   0   1   0   0 ...   0   0   0   1   0   0   0
# 1344   0   0   1   1   1   0   0 ...   0   1   1   1   1   0   0
# 1345   0   0   0   0   1   1   1 ...   0   0   0   1   1   0   0
# 1346   0   0   1   1   1   1   1 ...   0   1   1   1   0   0   0
# 
# [1347 rows x 64 columns]

test_data
#      0   1   2   3   4   5   6  ...  57  58  59  60  61  62  63
# 0     0   0   1   1   1   0   0 ...   0   0   1   1   0   0   0
# 1     0   1   1   1   1   1   1 ...   1   1   1   0   0   0   0
# 2     0   0   0   1   1   0   0 ...   0   0   1   1   1   0   0
# 3     0   0   0   1   1   1   1 ...   0   0   1   0   0   0   0
# 4     0   0   0   0   1   1   0 ...   0   0   0   1   1   0   0
# ..   ..  ..  ..  ..  ..  ..  .. ...  ..  ..  ..  ..  ..  ..  ..
# 445   0   0   1   1   1   0   0 ...   0   1   1   1   1   0   0
# 446   0   0   1   1   1   0   0 ...   0   1   1   1   1   0   0
# 447   0   0   1   1   1   0   0 ...   0   1   1   0   0   0   0
# 448   0   0   1   1   1   0   0 ...   0   1   1   1   0   0   0
# 449   0   0   1   1   1   0   0 ...   0   1   1   1   0   0   0
# 
# [450 rows x 64 columns]

分類 / 評価

svm.LinearSVC で分類する。

import sklearn.svm as svm
svc = svm.LinearSVC(random_state=state)

# 訓練データをフィット
svc.fit(train_data, train_target)
# LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
#      intercept_scaling=1, loss='l2', multi_class='ovr', penalty='l2',
#      random_state=<mtrand.RandomState object at 0x103f28350>, tol=0.0001,
#      verbose=0)

# テストデータを分類
predicted = svc.predict(test_data)
predicted = pd.Series(predicted)
predicted
# 0    1
# 1    5
# 2    0
# ...
# 447    7
# 448    8
# 449    5
# Length: 450, dtype: int64

最後に、得られた結果を metrics で評価する。

import sklearn.metrics as metrics
metrics.confusion_matrix(test_target, predicted)
# [[51  0  0  0  1  0  0  0  1  0]
#  [ 0 38  0  0  0  1  0  0  3  0]
#  [ 0  0 38  1  0  0  1  1  0  0]
#  [ 0  1  1 45  0  2  1  1  1  0]
#  [ 0  0  0  0 45  0  1  1  0  0]
#  [ 0  0  2  0  0 36  0  0  0  1]
#  [ 0  0  0  0  0  0 43  0  0  0]
#  [ 0  0  0  0  1  0  0 47  0  0]
#  [ 0  2  1  1  1  0  0  0 31  1]
#  [ 0  2  0  1  1  3  0  1  1 39]]

metrics.accuracy_score(test_target, predicted)
# 0.917777777778

うん、、、めんどくさい。この めんどくささは 以下 3 点に起因していると思う。

  • scikit-learn 返り値の ndarray を 都度 pandasデータ形式に変換する必要がある ( このとき、データの属性、たとえばカラム名を維持したい場合は毎回 columns を引数として渡す必要がある )
  • scikit-learn の個々のモジュールを都度 ロードする必要がある (これは pandas 云々は関係ないが)
  • scikit-learn にいちいち似たような引数を渡さなければならない ( 目的変数が変わることはないはずなので、一度 定義したものが使いまわせるとうれしい )

というわけで

作りました。

github.com

ドキュメントはこちら。

こちらを使うと 上のサンプルは以下のように書ける。

state = np.random.RandomState(1)

import pandas_ml as pdml
import sklearn.datasets as datasets

# データの読み込み / データ型の作成 (ModelFrame は DataFrame を継承したクラス)
df = pdml.ModelFrame(datasets.load_digits()) 

# 説明変数
df.data
#       0   1   2   3   4   5   6  ...  57  58  59  60  61  62  63
# 0      0   0   5  13   9   1   0 ...   0   6  13  10   0   0   0
# 1      0   0   0  12  13   5   0 ...   0   0  11  16  10   0   0
# 2      0   0   0   4  15  12   0 ...   0   0   3  11  16   9   0
# 3      0   0   7  15  13   1   0 ...   0   7  13  13   9   0   0
# 4      0   0   0   1  11   0   0 ...   0   0   2  16   4   0   0
# ...   ..  ..  ..  ..  ..  ..  .. ...  ..  ..  ..  ..  ..  ..  ..
# 1792   0   0   4  10  13   6   0 ...   0   2  14  15   9   0   0
# 1793   0   0   6  16  13  11   1 ...   0   6  16  14   6   0   0
# 1794   0   0   1  11  15   1   0 ...   0   2   9  13   6   0   0
# 1795   0   0   2  10   7   0   0 ...   0   5  12  16  12   0   0
# 1796   0   0  10  14   8   1   0 ...   1   8  12  14  12   1   0
# 
# [1797 rows x 64 columns]

# 目的変数
df.target
# 0    0
# 1    1
# 2    2
# ...
# 1794    8
# 1795    9
# 1796    8
# Name: .target, Length: 1797, dtype: int64

# 二値化
df.data = df.data.preprocessing.binarize(threshold=5)

# 訓練用とテスト用のデータに分割
train_df, test_df = df.cross_validation.train_test_split(random_state=state)
train_df
#       .target  0  1  2  3  4  5 ...  57  58  59  60  61  62  63
# 0           2  0  0  1  1  1  0 ...   0   1   1   1   0   0   0
# 1           6  0  0  0  1  1  0 ...   0   0   1   1   1   1   0
# 2           6  0  0  0  0  1  0 ...   0   0   0   1   1   0   0
# 3           7  0  0  0  1  1  1 ...   0   0   1   0   0   0   0
# 4           1  0  0  1  1  1  0 ...   0   0   1   1   1   0   0
# ...       ... .. .. .. .. .. .. ...  ..  ..  ..  ..  ..  ..  ..
# 1342        8  0  0  0  1  1  0 ...   0   0   1   1   1   0   0
# 1343        4  0  0  0  0  1  0 ...   0   0   0   1   0   0   0
# 1344        9  0  0  1  1  1  0 ...   0   1   1   1   1   0   0
# 1345        1  0  0  0  0  1  1 ...   0   0   0   1   1   0   0
# 1346        5  0  0  1  1  1  1 ...   0   1   1   1   0   0   0
# 
# [1347 rows x 65 columns]

# 訓練データをフィット
svc = train_df.svm.LinearSVC(random_state=state)
train_df.fit(svc)
# LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
#      intercept_scaling=1, loss='l2', multi_class='ovr', penalty='l2',
#      random_state=<mtrand.RandomState object at 0x103f3e350>, tol=0.0001,
#      verbose=0)

# テストデータを分類
predicted = test_df.predict(svc)
predicted
# 0    1
# 1    5
# 2    0
# ...
# 447    7
# 448    8
# 449    5
# Length: 450, dtype: int64

# 評価
test_df.metrics.confusion_matrix()
# Predicted   0   1   2   3   4   5   6   7   8   9
# Target                                           
# 0          51   0   0   0   1   0   0   0   1   0
# 1           0  38   0   0   0   1   0   0   3   0
# 2           0   0  38   1   0   0   1   1   0   0
# 3           0   1   1  45   0   2   1   1   1   0
# 4           0   0   0   0  45   0   1   1   0   0
# 5           0   0   2   0   0  36   0   0   0   1
# 6           0   0   0   0   0   0  43   0   0   0
# 7           0   0   0   0   1   0   0  47   0   0
# 8           0   2   1   1   1   0   0   0  31   1
# 9           0   2   0   1   1   3   0   1   1  39

test_df.metrics.accuracy_score()
# 0.9177777777777778

先に記載の課題は以下のように改善される。

  • scikit-learn の処理をデータに対して適用することで、pandasデータ形式 (を継承したデータ構造) を維持できる
  • データのプロパティから scikit-learn の各モジュールへアクセスできる。モジュールは必要になったときにロードされる
  • メソッド呼び出しの際、目的変数 / 説明変数を省略できる

もう少しまともな解説を別の記事で書きます。

3/19追記 続きはこちら。

sinhrks.hatenablog.com

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

9/12追記 パッケージ名変更。