読者です 読者をやめる 読者になる 読者になる

StatsFragments

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

Python pandas / scikit-learn 向けのちょっとしたパッケージ作った <前編>

pandas Python scikit-learn

こちらの続き。

sinhrks.hatenablog.com

pandas のデータを scikit-learn でうまく処理するためのパッケージを作ったのでその使い方を書きたい。今回は 適当なデータをファイルから読み込み -> 前処理してクラスタリングする、という例を書く。

このパッケージ 基本的には pandas と使い方は同じなので、以下 追加機能にあたる部分を中心に記載する。

  • データは 説明変数 / 目的変数の定義をもち、それぞれプロパティから参照 / 上書きできる。
  • scikit-learn の各サブパッケージにプロパティからアクセスできる。また、メソッド呼び出し時の引数を省略できる。

インストール

pip install pandas_ml

補足 今後 別パッケージへの拡張を考えているため、scikit-learn への依存は optional にしている。そのため、scikit-learn をインストールしていない方は別途インストールを。

データの読み込み

こういう例を書くとき、 iris 以外のデータがぱっと出てくる人ってかっこいいと思う。

# おまじない
import numpy as np
import pandas as pd
pd.options.display.max_rows = 8

state = np.random.RandomState(1)

# csv からデータ読み込み
# http://aima.cs.berkeley.edu/data/iris.csv
names = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species']
iris = pd.read_csv('iris.csv', header=None, names=names)

iris
#      Sepal.Length  Sepal.Width  Petal.Length  Petal.Width    Species
# 0             5.1          3.5           1.4          0.2     setosa
# 1             4.9          3.0           1.4          0.2     setosa
# 2             4.7          3.2           1.3          0.2     setosa
# 3             4.6          3.1           1.5          0.2     setosa
# ..            ...          ...           ...          ...        ...
# 146           6.3          2.5           5.0          1.9  virginica
# 147           6.5          3.0           5.2          2.0  virginica
# 148           6.2          3.4           5.4          2.3  virginica
# 149           5.9          3.0           5.1          1.8  virginica
# 
# [150 rows x 5 columns]

type(iris)
# <class 'pandas.core.frame.DataFrame'>

上で読み込んだデータを pandas_ml で処理するには、一度 pandas_ml.ModelFrame インスタンスに変換する必要がある。ModelFrame を作成するには、

  • 第一引数には、pd.DataFrame に変換できる何か ( DataFrame 自体, 辞書など) を渡す
  • target キーワードには、目的変数として扱いたい 列名の文字列、もしくは pd.Series に変換できる何か ( Series、もしくはリスト-like )を渡す

詳しくは ドキュメント を。

今回は 第一引数には pd.DataFrametarget には 目的変数として扱いたい列名である "Species" を指定する。

import pandas_ml as pdml
df = pdml.ModelFrame(iris, target='Species')

type(df)
# <class 'pandas_ml.core.frame.ModelFrame'>

# ModelFrame は DataFrame のサブクラス
isinstance(df, pd.DataFrame)
# True

補足 target キーワードを指定しない場合、目的変数をもたない ModelFrame インスタンスが作成される。

作成した ModelFramepd.DataFrame と同じように扱える。一部、 pd.DataFrame にないメソッド / プロパティを持つ。

df
#      Sepal.Length  Sepal.Width  Petal.Length  Petal.Width    Species
# 0             5.1          3.5           1.4          0.2     setosa
# 1             4.9          3.0           1.4          0.2     setosa
# 2             4.7          3.2           1.3          0.2     setosa
# 3             4.6          3.1           1.5          0.2     setosa
# ..            ...          ...           ...          ...        ...
# 146           6.3          2.5           5.0          1.9  virginica
# 147           6.5          3.0           5.2          2.0  virginica
# 148           6.2          3.4           5.4          2.3  virginica
# 149           5.9          3.0           5.1          1.8  virginica
# 
# [150 rows x 5 columns]

# データが目的変数を持っているかを確認
df.has_target()
# True

# 説明変数は data プロパティで参照
df.data
#      Sepal.Length  Sepal.Width  Petal.Length  Petal.Width
# 0             5.1          3.5           1.4          0.2
# 1             4.9          3.0           1.4          0.2
# 2             4.7          3.2           1.3          0.2
# 3             4.6          3.1           1.5          0.2
# ..            ...          ...           ...          ...
# 146           6.3          2.5           5.0          1.9
# 147           6.5          3.0           5.2          2.0
# 148           6.2          3.4           5.4          2.3
# 149           5.9          3.0           5.1          1.8
# 
# [150 rows x 4 columns]

# 目的変数は target プロパティで参照
df.target
# 0    setosa
# 1    setosa
# ...
# 148    virginica
# 149    virginica
# Name: Species, Length: 150, dtype: object

# 目的変数の列名を取得
df.target_name
# 'Species'

# これも一緒 (target の name 属性を見ているので)
df.target.name
# 'Species'

このとき、ModelFrame.data は 目的変数のない ModelFrame インスタンスに、また ModelFrame.targetpd.Series を継承した ModelSeries インスタンスになっている。

type(df.data)
# <class 'pandas_ml.core.frame.ModelFrame'>

df.data.has_target()
# False

type(df.target)
# <class 'pandas_ml.core.series.ModelSeries'>

isinstance(df.target, pd.Series)
# True

データの前処理

いくつか前処理を行う。まず、target についているラベル ( "setosa", "versicolor", "virginica" ) を数値 (0, 1, 2) に変換する。この処理は pandasmap がカンタン。

df.target.map({'setosa': 0, 'versicolor': 1, 'virginica': 2})
# 0    0
# 1    0
# ...
# 148    2
# 149    2
# Name: Species, Length: 150, dtype: int64

# これをそのまま target に代入すればよい
df.target = df.target.map({'setosa': 0, 'versicolor': 1, 'virginica': 2})
df
#      Species  Sepal.Length  Sepal.Width  Petal.Length  Petal.Width
# 0          0           5.1          3.5           1.4          0.2
# 1          0           4.9          3.0           1.4          0.2
# 2          0           4.7          3.2           1.3          0.2
# 3          0           4.6          3.1           1.5          0.2
# ..       ...           ...          ...           ...          ...
# 146        2           6.3          2.5           5.0          1.9
# 147        2           6.5          3.0           5.2          2.0
# 148        2           6.2          3.4           5.4          2.3
# 149        2           5.9          3.0           5.1          1.8
# 
# [150 rows x 5 columns]

次に、(特に理由はないが) "Sepal.Length" と "Sepal.Width" のみを正規化する。これは pandas にはない処理なので、 sklearn.preprocessing.normalize を使う。

ModelFramescikit-learn の各サブパッケージに対応したプロパティ (アクセサ) を持っており、サブパッケージをロードしなくても 対応する関数をメソッドとして利用できる。このとき、呼び出し元のデータが自動的に引数として渡されるため データの引数指定は不要。

サブパッケージとプロパティの対応一覧はドキュメント を。

df[['Sepal.Length', 'Sepal.Width']].preprocessing.normalize()
#      Sepal.Length  Sepal.Width
# 0        0.824513     0.565842
# 1        0.852851     0.522154
# 2        0.826599     0.562791
# 3        0.829266     0.558853
# ..            ...          ...
# 146      0.929491     0.368846
# 147      0.907959     0.419058
# 148      0.876812     0.480833
# 149      0.891385     0.453247
# 
# [150 rows x 2 columns]

# これもそのまま代入すればよい
df[['Sepal.Length', 'Sepal.Width']] = df[['Sepal.Length', 'Sepal.Width']].preprocessing.normalize()
df
#      Species  Sepal.Length  Sepal.Width  Petal.Length  Petal.Width
# 0          0      0.824513     0.565842           1.4          0.2
# 1          0      0.852851     0.522154           1.4          0.2
# 2          0      0.826599     0.562791           1.3          0.2
# 3          0      0.829266     0.558853           1.5          0.2
# ..       ...           ...          ...           ...          ...
# 146        2      0.929491     0.368846           5.0          1.9
# 147        2      0.907959     0.419058           5.2          2.0
# 148        2      0.876812     0.480833           5.4          2.3
# 149        2      0.891385     0.453247           5.1          1.8
# 
# [150 rows x 5 columns]

クラスタリング

ModelFrame.cluster.k_means で、sklearn.cluster.k_means を呼び出せる。このメソッドの返り値は centroid, label, inertia の 3つになる。

  • centroid: クラスタの中心点
  • label: 各レコードに振られるラベル
  • inertia: 各レコードともっとも近いクラスタ中心点との距離の二乗和
centroid, label, inertia = df.cluster.k_means(n_clusters=3, random_state=state)

centroid
# array([[ 0.82602947,  0.56251635,  1.464     ,  0.244     ],
#        [ 0.91055414,  0.41171559,  5.59583333,  2.0375    ],
#        [ 0.90545279,  0.42259059,  4.26923077,  1.34230769]])

label
# 0    0
# 1    0
# ...
# 148    1
# 149    1
# Length: 150, dtype: int32

inertia
# 31.598367459843072

もしくは、ModelFrame.cluster.KMeans から sklearn.cluster.KMeans インスタンスを作成して fit -> predict する。メソッドの呼び出しを ModelFrame 側から行うことで引数を省略できる。

kmeans = df.cluster.KMeans(n_clusters=3, random_state=state)
df.fit(kmeans)
# KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters=3, n_init=10,
#     n_jobs=1, precompute_distances=True,
#     random_state=<mtrand.RandomState object at 0x104053410>, tol=0.0001,
#     verbose=0)

df.predict(kmeans)
# 0    1
# 1    1
# ...
# 148    0
# 149    0
# Length: 150, dtype: int32

直前に利用された estimator は ModelFrame.estimator から参照できる。また、直近のクラスタリング結果は ModelFrame.predicted にキャッシュされている。

df.estimator
# KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters=3, n_init=10,
#     n_jobs=1, precompute_distances=True,
#     random_state=<mtrand.RandomState object at 0x104053410>, tol=0.0001,
#     verbose=0)

df.predicted
# 0    1
# 1    1
# ...
# 148    0
# 149    0
# Length: 150, dtype: int32

ModelFrame.metricssklearn.metrics の各メソッドを引数省略して呼び出せる。このとき、内部的には上のキャッシュが利用されている。

df.metrics.completeness_score()
# 0.8643954288752761

引数省略時の内部処理について

scikit-learn の各メソッドと 内部的に渡すデータの対応関係一覧は以下ドキュメントに記載している。何かおかしなことをやっていたら GitHub からご指摘ください。

まとめ

pandas_ml を利用して データの読み込み / 前処理 / クラスタリングを行う方法を記載した。

次は、Pipeline, Cross Validation, Grid Search あたりを。

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

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

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