pimientitoの機械学習

「機械学習って何だろう。」から、はじまり、いまだ???の毎日。数学初心者、PG・DBアマチュアのサービス・エンジニアが、どこまで理解できるのか。

【前処理の学習-35】データを学ぶ ~生成~③

 前回は、オーバーサンプリングの一種「ランダム・オーバーサンプリング」について学びました。

pimientito-handson-ml.hatenablog.com

 今回も引き続き「オーバーサンプリング」について学びます。なお、記事の導入部分は、前回の【前処理の学習-34】と一部重複するところがあります。


【今回の目標到達点】

 SMOTEによるデータ生成を学ぶ

【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「生成」の概要

 参考資料「前処理大全」第6章「生成」の冒頭で、著者はデータの生成について、以下のように述べています。

十分にデータがある場合には、データの生成が必要となることはまずありません。データを無理やり増やしても、データの価値は増やす前のデータが持っている価値とほとんど変わらないからです。しかし、それでもデータ生成が必要となるケースがあります。それは不均衡データを調整するときです。

参考・参照元:第6章「生成」(p.146)より抜粋


 著者は、同じく本章冒頭で、不均衡データ(偏りの大きいデータ)を、予測モデルの学習に使用すると予測精度が低下すると述べています。


 偏りを調整する主な方法として、参考資料では、以下の二点を挙げています。


  • データに重み付けして偏りを調整する

  • データを操作して偏りを整える



 一点目の「データに重み付けして偏りを調整する」方法は、学習モデルによって異なるようです。学習モデルの作成は未学習のため、今回の学習では割愛しています。


 二点目の「データを操作して偏りを整える」方法は、以下の通りです。


  • 件数の多いデータを減らす方法「アンダーサンプリング」

  • 件数の少ないデータを増やす方法「オーバーサンプリング」

  • 「アンダーサンプリング」と「オーバーサンプリング」を組み合わせる方法



今回のテーマ

 件数の少ないデータを増やす方法「オーバーサンプリング」


概要

 オーバーサンプリングは、不均衡データの中で、出現率が低いデータを増やす方法です。オーバーサンプリングの概要図は、以下の通りです。

f:id:Pimientito:20190825161505j:plain
オーバーサンプリング概要図


 参考資料 6-2「オーバーサンプリングによる不均衡データの調整」では、オリジナルデータを元に新しいデータを生成するSMOTEを紹介しています。


 SMOTEは、imbalanced-learn APIのOver-sampling methodsのひとつです。imbalanced-learn公式ドキュメント(以後、"公式ドキュメント"と表記)では、SMOTEの他にもオーバーサンプリングを実現する方法を紹介しています。


 ここから少しの間、参考資料「前処理大全」から離れ、公式ドキュメントの「2. Over-sampling」を元に、オーバーサンプリングについて学習を進めていきます。


オーバーサンプリングの種類

 公式ドキュメント「2. Over-sampling」で紹介されている方法は、以下の通りです。

  • Naive random over-sampling

  • the Synthetic Minority Oversampling Technique(SMOTE)

  • the Adaptive Synthetic(ADASYN)


今回の学習

 前回【前処理の学習-34】同様、公式ドキュメントのサンプルコードを参考にオーバーサンプリングを学びます。


サンプルデータセットの作成

 はじめにscikit-learnパッケージのmake_classification関数を使って不均衡なサンプルデータセットを作成します。各種パラメータ(一部)は、以下の通りです。

パラメータ名 概要
n_samples int 生成サンプルの個数
n_features int 特徴量の数
n_informative int 目的変数のラベルと相関が強い特徴量の数
特徴量名称:Infomative feature
n_redundant int Informative featureの線形結合から生成される特徴量の数
特徴量名称:Redundant feature
n_repeated int Informative, Redundant featureのコピーからなる特徴量の数
特徴量名称:Repeated feature
n_classes int 分類するクラス数
2を指定した場合:2値分類問題
3以上を指定した場合:多値分類問題
n_clusters_per_class int 1クラスあたりのクラスタ
weights list(float) クラスの比率。
不均衡データ生成時に使用
class_sep float 生成アルゴリズムに関係するパラメータ
random_state int 乱数パラメータ

参考・参照元:Hatena Blog「データ分析がしたい」~ scikit-learnを用いたサンプルデータ生成 ~(Overlap氏著) を参考に作成


 make_classification関数のパラメータについては、Overlap氏のブログ記事「データ分析がしたい」を参考にさせていただきました。(記事の詳細については、記事末尾【参考資料】をご覧ください)


 サンプルデータセットを作成するコードは、以下の通りです。

# 参考・参照元:imbalanced-learn Documentation 「2. Over-sampling」
# https://imbalanced-learn.readthedocs.io/en/stable/over_sampling.html
# 「2.1.1. Naive random over-sampling」を参考に作成

# サンプルデータセットの作成

# scikit-learn パッケージ
from sklearn.datasets import make_classification

X, y = make_classification(
    n_samples=10,          # 生成データ(サンプル)数:10件
    n_features=2,
    n_informative=2,
    n_redundant=0, 
    n_repeated=0, 
    n_classes=2,
    n_clusters_per_class=1, 
    weights=[0.20, 0.80],  # 2対8の割合
    class_sep=0.8, 
    random_state=0)


 この後に続くオーバーサンプリングの動作検証を容易にするため、データ数(サンプル数)は、極端に少なくしています。


 作成したサンプルデータセットは、以下の通りです。

f:id:Pimientito:20190817233437p:plain
サンプルデータセットの確認


 図「サンプルデータセットの確認」の最後で、コンテナデータ型「Collections」のCounterオブジェクトを使って、目的変数(クラスラベル)「y」の内訳を確認しています。


 Counter(y).items( )では、目的変数(クラスラベル)「y」の内容を[(要素名, 個数), ....]の組み合わせでリスト化しています。


 このデータセットを使用して、各オーバーサンプリングの特徴を確認します。


SMOTE(the Synthetic Minority Oversampling Technique)

 はじめに、参考資料「前処理大全」で紹介されているSMOTEアルゴリズムの説明文を参考に概要図を作成しました。

1.生成元のデータからランダムに1つのデータを選択

f:id:Pimientito:20191013171334j:plain
SMOTE概要図1(ターゲット1選択)


2.設定したkの値を元に、1~kの整数値(一様分布)からランダムに選択しnを設定

f:id:Pimientito:20191013171408j:plain
SMOTE概要図2(ターゲット2候補探索)


 ここで説明されている「kの値」とは、R言語ubBalance関数、またはPythonSMOTE関数のパラメータを指しています。(PythonのSMOTE関数のパラメータの場合「k_neighbors」)


 SMOTE関数のパラメータや仕様については、後程、詳しく学習します。


3.1で選択したデータにn番目に近いデータを新たに選択

f:id:Pimientito:20191013171449j:plain
SMOTE概要図3(ターゲット2選択)


4.1と3で選択したデータを元に新たなデータを生成

f:id:Pimientito:20191013171520j:plain
SMOTE概要図4(新規サンプル生成)


5.指定したデータ数に達するまで、1から4を繰り返す

参考・参照元:「前処理大全」6-2「オーバーサンプリングによる不均衡データの調整」(p.148-149)より一部抜粋


 上記では、SMOTEによるサンプル生成の流れを、大まかに掴みました。


 続いてimbalanced-learn公式ドキュメント(以後、"公式ドキュメント"と表記) 2.2.1.「Sample generation」(サンプル生成)で紹介されているサンプル生成の公式を学習します。


 SMOTEアルゴリズムのサンプル生成の公式は、以下の通りです。

   X_{new} = X_i\ +\ \lambda\ \times\ (X_{zi}\ -\ X_i)


 この公式は「 X_{i} X_{zi}の直線上に新しいサンプルを生成する」ことを表しています。また  \lambda (ラムダ)は、範囲0~1の乱数を意味します。


 下図の「 X_{new}」は、 X_{i} X_{zi}の中心に位置していますが、 \lambda (ラムダ)は乱数のため、必ずしも X_{i} X_{zi}の中心に、新規サンプルが生成されるとは限りません。


 前述した図に、SMOTEの公式の文字を重ねると、以下のようになります。  

f:id:Pimientito:20191012222317j:plain
SMOTEによるサンプル生成


 またSMOTE関数には、いくつか異なるバリエーションもあるようです。

関数名 概要
SMOTENC 説明変数に、連続値の列(特徴量)と離散値(カテゴリ値)の列が混在するデータセットを扱うときに使用
Synthetic Minority Over-sampling Technique for Nominal and Continuous (SMOTE-NC)
BorderlineSMOTE 少数派サンプル群と非少数派サンプル群との境界を利用し、最近傍法でサンプルを生成(SMOTEアルゴリズムに類似)
SVMSMOTE SVM分類器を使用してサポートベクトルを見つけ、それらを考慮したサンプルを生成
KMeansSMOTE SMOTEを適用する前にKMeansクラスタリング手法を使用する

参考・参照元:imbalanced-learn「imbalanced-learn API」、2.2.1. 「Sample generation」、他多数(本記事下部【参考資料】参照)を参考に作成


 機械学習アルゴリズムの「SVM(Support Vector Machine)」や「k-means法(k 平均法)」が、SMOTE関数に含まれていることを、公式ドキュメントを通して知りました。それぞれのアルゴリズムについては、次回の学習で、もう少し詳しく調べるため、今回は割愛し、SMOTE関数の学習を進めます。


 今回も公式ドキュメントで紹介されているサンプルコードを参考に、SMOTEの動きを確認します。SMOTEクラスのパラメータは、以下の通りです。

パラメータ名 初期値 概要
sampling_strategy auto リサンプリング(サンプル生成)対象クラスの指定
(autoの場合、'not majority'同等)
random_state None ランダム関数の指定
(Noneの場合、np.randomを使用)
k_neighbors 5 サンプル生成時に使用する最近傍のサンプル数
m_neighbors deprecated ver.0.4以降、使用非推奨
ver.0.6で削除予定のパラメータ
out_step deprecated ver.0.4以降、使用非推奨
ver.0.6で削除予定のパラメータ
kind deprecated ver.0.4以降、使用非推奨
ver.0.6で削除予定のパラメータ
svm_estimator deprecated ver.0.4以降、使用非推奨
ver.0.6で削除予定のパラメータ
n_jobs 1 可能な場合に開くスレッド数
ratio None ver.0.4以降、使用非推奨
ver.0.6で削除予定のパラメータ


 SMOTEを使用してサンプル生成を行なったコードは、以下の通りです。

# imbalanced-learn「imblearn.over_sampling.SMOTE」Examplesを参考に作成
# https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.SMOTE.html#imblearn.over_sampling.SMOTE

# imbalanced-learn API パッケージ
from imblearn.over_sampling import SMOTE

# オリジナル・サンプルデータの目的変数の内訳表示
print('Original dataset shape %s' % Counter(y))

# SMOTEクラスの生成
# k_neighborsの初期値5から1に変更
# random_stateの初期値「None」から乱数ジェネレータ用シード「42」に変更
# (サンプルコードの設定値をそのまま指定)
# 初期値が'deprecated'のパラメータは、コードから除外
sm = SMOTE(sampling_strategy='auto', k_neighbors=1, n_jobs=1, random_state=42, ratio=None)

# リサンプリング(サンプル生成)
X_resampled, y_resampled = sm.fit_resample(X, y)

# リサンプリング後の目的変数の内訳表示
print('Resampled dataset shape %s' % Counter(y_resampled))


 サンプルデータセットと、SMOTEによるサンプル生成を比較した結果は、以下の通りです。

f:id:Pimientito:20191015001134p:plain
サンプル生成結果の確認


 SMOTEにより不均衡データが調整されていることを確認できました。


今回の可視化

 前回【前処理の学習-34】同様に、公式ドキュメントの「Comparison of the different over-sampling algorithms」の「def plot_decision_function」と「Random over-sampling to balance the data set」のコードを組み合わせて、最初に作成したサンプルデータセットとSMOTEで生成したデータセットを比較表示させました。


 可視化のコーディングは、以下の通りです。

# 可視化(サンプルデータセットとRandomOverSamplerの出力結果を比較)
# 参考・参照元:
# 'Comparison of the different over-sampling algorithms'と
# 'Random over-sampling to balance the data set'を元に作成
# https://imbalanced-learn.readthedocs.io/en/stable/auto_examples/over-sampling/plot_comparison_over_sampling.html

import numpy as np

import matplotlib.pyplot as plt

from sklearn.svm import LinearSVC

# --- 関数 ---
def plot_decision_function(X, y, clf, ax):
    plot_step = 0.01
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() -1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
                                         np.arange(y_min, y_max, plot_step))
    
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    ax.contourf(xx, yy, Z, alpha=0.4)
    ax.scatter(X[:, 0], X[:, 1], alpha=0.8, c=y, edgecolor='k')

    
# --- 可視化 ---
# オブジェクトの初期化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7))


# オリジナル・サンプルデータセット
clf = LinearSVC().fit(X, y)
plot_decision_function(X, y, clf, ax1)

ax1.set_title('Original Sample Dataset')

# RandomOverSamplerで生成したデータセット
clf2 = LinearSVC().fit(X_resampled, y_resampled)
plot_decision_function(X_resampled, y_resampled, clf2, ax2)

ax2.set_title('SMOTE\'s result')

# プロット図表示
fig.tight_layout()



 プロット図を表示した結果は、以下の通りです。

f:id:Pimientito:20191015001629p:plain
オリジナルデータセットとSMOTEで生成したデータセットの比較

 上図の右側、SMOTEで生成したデータセットの少数データの丸印(紫色)が重なってしまい判別できません。サンプルの生成結果を確認するため「Original Sample Dataset」と「SMOTE's result」の同じ場所を拡大表示してみました。


f:id:Pimientito:20191015002139p:plain
拡大図(Original Sample Dataset)


f:id:Pimientito:20191015002246p:plain
拡大図(SMOTE's result)


 いかがでしょうか。図を拡大することで、サンプルが生成されていることを確認できました。ただし、初期の少数データが2個のみであるため、新しいサンプルは、その二点間でしか生成されていません。より複雑なサンプル生成を確認するには、初期のサンプル数を増やす必要があります。


今回のまとめ

 前回学習した「ランダム・オーバーサンプリング」は、既存サンプル(データ)のコピーのため、サンプル数は増えるものの、その内容は単純なものでした。


 今回学習した「SMOTE」は、無作為に選択された二点のサンプルの間のどこかに、新しいサンプルを生成するため、ランダム・オーバーサンプリングの結果より複雑な内容となりました。


 しかし、前回、今回の学習で使用した目的変数のサンプル(データ)は、すべて連続値であったため、これが離散値・カテゴリ値の場合、どのような結果になるのか。例えば、以前の学習で使用してきた様々なオープンデータの場合、どのように不均衡データを調整することができるのか。


 学習中は、都合の良いサンプルデータを使用してしまうため、現実社会のデータに置換えた時、自分は応用できるのだろうかと、ふっと考えてしまいます。常に「実践」を意識しながら、これからも学習を続けます。


 参考資料「前処理大全」の第6章「生成」では、SMOTEの説明で終了していますが、前回、今回の学習の中で、筆者自身が、いくつか調べたい事柄と出会ったため、次回も「生成」を学習します。



 今回は、以上です。



 

【参考資料】

imbalanced-learn Doc「Over-sampling」

imbalanced-learn.readthedocs.io


imbalanced-learn Doc「Comparison of the different over-sampling algorithms」

imbalanced-learn Doc「Random over-sampling to balance the data set」

imbalanced-learn.readthedocs.io


sklearn.datasets.make_classification

scikit-learn.org


Hatena Blog「データ分析がしたい」~ scikit-learnを用いたサンプルデータ生成 ~(Overlap氏著)

overlap.hatenablog.jp


Illustration of the sample generation in the over-sampling algorithm

imbalanced-learn.readthedocs.io


imbalanced-learn API

imbalanced-learn.readthedocs.io


imbalanced-learn「imblearn.over_sampling.SMOTENC」

imbalanced-learn.readthedocs.io


imbalanced-learn「imblearn.over_sampling.BorderlineSMOTE」

imbalanced-learn.readthedocs.io


imbalanced-learn「imblearn.over_sampling.SVMSMOTE」

imbalanced-learn.readthedocs.io


imbalanced-learn「imblearn.over_sampling.KMeansSMOTE」

imbalanced-learn.readthedocs.io


Borderline-SMOTE: A New Over-Sampling Method in Imbalanced Data Sets Learning

https://sci2s.ugr.es/keel/keel-dataset/pdfs/2005-Han-LNCS.pdf



本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。


お問い合わせ先:otoiawase@handson-t-arte.com



【前処理の学習-34】データを学ぶ ~生成~②

 前回は、データの偏りを調整する「アンダーサンプリング」について学びました。

pimientito-handson-ml.hatenablog.com

 今回は「オーバーサンプリング」の方法で不均衡データの調整を行ないます。


【今回の目標到達点】

 ランダム・オーバーサンプリングによるデータ調整を学ぶ

【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「生成」の概要

 参考資料「前処理大全」第6章「生成」の冒頭で、著者はデータの生成について、以下のように述べています。

十分にデータがある場合には、データの生成が必要となることはまずありません。データを無理やり増やしても、データの価値は増やす前のデータが持っている価値とほとんど変わらないからです。しかし、それでもデータ生成が必要となるケースがあります。それは不均衡データを調整するときです。

参考・参照元:第6章「生成」(p.146)より抜粋


 著者は、同じく本章冒頭で、不均衡データ(偏りの大きいデータ)を、予測モデルの学習に使用すると予測精度が低下すると述べています。


 偏りを調整する主な方法として、参考資料では、以下の二点を挙げています。


  • データに重み付けして偏りを調整する

  • データを操作して偏りを整える



 一点目の「データに重み付けして偏りを調整する」は、機械学習モデル作成時に行なう方法で、学習モデルによって対処がそれぞれ異なるようです。学習モデルの作成については未学習で、説明をまとめることが困難なため、今回の学習では割愛しています。


 二点目の「データを操作して偏りを整える」の具体的な方法は、以下の通りです。


  • 件数の多いデータを減らす方法「アンダーサンプリング」

  • 件数の少ないデータを増やす方法「オーバーサンプリング」

  • 「アンダーサンプリング」と「オーバーサンプリング」を組み合わせる方法



今回のテーマ

 件数の少ないデータを増やす方法「オーバーサンプリング」


概要

 オーバーサンプリングは、不均衡データの中で、出現率が低いデータを増やす方法です。オーバーサンプリングの概要図は、以下の通りです。

f:id:Pimientito:20190825161505j:plain
オーバーサンプリング概要図


 参考資料 6-2「オーバーサンプリングによる不均衡データの調整」では、オリジナルデータを元に新しいデータを生成するSMOTEを紹介しています。


 SMOTEは、imbalanced-learn APIのOver-sampling methodsのひとつです。imbalanced-learn公式ドキュメント(以後、"公式ドキュメント"と表記)では、SMOTEの他にもオーバーサンプリングを実現する方法を紹介しています。


 ここから少しの間、参考資料「前処理大全」から離れ、公式ドキュメントの「2. Over-sampling」を元に、オーバーサンプリングについて学習を進めていきます。


オーバーサンプリングの種類

 公式ドキュメント「2. Over-sampling」で紹介されている方法は、以下の通りです。

  • Naive random over-sampling

  • the Synthetic Minority Oversampling Technique(SMOTE)

  • the Adaptive Synthetic(ADASYN)


今回の学習

 今回から数回に分けて、公式ドキュメントのサンプルコードを参考にオーバーサンプリングを学びます。


サンプルデータセットの作成

 はじめにscikit-learnパッケージのmake_classification関数を使って不均衡なサンプルデータセットを作成します。各種パラメータ(一部)は、以下の通りです。

パラメータ名 概要
n_samples int 生成サンプルの個数
n_features int 特徴量の数
n_informative int 目的変数のラベルと相関が強い特徴量の数
特徴量名称:Infomative feature
n_redundant int Informative featureの線形結合から生成される特徴量の数
特徴量名称:Redundant feature
n_repeated int Informative, Redundant featureのコピーからなる特徴量の数
特徴量名称:Repeated feature
n_classes int 分類するクラス数
2を指定した場合:2値分類問題
3以上を指定した場合:多値分類問題
n_clusters_per_class int 1クラスあたりのクラスタ
weights list(float) クラスの比率。
不均衡データ生成時に使用
class_sep float 生成アルゴリズムに関係するパラメータ
random_state int 乱数パラメータ

参考・参照元:Hatena Blog「データ分析がしたい」~ scikit-learnを用いたサンプルデータ生成 ~(Overlap氏著) を参考に作成


 make_classification関数のパラメータについては、scikit-learn公式ドキュメント(英文)の説明だけでは、筆者には理解できなかったため、Overlap氏のブログ記事「データ分析がしたい」を参考にさせていただきました。(記事の詳細については、記事末尾【参考資料】をご覧ください)


 「2値分類問題」や「多値分類問題」、「生成アルゴリズム」など、多岐に渡り、まだまだ不勉強なところは沢山ありますが、このまま学習を進めます。


 サンプルデータセットを作成するコードは、以下の通りです。

# 参考・参照元:imbalanced-learn Documentation 「2. Over-sampling」
# https://imbalanced-learn.readthedocs.io/en/stable/over_sampling.html
# 「2.1.1. Naive random over-sampling」を参考に作成

# サンプルデータセットの作成

# scikit-learn パッケージ
from sklearn.datasets import make_classification

X, y = make_classification(
    n_samples=10,          # 生成データ(サンプル)数:10件
    n_features=2,
    n_informative=2,
    n_redundant=0, 
    n_repeated=0, 
    n_classes=2,
    n_clusters_per_class=1, 
    weights=[0.20, 0.80],  # 2対8の割合
    class_sep=0.8, 
    random_state=0)


 オーバーサンプリングの動作確認を容易にするため、データ数(サンプル数)は、極端に少なくしています。


 作成したサンプルデータセットは、以下の通りです。

f:id:Pimientito:20190817233437p:plain
サンプルデータセットの確認


 図「サンプルデータセットの確認」の最後で、コンテナデータ型「Collections」のCounterオブジェクトを使って、目的変数(クラスラベル)「y」の内訳を確認しています。


 Counterオブジェクトの書式は、以下の通りです。

class collections.Counter([iterable-or-mapping])


 参考・参照元Python 3.7.4 ドキュメント「collections --- コンテナデータ型」より抜粋


 Counter(y).items( )では、目的変数(クラスラベル)「y」の内容を[(要素名, 個数), ....]の組み合わせでリスト化しています。


 このデータセットを使用して、各オーバーサンプリングの特徴を確認します。


パッケージのインストール

 imbalanced-learn APIを使用する前にパッケージをインストールします。インストール方法は、以下の通りです。

  • pipの場合
pip install -U imbalanced-learn

参考・参照元:imbalanced-learn Docs「Install and contribution」- Install -より抜粋


  • Anacondaの場合
conda install -c conda-forge imbalanced-learn

参考・参照元:imbalanced-learn Docs「Install and contribution」- Install -より抜粋


 他にも依存関係のあるライブラリやパッケージが必要になる場合もあります。適宜インストールしてください。


Naive random over-sampling(RandomOverSampler)

 直訳すると「単純なランダム・オーバーサンプリング」。公式ドキュメント「2. Over-sampling」では、一般的なランダム・オーバーサンプリングとして「RandomOverSampler」が紹介されています。


 参考資料「前処理大全」 6-2「オーバーサンプリングによる不均衡データの調整」の冒頭では、ランダム・オーバーサンプリングについて、以下のように説明されています。

オーバーサンプリングはオリジナルデータから新たなデータを生成します。生成する方法の1つとして、ランダムサンプリングによって元のデータ数より多くデータを抽出する方法があります。この方法は非常に簡単ですが問題もあります。それは、完全に同じデータが出現してしまい、過学習が発生しやすくなってしまう問題です。

参考・参照元:6-2「オーバーサンプリングによる不均衡データの調整」(p.148)より抜粋


 ランダム・オーバーサンプリングの問題点を意識しつつ、動作確認を行ないます。はじめにRandomOverSamplerオブジェクトを生成します。

# 参考・参照元:imbalanced-learn Documentation 「2. Over-sampling」
# https://imbalanced-learn.readthedocs.io/en/stable/over_sampling.html
# 「2.1.1. Naive random over-sampling」を参考に作成

# imbalanced-learn API パッケージ
from imblearn.over_sampling import RandomOverSampler

# オブジェクト生成
ros = RandomOverSampler(random_state=0)

# 生成結果の確認
ros


 生成したオブジェクトの確認結果は、以下の通りです。

f:id:Pimientito:20190825224608p:plain
RandomOverSamplerオブジェクト


RandomOverSamplerクラスのパラメータは、以下の通りです。

パラメータ名 初期値 概要
random_state None 乱数アルゴリズムの種類を選択
ratio None ver 0.4以降、'sampling_strategy'で代替
ver 0.6にて削除予定
return_indices False 選択されたサンプルのインデックスを戻す
(ver 0.4以降、sample_indices_で代替)
sampling_strategy auto 再サンプリング対象の型や取得方法の選択
'auto'の場合、'not majority'と同等
(多数クラス外の全クラスを再サンプリング)


 生成されたRandomOverSamplerオブジェクトのfit_resampleメソッドを使って不均衡データを元にランダム・オーバーサンプリングを行ないます。

# サンプルデータセットを元に再サンプリング
X_resampled, y_resampled = ros.fit_resample(X, y)


 ランダム・オーバーサンプリングした結果は、以下の通りです。

f:id:Pimientito:20190825234658p:plain
ランダム・オーバーサンプリングによる再サンプリングの結果


 赤枠と青枠で囲っている部分が、重複したデータになっています。


 上述した参考資料「前処理大全」 6-2「オーバーサンプリングによる不均衡データの調整」の冒頭を再掲します。(ここでは、注目する部分を強調するため下線付き太字で表示していますが、原文は下線付き太字表記ではありません)

オーバーサンプリングはオリジナルデータから新たなデータを生成します。生成する方法の1つとして、ランダムサンプリングによって元のデータ数より多くデータを抽出する方法があります。この方法は非常に簡単ですが問題もあります。それは、完全に同じデータが出現してしまい過学習が発生しやすくなってしまう問題です。

参考・参照元:6-2「オーバーサンプリングによる不均衡データの調整」(p.148)より抜粋


 上図「ランダム・オーバーサンプリングによる再サンプリングの結果」の内容をまとめたのが、以下の表です。

n X_resampled(0) X_resampled(1) y_resampled nと同値 再サンプリング
1 0.98246389 0.4476239 1 - -
2 0.67391009 0.3072163 1 - -
3 0.0435122 0.14259842 0 - -
4 0.35221602 3.40358518 1 - -
5 1.10353352 -0.68392691 1 - -
6 0.98813064 -0.14831747 1 - -
7 0.09053667 0.00629453 0 - -
8 1.1739973 0.98740521 1 - -
9 1.00750599 0.09484027 1 - -
10 0.87174145 0.9913926 1 - -
11 0.0435122 0.14259842 0 3
12 0.09053667 0.00629453 0 7
13 0.09053667 0.00629453 0 7
14 0.0435122 0.14259842 0 3
15 0.09053667 0.00629453 0 7
16 0.09053667 0.00629453 0 7


 新しく生成された11番目から16番目のデータは、最初に作成したサンプルデータセットの3番目と7番目のデータと重複しています。


 参考資料で言及されている通り、データ数(サンプル数)は増えるものの、重複したデータが生成されていることを確認できました。


 続いて目的変数yとy_resampledの値を並べて比較してみます。

# collections (コンテナデータ型)
from collections import Counter

# 目的変数yとy_resampledのクラスラベル数の確認
print('y                      : ' + str(sorted(Counter(y).items())))
print('y_resampled  : ' + str(sorted(Counter(y_resampled).items())))


 比較した結果は、以下の通りです。

f:id:Pimientito:20190831173423p:plain
目的変数yとy_resampledの比較


 はじめに作成したサンプリングデータセットの目的変数(クラスラベル)yでは、ラベル'0'は2個でしたが、RandomOverSamplerで生成したy_resampledでは、ラベル'0'が8個に増えています。これで、ラベル'1'のデータ数(サンプル数)と同じになったことが確認できました。


 しかし、前回のアンダーサンプリングの学習では気付きませんでしたが、今回の学習を通して、筆者に新たな疑問が浮かびました。それは、。。。



  • 不均衡データとは、目的変数(クラスラベル)のみを対象とするものなのか。

  • 説明変数の偏りは、再サンプリングの対象となるのか。



 機械学習を系統立てて、順を追って学んでいないせいか、筆者は「機械学習の基礎」的なことを理解せず、学習を続けていることに、あらためて気付かされました。


 この疑問についても、今後、学習を続けていくなかで、調べて行かなければなりません。


今回の可視化

 今回の可視化は、公式ドキュメントの「Comparison of the different over-sampling algorithms」の「def plot_decision_function( )」と「Random over-sampling to balance the data set」のコードを組み合わせて、最初に作成したサンプルデータセットとRandomOverSamplerで生成したデータセットを比較表示させました。


 コードのなかで、scikit-learnのLinearSVCを使用していますが、ドキュメントや各種参考資料を調べても、筆者自身、まだ原理や理屈が飲み込めていないため、詳細な説明は割愛します。


 記事末尾【参考資料】に、筆者が参考にさせていただいた情報の一部を記載していますので、ご興味がございましたらご覧ください。


 可視化のコーディングは、以下の通りです。

# 可視化(サンプルデータセットとRandomOverSamplerの出力結果を比較)
# 参考・参照元:
# 'Comparison of the different over-sampling algorithms' と
# 'Random over-sampling to balance the data set'を元に作成
# https://imbalanced-learn.readthedocs.io/en/stable/auto_examples/over-sampling/plot_comparison_over_sampling.html

import numpy as np

import matplotlib.pyplot as plt

from sklearn.svm import LinearSVC

# --- 関数 ---
def plot_decision_function(X, y, clf, ax):
    plot_step = 0.01
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() -1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
                                         np.arange(y_min, y_max, plot_step))
    
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    ax.contourf(xx, yy, Z, alpha=0.4)
    ax.scatter(X[:, 0], X[:, 1], alpha=0.8, c=y, edgecolor='k')

    
# --- 可視化 ---
# オブジェクトの初期化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7))


# オリジナル・サンプルデータセット
clf = LinearSVC().fit(X, y)
plot_decision_function(X, y, clf, ax1)

ax1.set_title('Original Sample Dataset')

# RandomOverSamplerで生成したデータセット
clf2 = LinearSVC().fit(X_resampled, y_resampled)
plot_decision_function(X_resampled, y_resampled, clf2, ax2)

ax2.set_title('Random Over Sampling result')

# プロット図表示
fig.tight_layout()


 プロット図を表示した結果は、以下の通りです。

f:id:Pimientito:20190831221351p:plain
オリジナルデータセットとRandomOverSamplerで生成したデータセットの比較


 RandomOverSamplerで生成されてデータ(サンプル)の数は増えているにも関わらず、すべて重複した数値のため、プロット図のマーカーの数は、図「Original Sample Dataset」と図「Random Over Sampling result」共に10個ですが、決定境界線の角度が異なっていることは、筆者にも理解できます。


 しかし、それが何を意味しているのかは、残念ながら、まだ理解できていません。


今回のまとめ

 今回は、オーバーサンプリングの方法の一つである「ランダム・オーバーサンプリング」を学習しました。


 これまでの学習では、機械学習モデルで使用するデータの加工が主でしたが、今回のようにデータを生成することになると、いままで以上に、そのデータが使われる先(機械学習モデル)を意識する場面が増えてきました。


 文中でも述べましたが、そもそも機械学習モデルに使用するデータの形式(フォーマット)について、説明変数・目的変数や、ラべリングなどキーワードとして知っていても、現実社会の情報(いままでの学習で言えばオープンデータなど)を、どのように機械学習用データとして、再構築しなければいけないのか、あらためて考えさせられました。


 いままでの学習では、漠然と『参考資料「前処理大全」の学習が済んでから、機械学習アルゴリズムを学習する。』と考えていましたが、本来はそうではなく「同時進行で、学習を進めていく」ものだと、いつにも増して痛感しました。


 統計学の学習も取り入れながら「機械学習」へアプローチしていますが、まだまだ圧倒的に学習不足です。


 次回も、引続きオーバーサンプリングの学習を進めて行きます。



 今回は、以上です。



【参考資料】

imbalanced-learn Doc「Install and contribution」

imbalanced-learn.readthedocs.io


imbalanced-learn Doc「Over-sampling」

imbalanced-learn.readthedocs.io


imbalanced-learn Doc「Comparison of the different over-sampling algorithms」

imbalanced-learn Doc「Random over-sampling to balance the data set」

imbalanced-learn.readthedocs.io


sklearn.datasets.make_classification

scikit-learn.org


Hatena Blog「データ分析がしたい」~ scikit-learnを用いたサンプルデータ生成 ~(Overlap氏著)

overlap.hatenablog.jp


Python 3.7.4 ドキュメント >>Python標準ライブラリ >>データ型「collections --- コンテナデータ型」

docs.python.org


imblearn.over_sampling.RandomOverSampler

imbalanced-learn.readthedocs.io


Qiita 「scikit-learnでSVMのパラメータを調節してみた話」(@arata-honda氏著)

qiita.com


Qiita「SVM(サポートベクターマシーン)についてまとめてみた」(@arata-honda氏著)

qiita.com


Risky Dune「scikit.learn手法徹底比較! SVM編」

saket.hatenadiary.org



Qiita「機械学習~線形モデル(分類)~」(@fujin氏著)

qiita.com


HatenaBlog「見習いデータサイエンティストの隠れ家」~機械学習の分類結果を可視化!決定境界~(こーめい(dskomei)氏著)

www.dskomei.com



本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。



お問い合わせ先:otoiawase@handson-t-arte.com



【前処理の学習-33】データを学ぶ ~生成~①

 前回まで二回に渡り、予測モデルの学習・検証用データの「分割」について学びました。

pimientito-handson-ml.hatenablog.com

 今回からデータの「生成」について学びます。


【今回の目標到達点】

 アンダーサンプリングによるデータ調整を学ぶ

【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「生成」の概要

 参考資料「前処理大全」第6章「生成」の冒頭で、著者はデータの生成について、以下のように述べています。

十分にデータがある場合には、データの生成が必要となることはまずありません。データを無理やり増やしても、データの価値は増やす前のデータが持っている価値とほとんど変わらないからです。しかし、それでもデータ生成が必要となるケースがあります。それは不均衡データを調整するときです。

参考・参照元:第6章「生成」(p.146)より抜粋


 著者は、同じく本章冒頭で、不均衡データ(偏りの大きいデータ)を、予測モデルの学習に使用すると予測精度が低下すると述べています。


 不均衡データの偏りを調整する主な方法として、参考資料では、以下の二点を挙げています。


  • データに重み付けして偏りを調整する

  • データを操作して偏りを整える



 一点目の「データに重み付けして偏りを調整する」は、機会学習モデル作成時の方法で、学習モデルの種類や使用するライブラリによって、対処がそれぞれ異なり、いまの筆者のスキルでは説明をまとめることが難しいため、今回の学習では割愛します。


 二点目の「データを操作して偏りを整える」について、参考資料では、以下の方法を紹介しています。


  • 件数の多いデータを減らす方法「アンダーサンプリング」

  • 件数の少ないデータを増やす方法「オーバーサンプリング」

  • 「アンダーサンプリング」と「オーバーサンプリング」を組み合わせる方法



 今回から、上記の「アンダーサンプリング」と「オーバーサンプリング」について学びます。


今回のテーマ

 件数の多いデータを減らす方法「アンダーサンプリング」


概要

 参考資料6-1「アンダーサンプリングによる不均衡データの調整」では、データ量が多い方を、少ない方に寄せてデータ量を減らすことで、学習データ量のバランスを整える方法を取り上げています。

 アンダーサンプリングの概要図は、以下の通りです。

f:id:Pimientito:20190722234428j:plain
アンダーサンプリング概要図


 四角形の一つ一つを、それぞれ独立したデータ(またはレコード)と仮定したとき、上図左側は、白色データ量と黄色データ量のバランスが悪い不均衡データであると言えます。


 データを減らす方法は、いままで学んできたランダムサンプリングを使って行ないます。しかし、著者曰く、アンダーサンプリングは、情報量を減らしてしまうため、安易な使用は避けた方が良いと述べており、できればオーバーサンプリングを選択するか、またはアンダーサンプリングとオーバーサンプリングを組み合わせた方法を勧めています。


 なお、サンプリング・レートは、事前にデータの偏りを確認しつつ決めていくようです。


 

サンプルコード[Python]

 参考資料6-1「アンダーサンプリングによる不均衡データの調整」では、残念ながらサンプルコードは紹介されていません。


 しかし、参考資料の第2章「抽出」や、第4章「結合」で学んだ内容を応用すれば、今回の課題は、独自でコーディングすることができます。


サンプルデータセットを作成

 はじめにサンプルデータを作成します。

# サンプルデータの作成
import pandas as pd
import numpy as np

# 実行する度に生成される乱数値を固定する
np.random.seed(3)

# 1.0以上3.0未満の乱数を生成し、小数点以下を四捨五入
# 小数点以下を四捨五入するため、1.0, 2.0, 3.0の整数が生成される
df = pd.DataFrame({
    'numbers' : np.round((3 - 1) * np.random.rand(100) + 1)
})


 今回作成したサンプルデータの概要は、以下の通りです。

カラム名 範囲 データ数 概要
numbers 実数 1.0〜3.0 100 np.random.rand関数で生成した
1.0以上3.0未満の実数をround関数で
小数点以下を四捨五入


 np.random.seed関数を使用することで、毎回、同じ値(1.0以上3.0未満)が生成されます。


 またseed関数の引数や、生成されるデータの件数は、不均衡データになるよう調整しながら決めました。


 余談ですが、不均衡データを生成するに当たり、色々と試してみましたが、生成する値の種類(今回の場合、1.0~3.0の値)を増やしたり、生成されるデータの件数を増やすほど、各要素ごとの出力件数が、ある程度、均一になりやすいため、不均衡データを作成することが容易ではありませんでした。


 作成したサンプルデータは、以下の通りです。

f:id:Pimientito:20190728162337p:plain
サンプルデータの確認


不均衡データの確認

 次に、サンプルデータの不均衡の程度を確認します。groupby関数を使用してカラム「numbers」の種類ごとにデータ数をカウントします。

# グループ化し、それぞれ整数の個数を数える
result = df.groupby('numbers', as_index=False).size().reset_index()

# 抽出結果にカラム名を付与
result.columns = ['numbers', 'count']


 確認した結果は、以下の通りです。

f:id:Pimientito:20190728163859p:plain
グループ化したデータの件数を確認


 不均衡データを可視化して確認してみます。

# 結果を可視化(Seabornライブラリを使用)
from matplotlib import pyplot as plt
import seaborn as sns

sns.barplot(x='numbers', y='count', data=result)
plt.show()


 可視化した結果は、以下の通りです。

f:id:Pimientito:20190728164307p:plain
サンプルデータの可視化


 明らかに2.0のデータ数が多いです。このような不均衡なデータを、機械学習モデルの訓練や検証に使用すると、予測精度が低下するということも頷けます。


 例えば、現実では、1.0と3.0の出現率が圧倒的に高く、2.0はほとんど出現しないといった事例や現象を予測する機械学習モデルを作成している場合、このデータでは、正確な予測ができないでしょう。


サンプリング・レートを決める

 上記では、カラム「numbers」の2.0の件数が、他の二件に比べて多いことが分かりました。今回は、この2.0のデータをランダムサンプリングすることでデータ量を減らします。では、どのようにしてサンプリング・レートを決めるのでしょうか。


 今回は、すべてのデータ件数が、おおよそ同じ(20〜30件程度)になるようにしました。サンプリング・レートは、以下のように算出します。


 「最少データ件数の相対度数の二倍


 コードは、以下の通りです。

# サンプリング・レートを算出(最少データ件数の相対度数の二倍)
sampling_rate = (result['count'].min() / df.count()) * 2.0

# 結果の確認
sampling_rate


 算出したサンプリング・レートは、以下の通りです。

f:id:Pimientito:20190728171006p:plain
サンプリング・レートの算出


 今回、カラム「numbers」の3.0のデータが最も少なく14件(全体の14%)であったため、その二倍にあたる28%が、サンプリング・レートとして算出されました。


サンプリング対象を選択

 先ほどプロット図を通して、どのデータが一番件数が多いのかを確認しましたが、目視で確認した結果をコーディングするのは、あまり実用的とは言えません。あくまで機械的に判断した結果を、次の処理で利用する流れで進めていきます。


 最多件数のデータを確認し、データを抽出します。

# 最多件数の項目を確認
max_col = result.loc[(result['count'] == result['count'].max()), :]

# 結果の確認
max_col


 今回は、loc関数の中に条件式を入れ子にして、最多件数のデータの情報を取得しています。取得した情報は、以下の通りです。

f:id:Pimientito:20190728220651p:plain
サンプリング対象データの確認

ランダムサンプリングの実行

 ここでは、以下の通りにデータ抽出を行ないます。


  • サンプリング対象データ:サンプリング・レートに合わせて抽出

  • サンプリング非対象データ:すべて抽出



 それぞれ抽出したのちに、pandasのconcat関数を使用してデータをマージします。ここまでの処理のコーディングは、以下の通りです。

# データ量が多い不均衡データをサンプリング・レートの比率で、ランダムサンプリング
random_sampling = df.loc[(df['numbers'].values == max_col['numbers'].values), :].sample(frac=sampling_rate).reset_index(drop=True)

# 不均衡データ以外のデータをすべて抽出
non_random_sampling = df.loc[(df['numbers'].values != max_col['numbers'].values), :].reset_index(drop=True)

# ランダムサンプリングしたデータと、それ以外のデータを結合
df2 = pd.concat([random_sampling, non_random_sampling], axis=0).reset_index(drop=True)


 マージ後のデータを、再度、groupby関数で集約して、データの件数を確認します。

# グループ化し、それぞれ整数の個数を数える
result2 = df2.groupby('numbers', as_index=False).size().reset_index()

# 抽出結果にカラム名を付与
result2.columns = ['numbers', 'count']


 調整前と調整後の内容を並べてプロット図に表示し比較します。

# 不均衡データの調整前後を可視化して比較

# 可視化ライブラリ
import seaborn as sns
import matplotlib.pyplot as plt

# 図の初期化
fig, axes = plt.subplots(1, 2, sharey=True)

# 各プロットへデータセットを関連付け
# 不均衡データ調整前
result.plot.bar(ax=axes[0], x='numbers', y='count')

# 不均衡データ調整後
result2.plot.bar(ax=axes[1], x='numbers', y='count')


# 図の表示調整
# サブプロット図間の空間を0に設定
plt.subplots_adjust(wspace=0)

# 不均衡データ調整前
axes[0].set_title('Before')
axes[0].set_ylabel('count')
axes[0].legend().set_visible(False)    # 凡例非表示
axes[0].grid(axis='y', color='g', linestyle='-.', linewidth=0.5 )    # グリッド線の設定
axes[0].set_xticklabels(result['numbers'], rotation=0)                # X軸ラベルの表示向き

# 不均衡データ調整後
axes[1].set_title('After')
axes[1].legend().set_visible(False)
axes[1].grid(axis='y', color='g', linestyle='-.', linewidth=0.5 )
axes[1].set_xticklabels(result2['numbers'], rotation=0)

# 可視化
plt.show()


 可視化した結果は、以下の通りです。

f:id:Pimientito:20190728222145p:plain
アンダーサンプリングによる不均衡データの調整結果


 カラム「numbers」の2.0の件数が減っていることを確認できました。不均衡データの調整後では、データ量が減ってしまいましたが、他の値の件数とのバランスは良くなりました。


今回のまとめ

 今回は、本ブログ始まって以来、参考資料に「サンプルコード」が無い回でした。そのため、参考資料を読みながら、自身でコーディングすることになりました。


 しかし、筆者が作成したコードが、参考資料の著者が意図した内容と一致しているかは分かりません。もしかしたら、もっと効率が良い方法があるかもしれません。


 いろいろなパターンを想定した効率的なコーディングをするためには、やはり良い手本に沢山触れ、自身でもコーディングしながら試行錯誤を続けるしか方法はありません。


 ブログ記事の中でも述べましたが、参考資料の著者は、不均衡データの調整では「アンダーサンプリング」と「オーバーサンプリング」を組み合わせた方が良いと述べています。


 次回「オーバーサンプリング」を学び、「アンダーサンプリング」と「オーバーサンプリング」の長所や短所を明確にしたいと思います。



 今回は、以上です。




本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。



お問い合わせ先:otoiawase@handson-t-arte.com



【前処理の学習-32】データを学ぶ ~分割~②

 前回は、交差検証を行なうためのデータの分割について学びました。

pimientito-handson-ml.hatenablog.com

 今回は、時系列データの分割について学びます。


【今回の目標到達点】

 時系列データの分割を学ぶ

【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

 「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「分割」の概要

 参考資料「前処理大全」第5章「分割」の冒頭で、著者はデータの分割について、以下のように述べています。

データの分割は予測モデルを評価する際に必要になる前処理です。主に学習データ(予測モデルを構築する際に利用するデータ)と検証データ(モデルの精度を測定するためのデータ)の分割に利用されます。

 学習データと検証データは、必要とする列データは同じです。「予測モデルに入力するための列データ」と「予測モデルの予測対象の列データ」です。そのため、学習データと検証データに対して適用する前処理はすべて同じです。

参考・参照元:第5章「分割」(p.130)より抜粋


 第5章「分割」では、データの分割について、以下の方法が紹介されています。


  • レコードデータにおけるモデル検証用のデータ分割

  • 時系列データにおけるモデル検証用のデータ分割



今回のテーマ

 時系列データにおけるモデル検証用のデータ分割


概要

 参考資料5-2「時系列データにおけるモデル検証用のデータ分割」では、時系列データの分割方法について紹介されています。

 なお著者曰く、前回学んだ交差検証では、時系列データを安易に使用できないと言及されています。以下、該当部分の抜粋です。

多くの人が間違えてしまいがちなのですが、実は時系列データにおいて単純な交差検証は有効ではありません。なぜなら、交差検証によって不当にモデル精度が高くなってしまうことが多いからです。これは、未来のデータを使って予測モデルを作成し、過去のデータを検証しているケースが混ざってしまっていることが大きな原因です。

参考・参照元:第5章「分割」5-2「時系列データにおけるモデル検証用のデータ分割」(p.138-139)より抜粋


 学習データに、予測対象のデータが混入することを「リーク(leak)」と呼びます。リークについては、前章「結合」で軽く触れました。

pimientito-handson-ml.hatenablog.com


 前回【前処理の学習-31】で学んだ交差検証(クロス・バリデーション)では、分割したデータを順序不同で学習・検証するため、未来データのリークを防ぐことができません。

f:id:Pimientito:20190601215316j:plain
交差検証の流れ


 上図の場合、オレンジ色の枠が検証データに該当します。学習・検証データの選択位置が交差検証ごとに前後するため、時系列データが適していないことが分かります。


 そこで著者は、時系列データに適した分割方法を紹介しています。


  • 時間軸に対してスライドしながら検証する方法

  • 時間軸に対して学習データを増やしながら検証する方法



 ひとつずつ処理の概要を学習します。


時間軸に対してスライドしながら検証する方法

 一つ目は、分割したデータを、過去から未来へ順次スライドする方法です。

f:id:Pimientito:20190701232018j:plain
学習期間を一定にするパターン


 この方法は、学習データ・検証データの抽出期間を固定して、検証ごとにデータの抽出場所をスライドします。例えば、検証データが二か月分右に移動すると、学習データも二か月分右に移動します。


 常に一定の期間でスライドしながら学習を進めるため、データのリークは発生しません。(事前に、日付型データの並び替えは必要です)


 上図「学習期間を一定にするパターン」の場合は、以下のようにデータをスライドしながら学習・検証しています。


  • 検証1回目:学習データ(1月~6月)、検証データ(7月~8月)

  • 検証2回目:学習データ(3月~8月)、検証データ(9月~10月)

  • 検証3回目:学習データ(5月~10月)、検証データ(11月~12月)



 この例題の問題点は、年間を通した学習・検証になっていないため、季節によって変動する傾向を正確に捉えることができません。そのため、このような学習・検証を行なう場合は、使用するデータが通年になるよう長い期間のデータが必要になると著者は述べています。


時間軸に対して学習データを増やしながら検証する方法

 二つ目は、時系列データの期間が短い場合、検証ごとにデータをスライドさせずに、使用済み検証データを、学習データに追加しデータ量を補う方法です。

f:id:Pimientito:20190701232125j:plain
学習期間を増やしていくパターン


 上図「学習期間を増やしていくパターン」の場合は、以下のように学習データを増やしながら、学習・検証しています。


  • 検証1回目:学習データ(1月~6月)、検証データ(7月~8月)

  • 検証2回目:学習データ(1月~8月)、検証データ(9月~10月)

  • 検証3回目:学習データ(1月~10月)、検証データ(11月~12月)



 この方法にも問題はあり、検証ごとに学習データ量が異なるため、モデルの精度を正確に把握することは難しく、そのため、データ量とモデル精度の関係も同時に把握する必要があるとのことです。


 データとモデルの精度の相関関係を知る方法については、まだ学習経験が無く、手順が不明なため、今回は割愛します。


サンプルコード[Python]

 参考資料によると、Pythonには、時系列データを分割処理するライブラリが、いまのところ無いため、自身でコーディングする必要があるそうです。ここでは、参考資料のサンプルコードを通して処理や動作を確認します。


サンプルデータセットを作成

 今回のサンプルデータセットの内容は、以下の通りです。

カラム名 内容 レコード件数
T_Data YYYY-mm-dd形式の日付型データ 731
Num1 np.random.randで生成した実数値データ 731
Num2 np.random.randで生成した実数値データ 731


 日付型データは、2019-01-01から2020-12-31の二年間731日分のデータです。2020年が閏年のため、レコード数が731件になります。


 実数型データのNum1とNum2は、筆者自身が、あまりNumPyライブラリに慣れていないため、練習として作成しました。カラム名や数値自体には、特に意味はありません。


 データセットを作成するコードは、以下の通りです。

#  【前処理の学習-32】データを学ぶ ~分割~②
# テストテーブル作成

import pandas as pd
import numpy as np

# 日付データの作成方法
tbl_lesson32 = pd.DataFrame({
    'T_Date' : pd.date_range('2019-01-01', '2020-12-31'), 
    'Num1' : np.random.rand(731), 
    'Num2' : np.random.rand(731)})


 作成されたデータセットの内容を、head関数と、info関数で確認します。

f:id:Pimientito:20190703214032p:plain
データの確認(head関数、info関数)


 日付型データと実数型データが作成されました。続いて参考資料のサンプルコードから時系列データを分割する方法を学びます。コーディングする前に、処理の流れを掴むためにイメージ図を作成しました。

f:id:Pimientito:20190704002034j:plain
サンプルコードの処理の流れ(時系列データの分割)


 前述した図「学習期間を一定にするパターン」「学習期間を増やしていくパターン」では、月単位のデータで学習・検証をしていましたが、サンプルコードでは、レコード単位でスライドしています。


 サンプルコードを参考に、再作成したコードは、以下の通りです。

# 前処理大全 5-2「時系列データにおけるモデル検証用のデータ分割」
# サンプルコード python_awesome.py(p.144-145)を参考に作成

# カウンター初期化
counter = 1

#  学習データの開始行番号
train_start = 0

# 学習データの終了行番号
train_end = 24

# 検証データ件数
test_range = 12

# スライドするデータ件数
slide_rows = 12

# 年月に基づいてデータを並び替え
tbl_lesson32.sort_values(by='T_Date')

while True:
    # 検証データの終了行番号
    test_end = train_end + test_range
    
    # 行番号を指定して、元データから学習データを抽出
    train = tbl_lesson32[train_start : train_end]
    print('train : ' + str(counter))
    print(train)
    print('')
    
    # 行番号を指定して、元データから検証データを抽出
    test = tbl_lesson32[train_end : test_end]
    print('test : ' + str(counter))
    print(test)
    print('')
    
    # 検証データの終了行番号が元データの行数以上になっているか判定
    if test_end > len(tbl_lesson32.index):
        
        # 全データを対象にした場合、終了
        break
        
    # データをスライドさせる
    train_start += slide_rows
    train_end += slide_rows
    
    # カウンター加算
    counter += 1
    
# --- これ以降は、交差検定の結果をまとめるコード ---


 実行結果は、以下の通りです。

f:id:Pimientito:20190703221604p:plain
時系列データ分割一周目(学習データ・検証データ)


 左端のインデックス番号を追うと、レコード単位で移動していることが分かります。次の結果は、二周目の学習データと検証データです。


f:id:Pimientito:20190703221708p:plain
時系列データ分割二周目(学習データ・検証データ)


 学習データと検証データの間に位置するレコード(赤枠部分)の抽出は、注意が必要です。Pythonのスライシング処理では、インデックスの指定を誤ると意図しないところでデータが欠落してしまいます。以下で、スライシングの動作について、少し詳しく学習します。


スライシング

 スライシングとは、[ : ]を使って配列データ(*)から部分抽出する方法です。抽出位置の指定方法は、以下の通りです。(*) 二文字以上の文字列も配列データです。


配列データA [ 抽出開始位置:抽出終了位置]


 ここで、スライシングのすべてをご紹介できませんが、連続したデータを抽出する際に、特に注意が必要な点について学習します。


 例えば、N個ある連続データを二つ以上の変数に分割する時には、以下のような手順でコーディングすると思います。



  • 変数A ⇨ Index[0] 〜 Index[X]

  • 変数B ⇨ Index[X + 1] 〜 Index[N]



 しかし、Pythonのスライシングでは、上記の指定では、変数A、B共に、それぞれ意図している最後尾のデータが欠落してしまいます。スライシングは、指定した開始位置から終了位置ー1までしか抽出されません。


 意図した範囲のデータを抽出する場合、末尾のインデックスに「+1」したインデックスを指定します。スライシングの範囲指定のイメージは、以下の通りです。

f:id:Pimientito:20190713221637j:plain
スライシングのインデックス指定のイメージ図


 イメージ図と同じ内容で、サンプルコードを作成しました。実際にコードを実行しながら、結果を確認します。はじめにデータを作成します。

#  スライシングの動作確認
import pandas as pd

# 日付型サンプルデータの作成
df = pd.DataFrame({
    'date' : pd.date_range('2019-01-01', '2019-01-16')})

# データの確認
df[:]


 作成したデータの内容は、以下の通りです。

f:id:Pimientito:20190713224109p:plain
スライシング動作確認(データ)


 続いてイメージ図と同じように2019-01-01〜2019-01-10のデータを抽出します。2019-01-01のインデックスは「0」、2019-01-10のインデックスは「9」を指しています。

# 学習データ部分抽出の確認 - 本来のインデックス番号の指定 -
df[0:9]


 指定したインデックスで抽出されたデータは、以下の通りです。

f:id:Pimientito:20190713224618p:plain
スライシング動作確認 -その1-


 いままで述べてきた通り、意図した範囲のデータではありませんでした。次に終了位置のインデックスに「+1」した値で指定します。

# 学習データ部分抽出の確認 - インデックス番号+1の指定 -
df[0:10]


 インデックス「10」のデータは「2019-01-11」ですが、抽出できたデータは、2019-01-01〜2019-01-10でした。確認した結果は、以下の通りです。

f:id:Pimientito:20190713225514p:plain
スライシング動作確認 -その2-


 スライシングを使った部分抽出の学習は、ここまでです。


 言語の仕様を正確に把握していないことで、コーディングミスと混同する恐れもあることを、あらためて学びました。これからも言語の細かい仕様について、少しずつ学習を続けたいと思います。


今回の学習

 「今回のテーマ」の内容を踏まえて、最初にテストデータの作成を行います。まず、どのようなデータが、今回の学習に適しているのか検討します。


テストデータの概要

条件

 以下の条件を満たしているデータが、今回の学習に適していると考えます。

  • 時系列データを含んでいること

  • データのボリュームが年単位であること


テストデータの選択

 上記「条件」の内容をもとに考えたところ、すぐに頭に浮かぶのは、本ブログのスタート時から、何度もお世話になっている「気象データ」です。今回も、この気象データを使用して学習します。

f:id:Pimientito:20190707233114p:plain
国土交通省 気象庁「過去の気象データ・ダウンロード」ホームページ


テストデータの加工

 今回は、時系列データの分割について学んでいるため、時系列データのカラム以外は、最小限のカラムに抑えました。データの内容は、以下の通りです。

項目 概要
カラム 年、月、日、日平均気温
範囲 2017/01/01〜2019/07/06
地域 東京
1レコード単位


 気象庁のサイトからダウンロードしたデータを読み込みます。

#【前処理の学習-32】データを学ぶ ~分割~②
# 前処理大全 5-2「時系列データにおけるモデル検証用のデータ分割」

import pandas as pd

# データの読み込み(読み込み開始行:5〜、読み込み対象列:0(年), 1(月), 2(日), 3(日平均気温))
df = pd.read_csv('./data/lesson32_sample_data.csv', encoding='shiftjis', usecols=[0, 1, 2, 3], skiprows=4)
df.columns = ['Year', 'Month', 'Day', 'Temp_mean']


 読み込んだデータの内容は、以下の通りです。

f:id:Pimientito:20190707235143p:plain
データの確認


 ダウンロードしたデータでは、時系列データの「年」「月」「日」が、個々のカラムに別れているため、ひとつのカラムにまとめます。

# データの整形
tbl_lesson32 = \
        pd.DataFrame({'Date' : pd.to_datetime(df[['Year', 'Month', 'Day']], format='%Y%m%d'), 
                      'Temp_mean' : df['Temp_mean'] })


 まとめた結果は、以下の通りです。

f:id:Pimientito:20190707235815p:plain
時系列データ「Date」の確認


 今回のテストデータの加工は、とても簡単ですが、これで準備が整いました。


テストデータを使って学習

 「今回のテーマ」では「月単位」で、データを分割していました。一方、参考資料のサンプルコードでは「レコード単位(日単位)」でスライドする方法でした。ここでは「月単位」で、学習データと検証データを分割する方法のコーディングに挑戦します。


 今回のコーディングでは、時系列データを学習データ・検証データに分割する部分を関数化しました。ただし、学習・検証する処理は、ダミー処理として集約関数を使用しています。


 また実用的なコードのように、一般的な入力チェックやエラーチェックは考慮していません。


自作関数「DateTypeData_Validation」

 関数の引数や戻り値は、以下の通りです。

パラメータ 概要
tbl データテーブル(データフレーム)
vl_start 検証開始年月日
vl_end 検証終了年月日
train_horizon 学習期間(月単位)
test_horizon 検証期間(月単位)
list_col カラムリスト
key_col ソート対象カラム
slide_flag スライド式(True)/学習期間増加式(False)
戻り値 無し


 今回の自作関数の処理の流れは、以下の通りです。

f:id:Pimientito:20190714153107j:plain
自作関数「DateTypeData_Validation」の処理の流れ


 実際のコードは、以下の通りです。なお、処理結果を確認するためのデバッグコードは残したままにしています。

def DateTypeData_Validation(tbl, vl_start, vl_end, train_horizon, test_horizon, list_col, key_col, slide_flag=True):
    
    # Pandasライブラリ
    import pandas as pd
    
    # 日付型データ操作用関数
    from pandas.tseries.offsets import Day, MonthEnd
    
    # 変数初期化
    # ループカウンター
    lp_cnt = 1
    
    # 学習データ開始位置
    train_start_point = pd.to_datetime(vl_start)
    
    # 学習データ終了位置
    train_end_point = train_start_point + MonthEnd(train_horizon)
    
    # 検証データ開始位置
    test_start_point = train_end_point + 1 * Day()
    
    # 検証データ終了位置
    test_end_point = test_start_point + MonthEnd(test_horizon)
    
    # 文字列型日付を、日付型に変換
    tbl[key_col] = pd.to_datetime(tbl[key_col])
        
    # 引数sort_keyに基づいてデータを並び替え
    tbl.sort_values(by=key_col)
    
    # 時系列型データの検証
    while True:
        
        # スライド式の場合
        if slide_flag :
            
            # 学習データ抽出
            train_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= train_start_point) & \
                                 (pd.to_datetime(tbl[key_col].values) <= train_end_point), : ]
            #print(train_data.values)
                        
            # 検証データ抽出
            test_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= test_start_point) & \
                                (pd.to_datetime(tbl[key_col].values) <= test_end_point), : ]
            
            # データ抽出位置更新
            # 学習データ開始位置
            train_start_point = train_start_point + MonthEnd(test_horizon) + 1 * Day()
            
            # 学習データ終了位置
            train_end_point = train_start_point + MonthEnd(train_horizon)
            
            # 検証データ開始位置
            test_start_point = train_end_point + 1 * Day()
            
            # 検証データ終了位置
            test_end_point = test_start_point + MonthEnd(test_horizon)
        
        # 学習期間増加式の場合
        else :
            
            # 学習データ抽出
            train_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= train_start_point) & \
                                 (pd.to_datetime(tbl[key_col].values) <= train_end_point), : ]
            
            # 検証データを抽出
            test_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= test_start_point) & \
                                (pd.to_datetime(tbl[key_col].values) <= test_end_point), : ]
                        
            # データ抽出位置更新 
            # 学習データ開始位置(固定のため、更新不要)
            
            # 学習データ終了位置
            train_end_point = train_end_point + MonthEnd(test_horizon)
            
            # 検証データ開始位置
            test_start_point = train_end_point + 1 * Day()
            
            # 検証データ終了位置
            test_end_point = test_start_point + MonthEnd(test_horizon)
        
        
        # 検証
        # 学習データ
        train_result = \
            train_data.groupby([train_data[key_col].dt.year, train_data[key_col].dt.month]).agg({list_col[1] : ['count', 'mean', 'std']})
        
        # 結果の表示(デバッグ用)
        print('')
        print('train_result : ' + str(lp_cnt))
        print('--------------info--------------------')
        print(train_result.info())
        print('--------------values-----------------')
        print(train_result.values)
        print('----------------------------------')


        # 検証データ
        test_result = \
            test_data.groupby([test_data[key_col].dt.year, test_data[key_col].dt.month]).agg({list_col[1] : ['count', 'mean', 'std']})

        # 結果の表示(デバッグ用)
        print('')
        print('test_result : ' + str(lp_cnt))
        print('--------------info--------------------')
        print(test_result.info())
        print('--------------values-----------------')
        print(test_result.values)
        print('----------------------------------')
        print('')
        
           
        # 終了判定(次回のテストデータ終了位置が、指定範囲を超えている場合)
        if test_end_point  > pd.to_datetime(vl_end) : 
            
            print('End of Cross_Validation.')
            break
        
        # ループカウンター
        lp_cnt += 1
        


 関数の呼び出し元は、以下の通りです。

# 関数の呼び出し
DateTypeData_Validation(tbl_lesson32, '20180101', '20181231', 4, 2, tbl_lesson32.columns, 'Date', True)


 パラメータ値は、以下の通りです。

パラメータ
tbl tbl_lesson32
vl_start 2018/01/01
vl_end 2018/12/31
train_horizon 4(ヶ月)
test_horizon 2(ヶ月)
list_col tbl_lesson32.columns
key_col Date
slide_flag True(スライド式)


 「slide_flag」で、Trueを指定すると「スライド式」、Falseを指定すると「学習期間増加式」の検証に切り替わります。


  ここからは、この関数を各パートに分けて見て行きます。

抽出範囲の指定(スライド式)

 スライド式でデータを抽出する部分です。今回のコーディングでは、loc関数で抽出範囲を指定しています。スライド式のイメージは、以下の通りです。

f:id:Pimientito:20190714002519j:plain
スライド式イメージ図


 データ抽出後、次のループのために、抽出開始位置と終了位置の値を更新します。

# 学習データ抽出
train_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= train_start_point) & \
                    (pd.to_datetime(tbl[key_col].values) <= train_end_point), : ]
                        
# 検証データ抽出
test_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= test_start_point) & \
                   (pd.to_datetime(tbl[key_col].values) <= test_end_point), : ]

# データ抽出位置更新
# 学習データ開始位置
train_start_point = train_start_point + MonthEnd(test_horizon) + 1 * Day()
            
# 学習データ終了位置
train_end_point = train_start_point + MonthEnd(train_horizon)
            
# 検証データ開始位置
test_start_point = train_end_point + 1 * Day()
            
# 検証データ終了位置
test_end_point = test_start_point + MonthEnd(test_horizon)


抽出範囲の指定(学習期間増加式)

 学習期間増加式でデータを抽出する部分です。こちらもスライド式同様、loc関数を使用して抽出範囲を決めています。


 学習期間増加式の場合、学習データの開始位置は固定にしています。検証が完了して、次のループ時には、前回ループ時の検証データ抽出範囲を学習データに追加します。


 学習期間増加式のイメージは、以下の通りです。

f:id:Pimientito:20190714003026j:plain
学習期間増加式イメージ図


 学習データの開始位置は固定のため、コーディングは不要ですが、コードの該当場所には、コメントを残しておきます。

# 学習データ抽出
train_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= train_start_point) & \
                    (pd.to_datetime(tbl[key_col].values) <= train_end_point), : ]
            
# 検証データを抽出
test_data = tbl.loc[(pd.to_datetime(tbl[key_col].values) >= test_start_point) & \
                  (pd.to_datetime(tbl[key_col].values) <= test_end_point), : ]
                        
# データ抽出位置更新 
# 学習データ開始位置(固定のため、更新不要)
            
# 学習データ終了位置
train_end_point = train_end_point + MonthEnd(test_horizon)
            
# 検証データ開始位置
test_start_point = train_end_point + 1 * Day()
            
# 検証データ終了位置
test_end_point = test_start_point + MonthEnd(test_horizon)


学習と検証

 続いて、学習・検証のパートとなります。筆者は、まだ「予測モデル」を作成したことがないため、今回はダミー処理として、groupby関数と集約関数(count、mean、std)を実行しています。

# 検証
# 学習データ
train_result = \
    train_data.groupby([train_data[key_col].dt.year, train_data[key_col].dt.month]).agg({list_col[1] : ['count', 'mean', 'std']})
        
# 検証データ
test_result = \
    test_data.groupby([test_data[key_col].dt.year, test_data[key_col].dt.month]).agg({list_col[1] : ['count', 'mean', 'std']})
           


 groupby関数では「年」と「月」でグループ化しているため、複数年のデータで学習・検証を行なっても、月毎にデータが混ざることがありません。また「月」だけを指定して学習させれば、季節の傾向を学習・検証できるかもしれません。


 agg関数の引数では、具体的なカラム名ではなくカラムリストを使用しています。その理由は、関数内で特定のカラム名を指定することを避けたかったためです。


 しかし、同じ関数内で、カラムリストのインデックスは、1で固定してしまっているところが不完全な点です。まだまだ、改善の余地が多い関数です。


終了判定

 ループの終了条件は、以下の通りです。

# 終了判定(次回の検証データ終了位置が、指定範囲を超えている場合)
if test_end_point  > pd.to_datetime(vl_end) : 
            
    print('End of Cross_Validation.')
    break
        


 次回のループ時に、検証データの終了位置が、関数パラメータ「vl_end」を越えてしまう場合、学習・検証を終了させています。


 大まかですが、この関数の処理の流れについて説明しました。次に、この関数を実行した結果を確認します。


実行結果の確認

 はじめに「スライド式」の実行結果の一部を確認します。ループ1回目の結果は、以下の通りです。

f:id:Pimientito:20190714124215p:plain
検証1回目(スライド式)


 ループ2回目の結果を見ると、train_result、test_resultのそれぞれ「MultiIndex」の値が、ループ1回目の結果から2ヶ月分スライドしていることが確認できます。

f:id:Pimientito:20190714124305p:plain
検証2回目(スライド式)


 続いて「学習期間増加式」の実行結果の一部を確認します。

f:id:Pimientito:20190714124526p:plain
検証1回目(学習期間増加式)


 ループ2回目のtrain_resultの「MultiIndex」が、ループ1回目の4ヶ月分から、6ヶ月分に増えていることが確認できました。


 また「values」の結果もループ1回目より2回目の方が行数が増えていることも確認できます。

f:id:Pimientito:20190714124713p:plain
検証2回目(学習期間増加式)


 すべての結果を載せることはできませんが「スライド式」「学習期間増加式」共に、正常に動作していることが確認できました。


 関数の見直す点は、まだまだ多くありますが、今回の学習では、ここまでとします。


可視化に挑戦

 今回作成した自作関数「DateTypeData_Validation」の一部を変更して、可視化のためのデータを作成します。変更したコード部分のみ記載します。


 はじめに、結果の値を代入する変数train_resultとtest_resultをリスト型変数に変更します。

# リスト変数作成
train_result = []
test_result = []


 続いて学習・検証結果をリスト変数にappend関数で追加するようにコードを変更します。また複数の数値を同時に管理・操作することが、いまの筆者には、まだ難しいため、集約関数はstd(標準偏差)のみに絞りました。

# 検証
# 学習データ
train_result.append(train_data.groupby([train_data[key_col].dt.year, train_data[key_col].dt.month]).agg({list_col[1] : ['std']}).reset_index(drop=True))

# 検証データ
test_result.append(test_data.groupby([test_data[key_col].dt.year, test_data[key_col].dt.month]).agg({list_col[1] : ['std']}).reset_index(drop=True))        


 ループの終了判定の部分を、break文からreturn文に変更して、各結果が格納されているリスト変数train_resultとtest_resultを戻り値に設定します。

# 終了判定(次回の検証データ終了位置が、指定範囲を超えている場合)
if test_end_point  > pd.to_datetime(vl_end) : 
    print('End of Cross_Validation.')
    return train_result, test_result


 最後に、関数の戻り値を受ける変数を、関数呼び出し元に設定します。

# 関数の呼び出し
train_result, testDate = DateTypeData_Validation(tbl_lesson32, '20180101', '20181231', 4, 2, tbl_lesson32.columns, 'Date', True)


 変更したコードを実行した結果は、以下の通りです。(train_resultの場合)

f:id:Pimientito:20190714222700p:plain
学習結果(std関数)の確認(4回分)


 学習結果の数値をプロット図にするため、リスト型からNumPy型配列へ変換します。

# list型をNumPy型配列に変換
import numpy as np

np_train_result = \
    np.array([train_result[x].values for x in range(len(train_result))])

# 変換結果の確認
np_train_result


 変換した結果は、以下の通りです。

f:id:Pimientito:20190714223106p:plain
list型からNumPy型配列に変換した結果


 NumPy型二次元配列変数をインデックス指定で、値を表示させて確認します。(インデックス[0]と[1]のみ)

f:id:Pimientito:20190714223525p:plain
インデックス指定でデータを確認


 最後にプロット図を作成します。グラフは、折れ線グラフで表示させました。表示内容は、ダミー処理の標準偏差の値を使用しています。(表示内容に意味はありません。)

# 可視化ライブラリ
import matplotlib.pyplot as plt

# figureオブジェクトの初期化
fig = plt.figure()

# サブプロット図の関連付け
ax1=fig.add_subplot(2, 2, 1)
ax2=fig.add_subplot(2, 2, 2)
ax3=fig.add_subplot(2, 2, 3)
ax4=fig.add_subplot(2, 2, 4)

# train_resultの結果の関連付け
ax1.plot(np_train_result[0], 'go--')    # 緑色表示
ax2.plot(np_train_result[1], 'ro--')    # 赤色表示
ax3.plot(np_train_result[2], 'yo--')    # 黄色表示
ax4.plot(np_train_result[3], 'bo--')    # 青色表示

# サブプロット図ごとにタイトル表示
ax1.set_title('train_result[0]')
ax2.set_title('train_result[1]')
ax3.set_title('train_result[2]')
ax4.set_title('train_result[3]')

# サブプロット図周辺の空白調整
plt.subplots_adjust(wspace=0.3, hspace=0.5)

# プロット図表示
plt.show()


 表示したプロット図は、以下の通りです。

f:id:Pimientito:20190714224140p:plain
train_resultの結果をプロット図化


 今回のプロット図からは、何の推論も立たないですが、自身で作成した予測モデルを検証して行く過程を、ほんの少し体験できたような気分になれました。


今回のまとめ

 前回と今回の二回で、データの分割を学びました。学習に入る前に考えていたデータ分割とは意味が異なり、非常に戸惑うことが多かったように思います。


 第一に、予測モデルのための学習データ・検証データの分割については、教科書レベルでは知っていましたが、実際に予測モデルを作成したことがない筆者にとっては、データ分割後の処理の方が気になってしまい、いろいろと参考資料を漁ってみるものの、もう少し先で学ぶ内容が多く、結果的に、いままで学んだgroupby関数や集約関数を使ってダミー処理に落ち着くといった手戻りが多いテーマでした。


 また、データを分割するライブラリや関数は、Pythonには、まだ少ないようで、R言語で実現できる機能をコーディングする必要があり、そのためには、Pythonの仕様をより深く理解しなければならないことを痛感しました。


 本ブログを始めて一年が経過しましたが、「機械学習」を実務に活かすには、まだまだ何も理解しておらず、また何かを開発することもできない己の現実を知り、ため息つく日もありますが、まずは自分が選んだこの一冊「前処理大全」を読み切るまでは「機械学習」の学習を続けていきたいと思います。


 一年間、本当にありがとうございました。また、次の一年も、よろしくお願いいたします。



 今回は、以上です。



【参考資料】

国土交通省 気象庁「過去の気象データ・ダウンロード」

www.data.jma.go.jp



本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。



お問い合わせ先:otoiawase@handson-t-arte.com


【前処理の学習-31】データを学ぶ ~分割~①

 前回まで五回に渡り、データの「結合」について学びました。

pimientito-handson-ml.hatenablog.com

 今回からデータの「分割」について学びます。


【今回の目標到達点】

 交差検証(クロスバリデーション)のためのデータ分割を学ぶ


【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「分割」の概要

 参考資料「前処理大全」第5章「分割」の冒頭で、著者はデータの分割について、以下のように述べています。

 データの分割は予測モデルを評価する際に必要になる前処理です。主に学習データ(予測モデルを構築する際に利用するデータ)と検証データ(モデルの精度を測定するためのデータ)の分割に利用されます。


 学習データと検証データは、必要とする列データは同じです。「予測モデルに入力するための列データ」と「予測モデルの予測対象の列データ」です。そのため、学習データと検証データに対して適用する前処理はすべて同じです。

参考・参照元:第5章「分割」(p.130)より抜粋


 資料によって、データの名称が異なるため、以下の表に代表的な名称をまとめました。

名称 使用場面
学習データ
訓練データ
レーニングデータ
訓練セット
Train Data
予測モデル構築時
検証データ
評価データ
テストデータ
テストセット
Test Data
予測モデル精度測定(評価)時


 このほかにも、本番の予測に使う「本番データ」や「適用データ」と呼ばれるものや、モデル構築後の性能評価のためだけに使用するデータなどもあります。


 筆者のような機械学習初学者にとっては、単なる名称だけでも迷ってしまいますが、以下のような例をイメージすると、理解しやすいかもしれません。

使用場面 高校野球のボールに例えると
モデル構築時
(特徴量設計)
放課後の部活用ボール
(使い込んでボロボロ)
モデル構築時
(モデルテスト)
練習試合用ボール
(たまに使うのできれい)
モデル検証
または本番
公式戦用ボール
(新品)


 少し変な例えですが、データの名称については、あまり神経質に捉える必要はなく、予測モデルが完成するまでに、自分自身が、どの位の「トライ&エラー」を行なうのか計算した上で、使用するデータを分割し、保存すれば良いのではと思います。


 今回、第5章「分割」では、データの分割について、以下、二つの方法が紹介されています。


  • レコードデータにおけるモデル検証用のデータ分割

  • 時系列データにおけるモデル検証用のデータ分割



 次の章から、具体的にデータの分割を学びます。


今回のテーマ

 レコードデータにおけるモデル検証用のデータ分割


概要

 参考資料 5-1「レコードデータにおけるモデル検証用のデータ分割」では、交差検証(クロスバリデーション)に使用するデータの分割方法について紹介されています。


 筆者は、まだ自身で「予測モデル」を作成した経験がありません。今回の学習をはじめる前に、機械学習の基本的なことについて、少し触れたいと思います。


モデル

 はじめに予測モデル・機械学習モデルの「モデル」とは、一体何を指しているのでしょうか。以下、引用します。

機械学習では、学習の結果得られた法則性を表すものを、一般的に「モデル」と呼びます。機械学習モデルは適用するアルゴリズムごとに異なり、それぞれ何らかの数式やデータ構造と、その中に含まれる変更可能なパラメータの値の集合で表現されます。

参考・参照元:データサイエンティスト養成読本 機械学習入門編(技術評論社) 第1部 特集1 「機械学習を使いたい人のための入門講座」(比戸将平氏著)第1章 機械学習の概要 「モデル」より抜粋


 モデルとは、値を入力することで、ある結果を導き出す(予測する/分類する)コードや、数式のアルゴリズムのことを指す言葉のようです。以下は、イメージ図です。


f:id:Pimientito:20190615182702j:plain
機械学習モデルイメージ図


モデルの性能評価

 モデルの性能評価の概要について調べてみました。以下、引用します。

学習に用いるデータ集合Dは観測データxとその正解ラベルyからなる.機械学習では、Dを使って未知の観測データのラベルを予測するシステム、アルゴリズムを学習するので、学習結果のシステム性能を評価する必要がある.これは、(1)種々のアルゴリズムを比較してどのアルゴリズムが優位であるかを示すため、(2)実データに適用したときどの程度の性能を示すか目処をつけるためである.

参考・参照元東京大学工学教程 情報工学 機械学習(中川裕志氏著 丸善出版) 1.5 「評価方法」(p.16)より抜粋


 「正解ラベル」による性能評価は「教師あり学習」「教師なし学習」「強化学習」の、すべてで使用できる性能評価なのでしょうか。残念ながら、もう少し先のお話しになってしまうため、今回は割愛します。


ホールドアウト法

 予測モデルの検証方法のひとつ。以下、引用します。  

ホールドアウト法は、機械学習のモデルの汎化性能を評価するために従来より使用されている一般的なアプローチである。

参考・参照元:達人データサイエンティストによる理論と実践 Python 機械学習プログラミング[第2版](インプレス) 6.2.1 ホールドアウト法(p.185)より抜粋


 従来のホールドアウト法は、元のデータセットをモデルの「トレーニング用」と「性能評価用」に分割しますが、性能評価用のデータでも繰り返し使用すれば、トレーニング用データと同様であるという考えから、同著「達人データサイエンティストによる理論と実践 Python 機械学習プログラミング[第2版]」では、以下、三つのデータに分ける方法を紹介しています。


データセット 使用場面
レーニングデータセット 様々なモデルの学習に使用
検証データセット モデル選択時に使用
テストデータセット 最終的な性能評価


 三つに分割されたデータを使用した場合のホールドアウト法の流れは、以下の通りです。

f:id:Pimientito:20190601122024j:plain
予測モデル構築時の分割データ使用場面


 予測モデル構築時に繰り返し使用するデータは「トレーニングデータ」と「検証データ」です。「テストデータ」を繰り返し使用し、入力値(ハイパーパラメータ)の調整を行なうことは、モデルの「過学習」に繋がります。


過学習(Over-learning)/過適合(Over-fitting)

 以下、引用します。

正解ラベル付きの観測データ集合Dから学習した回帰や分類の予測式がDに精度高くあてはまるが、一方で新規データの予測精度は必ずしもよくないという現象を過学習あるいは過適合と呼ぶ.

参考・参照元東京大学工学教程 情報工学 機械学習(中川裕志氏著 丸善出版) 4.1 「過学習」(p.69)より抜粋


 過学習と学習不足、または妥当な性能のモデルの評価例を、簡単な図にすると、以下のようになります。

f:id:Pimientito:20190607000133j:plain
決定境界による学習不足と過学習


交差検証(k分割交差検証)

 最も有名な予測モデルの検証方法。以下、引用します。

最もメジャーなモデルの検証方法は、交差検証(クロスバリデーション)です。交差検証では、データをいくつかに分割し、その分割した1つのデータ群をモデルの評価用のデータとして利用し、その他のデータ群でモデルの学習を行ないます。すべてのデータ群が一度だけ評価用のデータとして採用されるように、データ群の個数分繰り返して精度測定を行い、モデルを評価します。

参考・参照元:「前処理大全」5-1「レコードデータにおけるモデル検証用のデータ分割」(p.131)より抜粋


  交差検証(クロスバリデーション)の流れは、以下の通りです。

f:id:Pimientito:20190601215316j:plain
交差検証の流れ


 上図では、各交差検証の精度測定を「平均二乗誤差」で算出し、最後に、すべての交差検証の結果を合算して平均値を算出しています。


 しかし「交差検証」という方法では、すべて平均二乗誤差の平均を使ってモデルの精度測定を行なうのでしょうか。残念ながら、いまは、その答えが分かりません。今後、学習を続けるなかで学ぶ必要があります。


 なお、この図からは、ホールドアウト法の説明図にあった「テストデータ(最終性能評価用)」のようなものが存在しません。学習データと検証データを繰り返し使用することを考えると、最終性能評価用データを、訓練前に別途分割しておくと良いかもしれません。


 まだまだ機械学習の基礎知識のお話しは尽きませんが、学習を進めます。


サンプルコード[Python]

 今回のサンプルコード(python_awesome.py(p.136-138))では、ホールドアウト法と交差検証(クロスバリデーション)法を組み合わせています。処理の流れは、以下の通りです。


  1. ホールドアウト検証用関数で、トレーニングデータと、テストデータを分割

  2. レーニングデータを、交差検証(クロスバリデーション)用関数で、トレーニングデータと、検証データに分割



 サンプルコードを使用して、動作確認を行ないます。


サンプルデータセットを作成

 動作確認を行なう前に、サンプルデータセットを作成します。

#前処理大全 5-1「レコードデータにおけるモデル検証用のデータ分割」
#サンプルコード python_awesome.py(p.136-138)を参考に作成
#テストテーブル作成
import pandas as pd

tbl_lesson31 = \
pd.DataFrame({'name' : ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'], 
              'number' : range(1, 27), 
              'double' : [x * 2 for x in range(1, 27)], 
              'square' : [x**2 for x in range(1, 27)], 
              'cube' : [x**3 for x in range(1, 27)]})


 今回は、データ分割の動作確認を行ないやすいようにレコード数の少ないテーブルを作成しました。データの内容は、以下の通りです。

f:id:Pimientito:20190602224043p:plain
tbl_lesson31の内容


 各データの内容は、以下の通りです。

カラム名 内容 レコード件数
name 小文字アルファベット(a-z) 26
number 1-26までの数字 26
double 1-26の数字に2を乗算 26
square 1-26の数字の二乗 26
cube 1-26の数字の三乗 26


ホールドアウト法によるデータ分割

 train_test_split関数を使用してトレーニングデータ、テストデータに分割します。

 またトレーニングデータ、テストデータを、それぞれ説明変数と目的変数(応答変数)に分割します。

データ名 変数種別 カラム名 割合(%)
レーニングデータ 説明変数 name,
number,
double,
square
80
レーニングデータ 目的変数 cube 80
テストデータ 説明変数 name,
number,
double,
square
20
テストデータ 目的変数 cube 20


 説明変数・目的変数とは・・・


  • 説明変数とは、予測に使用する値

  • 目的変数とは、予測対象の値



 今回は、トレーニングデータを全体の80%、テストデータは全体の20%の割合で分割します。分割の割合は、test_sizeパラメータに小数点形式で指定します。

#sklearnライブラリ(ホールドアウト法)
from sklearn.model_selection import train_test_split

#ホールドアウト法でデータを分割
dt_training, dt_test, target_training, target_test = \
    train_test_split(tbl_lesson31.drop('cube', axis=1), tbl_lesson31[['cube']], test_size=0.2)

#分割後のデータのインデックスを振り直し
dt_training.reset_index(inplace=True, drop=True)
dt_test.reset_index(inplace=True, drop=True)
target_training.reset_index(inplace=True, drop=True)
target_test.reset_index(inplace=True, drop=True)


 train_test_split関数の概要は、以下の通りです。

f:id:Pimientito:20190602180643p:plain
train_test_split関数の説明


 分割したデータは、以下の通りです。(分割される行や順番は、分割する度に異なります)

f:id:Pimientito:20190602225847p:plain
レーニングデータ(説明変数、80%)


f:id:Pimientito:20190602225936p:plain
レーニングデータ(目的変数、80%)


f:id:Pimientito:20190602230024p:plain
テストデータ(説明変数、20%)


f:id:Pimientito:20190602230059p:plain
テストデータ(目的変数、20%)


交差検証(クロスバリデーション)法によるデータ分割

 次にホールドアウト法で分割したトレーニングデータ(元データの80%)を、交差検証(クロスバリデーション)で使用します。


 はじめにレコード件数をrange関数の範囲として利用し、行番号リストを作成します。

#sklearnライブラリ(クロスバリデーション)
from sklearn.model_selection import KFold

#80%に分割したトレーニングデータで交差検証を実施
#トレーニングデータのレコード件数を行番号リストとして使用
row_number = list(range(len(target_training)))


 実行結果は、以下の通りです。

f:id:Pimientito:20190602231811p:plain
行番号リストの表示


 次に、KFold関数を使用して、交差検証の初期設定を行ないます。

#交差検証用データの分割(=交差検証回数の指定)
# レコード20件を5分割(4レコード × 5回分(トレーニング:4回分(16レコード)、検証:1回分(4レコード)))
k_fold = KFold(n_splits=5, shuffle=True) 


 交差検証で分割するデータの内訳は、以下の通りです。

項目 数量
元データレコード件数 20
1データ分割内のレコード件数 4
データ分割数 5
学習回数 4
検証回数 1


 k_foldオブジェクトの設定内容は、以下の通りです。

f:id:Pimientito:20190602233941p:plain
k_foldオブジェクトの設定内容


 KFold関数のパラメータは、以下の通りです。

パラメータ名 初期値 概要
n_splits 3 データ分割数(学習データ+検証データ)
random_state None データシャッフル時の乱数値
shuffle False 分割前データのシャッフル


 KFold.split関数を使用して、データを分割します。

 (以下のサンプルコードでは、処理内容の見える化を行なうため、print()文を多く含んでいます)

#ループカウンター
counter = 1

#交差検証繰り返し処理(並列処理も可能な部分)
for train_crossval_no, test_crossval_no in k_fold.split(row_number):
    
    print('loop count : ' , counter)
    print('train_crossval_no : ', train_crossval_no)
    print('test_crossval_no : ', test_crossval_no)
    print('')
    
    #交差検証における学習データを抽出
    train_crossval = dt_training.iloc[train_crossval_no, : ]
    
    #データの表示(train_crossval)
    print('train_crossval : ', counter)
    print(train_crossval)
    print('')
    
    #交差検証における検証データを抽出 ※トレーニングデータから取得
    test_crossval = dt_training.iloc[test_crossval_no, : ]
    
    #データの表示(test_crossval)
    print('test_crossval : ', counter)
    print(test_crossval)
    print('')
    print('-------')
    
    #ループカウンター インクリメント
    counter +=1


 分割したデータを、以下の内容で表示しました。

 (5回分のfor文実行結果)

表示名 表示内容
loop count for文の回数
train_crossval_no レーニングデータとして
抽出された行番号
test_crossval_no 検証データとして
抽出された行番号
train_crossval:n n回目のトレーニングデータ
(説明変数)の内容
test_crossval:n n回目の検証データ
(説明変数)の内容


f:id:Pimientito:20190602235832p:plain
交差検証のためのデータ分割 1回目


f:id:Pimientito:20190602235925p:plain
交差検証のためのデータ分割 2回目


f:id:Pimientito:20190602235955p:plain
交差検証のためのデータ分割 3回目


f:id:Pimientito:20190603000023p:plain
交差検証のためのデータ分割 4回目


f:id:Pimientito:20190603000054p:plain
交差検証のためのデータ分割 5回目


 分割されるトレーニングデータ、または検証データの内容が、毎回異なることを確認しました。


 なお、このサンプルコードでは、トレーニングデータの「目的変数」と、テストデータの「説明変数」「目的変数」のデータを分割するコードは、割愛されています。


 よってFor文の中のコーディングは、本来は予測モデルの訓練および検証を行なうため、もう少し複雑な内容になります。


今回の学習

 「今回のテーマ」の内容を踏まえて、最初にテストデータの作成を行います。まず、どのようなデータが、今回の学習に適しているのか検討します。


テストデータの概要

条件

 以下の条件を満たしているデータが、今回の学習に適していると考えます。

  • すでに機械学習用データとして整備されているもの


テストデータの選択

 上記「条件」で挙げたように、今回の学習では、どのようなデータが機械学習に使用されているのかを学ぶため、事前に機械学習用データとして整備されているオープンデータを探しました。


 今回のデータは「the UC Irvine Machine Learning Repository」から「Car Evaluation Data Set」をダウンロードして使用しました。

f:id:Pimientito:20190530003550p:plain
UCI Machine Learning Repository ロゴマーク


f:id:Pimientito:20190530003917p:plain
「Car Evaluation Data Set」紹介ページ


テストデータの加工

 今回使用するオープンデータは、既に整備されているためデータをダウンロードし、Pythonで読み込めば、すぐに使用できます。(カラム名のレコードが含まれていないため、別途カラム名の定義は必要です)

# UCI (University of California, Irvine カリフォルニア大学アーバイン校)
# Machine Learning Repository
#「Car Evaluation Data Set」
# http://archive.ics.uci.edu/ml/datasets/Car+Evaluation

import pandas as pd

df = pd.read_csv('./data/lesson31/car.data', encoding='utf-8')
df.columns=['buying', 'maint', 'doors', 'persons', 'lug_boot', 'safety', 'class']


 読み込んだ結果は、以下の通りです。

f:id:Pimientito:20190530004952p:plain
「Car Evaluation Data Set」の内容


 データセットの概要は、以下の通りです。

カラム名 概要 カテゴリ 予測対象
buying buying price low,
med,
high,
vhigh(*1)
-
maint price of the maintenance low,
med,
high,
vhigh
-
doors number of doors 2,
3,
4,
5more
-
persons capacity in terms of
persons to carry
2,
4,
more
-
lug_boot the size of
luggage boot
small,
med,
big
-
safety estimated safety of
the car
low,
med,
high
-
class Class Values unacc(*2),
acc(*3)
good
vgood

参考・参照元UCI Machine Learning Repository 「Car Evaluation Data Set」- Data Set Information -より抜粋


(*1) very high(非常に高い)の略と推測。

(*2) unacceptable(許容できない、受入不可)の略と推測。

(*3) acceptable(許容できる、受入可)の略と推測。


カテゴリデータを数値化

 データセットの内容を確認したところ、ほとんどの値が文字列型でした。統計学でいうところの「質的データ」に該当するカテゴリデータには、以下の種類があります。

名称 概要
名義特徴量(*1) 色や物の名称など、それだけでは順序付けが不可能
順序特徴量 データの並び替えや順序・順位付けが可能

(*1) 特徴量とは、予測モデルへ入力する値。説明変数、パラメータなどとも呼ばれています。


 文字列型のカテゴリデータは、そのままでは予測モデルへ投入できないため、整数型の値に変換する必要があります。なお文字列型データと整数型データを関連付ける(変換する)処理を「マッピング」と呼びます。


 カテゴリデータのマッピングには、名義特徴量と順序特徴量と、それぞれに異なる手段があり、取り扱う特徴量ごとに細かく設定・処理を行ないますが、今回は、すべてのデータを順序特徴量としてマッピング処理を行ないます。

#参考・参照元:達人データサイエンティストによる理論と実践 Python 機械学習プログラミング[第2版](インプレス)
#第4章 データ前処理 - よりよいトレーニングセットの構築 - (p.105)を元に作成

import numpy as np
import pandas as pd

#ラベルと整数を関連付けたディクショナリを作成
buying_mapping = {'low':1, 'med':2, 'high':3, 'vhigh':4}
maint_mapping = {'low' : 1, 'med' : 2, 'high' : 3, 'vhigh' : 4}
doors_mapping = {'2' : 1, '3' : 2, '4' : 3, '5more' : 4}
persons_mapping = {'2' : 1, '4' : 2, 'more' : 3}
lug_boot_mapping = {'small' : 1, 'med' : 2, 'big' : 3}
safety_mapping = {'low' : 1, 'med' : 2, 'high' : 3}
class_mapping = {'unacc' : 1, 'acc' : 2, 'good' : 3, 'vgood' : 4}

#ラベルを整数に変換
df['buying'] = df['buying'].map(buying_mapping)
df['maint'] = df['maint'].map(maint_mapping)
df['doors'] = df['doors'].map(doors_mapping)
df['persons'] = df['persons'].map(persons_mapping)
df['lug_boot'] = df['lug_boot'].map(lug_boot_mapping)
df['safety'] = df['safety'].map(safety_mapping)
df['class'] = df['class'].map(class_mapping)

#データの確認
df.head()


 すべての特徴量を整数値に変換した結果は、以下の通りです。

f:id:Pimientito:20190609184531p:plain
カテゴリデータを整数値にマッピング


 整数化した特徴量を、文字列型のカテゴリデータへ再マッピングする場合は、以下の処理を行ないます。

#逆マッピング用(確認のために作成)
#逆マッピング用ディクショナリの作成
inv_buying_mapping = {v: k for k, v in buying_mapping.items()}
inv_maint_mapping = {v: k for k, v in maint_mapping.items()}
inv_doors_mapping = {v: k for k, v in doors_mapping.items()}
inv_persons_mapping = {v: k for k, v in persons_mapping.items()}
inv_lug_boot_mapping = {v: k for k, v in lug_boot_mapping.items()}
inv_safety_mapping = {v: k for k, v in safety_mapping.items()}
inv_class_mapping = {v: k for k, v in class_mapping.items()}

#ラベルをラベル名に再変換
df['buying'] = df['buying'].map(inv_buying_mapping)
df['maint'] = df['maint'].map(inv_maint_mapping)
df['doors'] = df['doors'].map(inv_doors_mapping)
df['persons'] = df['persons'].map(inv_persons_mapping)
df['lug_boot'] = df['lug_boot'].map(inv_lug_boot_mapping)
df['safety'] = df['safety'].map(inv_safety_mapping)
df['class'] = df['class'].map(inv_class_mapping)

#データの確認
df.head()


 再マッピングした結果は、以下の通りです。

f:id:Pimientito:20190609185238p:plain
文字列型カテゴリデータへ再マッピング


 ここでは、整数型の値にマッピングしたデータを使用します。


テストデータを使って学習

 ここからは「今回のテーマ」で取り上げた手順に則ってPythonで学習を進めます。

ホールドアウト法でデータを分割(その1)

 サンプルコードと同様にtrain_test_split関数を使ってデータを分割します。

#ホールドアウト法でのデータ分割(テストデータ20%確保)
train_data, test_data, train_target, test_target = \
    train_test_split(df.drop('class', axis=1), df[['class']], test_size=0.2)

#インデクスの採番
#データセット(説明変数)
train_data.reset_index(inplace=True, drop=True)
test_data.reset_index(inplace=True, drop=True)

#予測対象データセット(目的変数)
train_target.reset_index(inplace=True, drop=True)
test_target.reset_index(inplace=True, drop=True)


 データを分割した結果は、以下の通りです。

f:id:Pimientito:20190609231944p:plain
ホールドアウト法でデータを分割した結果


ホールドアウト法でデータを分割(その2)

 ここでは、その1の手順とは異なる指定方法で、データを分割する方法をご紹介します。

 その1の方法では、DataFrame型の分割データが出力されますが、その2の方法では、Numpy配列型で出力されます。

#分割方法 - その2 -
#①指定列データを抽出(X:説明変数、y:目的変数)
X, y = df.iloc[:, 0:6].values, df.iloc[:, 6].values

#②X, yを、それぞれ分割比率(20%)に沿ってデータを分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)


 出力結果は、以下の通りです。

f:id:Pimientito:20190609234622p:plain
ホールドアウト法でデータを分割した結果(その2)


 type関数を使って、それぞれの型を確認します。

f:id:Pimientito:20190609234833p:plain
分割データの型を確認


 なおNumpy型配列の場合、shape属性ではカラム数が表示されていません。データの内容を見比べてみます。

 DataFrame型の場合は、行/列がマトリックス(格子形状)で管理されていることが分かりました。

f:id:Pimientito:20190616182224p:plain
DataFrame型一次元配列の場合


 一方、Numpyの場合は、リスト型変数のように要素が連続して管理されています。

f:id:Pimientito:20190616183044p:plain
Numpy型一次配列の場合


 同じshape属性ですが、PandasライブラリとNumpyライブラリで仕様が異なるようです。仕様については、また別の機会で調べてみます。


 その1では、DataFrame型データの分割、その2では、Numpy型データの分割をご紹介しました。データの種類や状況によって、どのように使い分けるのか、今後学習を進めるなかで、理解を深めます。


k分割交差検証でデータを分割

 最後は、k分割交差検証を行ないます。はじめに行番号リストを作成します。

#対象行の行番号リストを生成
Row_Number = list(range(len(train_target)))


 作成したリストは、以下の通りです。

f:id:Pimientito:20190616220235p:plain
行番号リストの確認


 次にKFold関数のパラメータ値を設定します。

# 交差検証用データの分割
from sklearn.model_selection import KFold

#パラメータ値(分割:5, データシャッフル:有り)
k_fold = KFold(n_splits=5, shuffle=True)

#設定値の確認(k_fold)
k_fold


 KFold関数のパラメータ設定の結果は、以下の通りです。

f:id:Pimientito:20190616221417p:plain
KFold関数のパラメータ設定


 最後に、ループ文の中にデータの分割と予測モデルを検証するコードを記載します。(今回は、データの分割まで)

# k分割交差検証を実施
for train_cssvl_no, test_cssvl_no in k_fold.split(Row_Number):
    
    # 学習データを分割(説明変数、目的変数)
    train_cssvl_data = train_data.iloc[train_cssvl_no, : ]
    train_cssvl_target=train_target.iloc[train_cssvl_no, :]
    
    # 検証データを分割(説明変数、目的変数)
    test_cssvl_data = train_data.iloc[test_cssvl_no, : ]
    test_cssvl_target=train_target.iloc[test_cssvl_no, : ]


    #--------------

    # 性能評価対象のモデルを記載
    
    #--------------


 モデルの性能評価を行なう部分に、いろいろな書籍のサンプルコード(例:cross_val_score関数や、パイプラインを使用して変換器と推定器を結合など)を写経してみましたが、たくさんのエラーや、何の結果か分からない数値が表示されるなど、解説できない結果ばかりだったため、今回はデータを分割するところまでとします。


可視化に挑戦

 今回の学習では、データを分割したものの、実際に検証できる「予測モデル」が無いため、可視化するデータを取得することができませんでした。

 その代わりに、今回の可視化では、使用するデータの事前調査を行なう方法のひとつ「相関行列図」を、seabornライブラリを使ってデータを可視化します。


 相関行列図では、行と列が交差する特徴量同士の相関関係を見ることができます。

f:id:Pimientito:20190616232751j:plain
相関行列説明図


 相関行列図の対角線上を赤線で引かれているグレー部分は、行列共に同じ特徴量(項目)を掛け合わせているので、相関関係の確認対象外です。


 また赤線に対して垂直に交わる紺色の矢印線で結ばれているマス目(同系色のペア)は、行列の並びが反対の同一項目です。


 今回のテストデータ「Car Evaluation Data Set」を使用して相関行列図を作成します。このテストデータの目的変数「class」が、その他の説明変数と、どのような相関関係(一方に変化があれば、他方も影響を受ける関係)があるのか確認します。

# 達人データサイエンティストによる理論と実践
# Python 機械学習プログラミング(インプレス)
# 10.2.3 相関行列を使って関係を調べる(p.303-) サンプルコードを元に作成

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

cols = ['buying', 'maint', 'doors', 'persons', 'lug_boot', 'safety', 'class']
cm = np.corrcoef(df[cols].values.T)

hm = sns.heatmap(cm, 
                 cbar=True, 
                annot=True, 
                square=True, 
                fmt='.2f', 
                annot_kws={'size' : 10}, 
                yticklabels=cols,
                xticklabels=cols)

plt.tight_layout()
plt.show()


 データの相関関係を可視化した結果は、以下の通りです。

f:id:Pimientito:20190609185649p:plain
相関行列を使って関係を調べる


 この図で見る限りでは、目的変数「class」は、説明変数「safety」と「persons」からは、何らかの影響を受ける関係であることが分かりました。


今回のまとめ

 今回から、データの「分割」の章に入りました。

 いままでは、どちらかと言えば、データテーブルの操作などデータベース寄りのお話しが多く、何とか課題についていけていたのですが、この章では、一気に機械学習の要素が増え、なかなか前へ進むことができませんでした。


 「機械学習モデル」「予測モデル」という名前は知っていても、どのようにコーディングしていくものなのか、いまだに理解できておらず、今回の執筆中でも、何度かk分割交差検証のループ文に、参考資料等のサンプルコードを挿入してみました。

 実行結果に「accuracy:0.0xx, 0.0xxx......」と表示されるものの、筆者自身が、その結果を解釈できなかったため、今回の記事に盛り込むことは控えました。


  • 機械学習モデルって、どうやって作るの?

  • 開発者が意図した結果を出すモデルと、過学習モデルの境界は、どこにあるの?

  • 予測・推測するモデルと、分類するモデルは、入力値が、連続データか離散データかの違いだけなの?

  • データを入れただけなのに「accuracy」とは、何に対しての正確性なの?


 などなど。今回の執筆では、いままで以上に、機械学習に対しての疑問が浮かんできました。


 本ブログを開始して、間もなく一年になります。この一年で、筆者自身、機械学習を理解できるようになったのでしょうか。。。。次の一年では、もっともっと機械学習の中心に向かってアプローチしなければなりません。


 まだまだ、機械学習の学習は続きます。



 今回は、以上です。



【参考資料】

データサイエンティスト養成読本 機械学習入門編 (Software Design plus)

データサイエンティスト養成読本 機械学習入門編 (Software Design plus)


東京大学工学教程 情報工学 機械学習

東京大学工学教程 情報工学 機械学習


[第2版]Python 機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)

[第2版]Python 機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)



UCI(University of California, Irvine(カリフォルニア大学アーバイン校)) Machine Learning Repository

archive.ics.uci.edu


Machine Learning Repository「Car Evaluation Data Set」

archive.ics.uci.edu


本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。


お問い合わせ先:otoiawase@handson-t-arte.com


【前処理の学習-30】データを学ぶ ~結合~⑤

 前回は、Pythonによる過去データの結合について学びました。

pimientito-handson-ml.hatenablog.com

 今回は、すべてのデータを結び付ける「全結合」について学びます。


【今回の目標到達点】

 データテーブルを全結合する

【目次】


参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

 「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「データ結合」の概要

 参考資料「前処理大全」の「第4章 結合」冒頭で、データの結合について、著者は以下のように述べています。

必要なデータが1つのテーブルにすべて入っていることはまれです。業務システムのデータベースは、データの種類ごとにテーブルが分かれているからです。一方、データ分析用のデータは1つのテーブルにまとまった横に長いデータが望ましく、そのようなデータを得るためにはテーブル同士を結合する処理が必要になります。

参考・参照元:第4章「結合」(p.084)より抜粋


 その上で、データの結合について、以下、3つの考え方をご紹介されています。

  • マスタテーブルから情報を取得

  • 条件に応じて結合するマスタテーブルを切り替え

  • 過去データから情報を取得


今回のテーマ

 テーブルの全結合

概要

 参考資料「前処理大全」の「4-4 全結合」の冒頭で、著者は全結合の必要性について述べています。


 例えば、結合するテーブルの一方に、データが存在しない場合、テーブル結合時に「存在しないレコード」が「存在する」ことに気付く事ができません。


 そのような状況を回避するためには、テーブルを全結合し、すべての組み合わせを生成することで「存在しないレコード」の存在も認識できるようになります。


 次の項よりサンプルコードを使って、実際に動作確認を行ないます。


サンプルコード[Python]

 Pythonでは、テーブルの全結合を可能とする関数が提供されていないため、参考資料のサンプルコードでは、テーブルの全結合を実現するためのコーディングが紹介されています。


 しかしサンプルコードから汎用的な「構文」だけを抜き出すことが難しいため、サンプルコードの動作確認を行ないながら、学習を進めます。本ブログのコードは、参考資料の「python_awesome.py」(抜粋)(p.127)を参考に再作成しています。


テーブル作成

 使用するサンプルテーブルは、ひとつひとつの処理を理解しやすいように、規模の小さいものを作成しました。

#サンプルコード[Python]
#前処理大全 4-4 全結合 python_awesome.py(抜粋)(p.127)を参考に作成 

import pandas as pd

# 日付型用ライブラリ
from datetime import date as dt

#日付の計算用ライブラリ
from dateutil.relativedelta import relativedelta

#年月テーブルの作成
tbl_month = pd.DataFrame({
    'year_month' : [(dt(2019, 1, 1) + relativedelta(months=x)).strftime("%Y%m")
                   for x in range(0, 3)]
})

#顧客テーブルの作成
tbl_customer = pd.DataFrame({
   'customer_id' : ['c_01', 'c_02', 'c_03'],
    'name' : ['ABC corp.', 'DEF, Inc.', 'PQR Ltd.'],
    'location' : ['東京', '大阪', '福岡' ]
})

#売上テーブル
tbl_sales = pd.DataFrame({
    'customer_id' : ['c_01', 'c_01', 'c_01', 'c_01', 'c_01', 'c_01', 
                                'c_02', 'c_02', 'c_02', 'c_02', 'c_03', 'c_03', 'c_03'],
    'sales_date' : [dt(2019,1,10), dt(2019, 1, 25), dt(2019, 2, 8), dt(2019,2,22), 
                            dt(2019, 3, 8), dt(2019, 3, 20), dt(2019, 2, 5), dt(2019,2,15), 
                            dt(2019, 3, 5), dt(2019, 3, 20), dt(2019,1,15), dt(2019, 1, 25), dt(2019, 2, 12)],
    'proceeds' : [2000, 2500, 1800, 5000, 3800, 10000, 
                          2800, 1500, 3300, 5700, 4800, 12000, 1200]
    
})


 各テーブルの概要は、以下の通りです。

【tbl_month(年月テーブル)】顧客テーブルと売上テーブルを結合するための中間テーブル

カラム名 型名 概要
year_month 日付 YYYYMM形式


【tbl_customer(顧客テーブル)】

カラム名 型名 概要
customer_id 文字列 c_01〜c_03
name 文字列 顧客名
location 文字列 都道府県名


【tbl_sales(売上テーブル)】

カラム名 型名 概要
customer_id 文字列 c_01〜c_03
sales_date 日付 YYYYMMDD形式
proceeds 整数 売上金


 各テーブルのデータは、以下の通りです。

【tbl_month(年月テーブル)】

f:id:Pimientito:20190503231137p:plain
tbl_month(年月テーブル)


【tbl_customer(顧客テーブル)】

f:id:Pimientito:20190503231328p:plain
tbl_customer(顧客テーブル)


【tbl_sales(売上テーブル)】

f:id:Pimientito:20190503231356p:plain
tbl_sales(売上テーブル)


 ここで注目したいのは、以下の部分です。連続した日付型の配列要素を、relativedelta関数を使用して作成しています。

#日付の計算用ライブラリ
from dateutil.relativedelta import relativedelta

#年月テーブルの作成
tbl_month = pd.DataFrame({
    'year_month' : [(dt(2019, 1, 1) + relativedelta(months=x)).strftime("%Y%m")
                   for x in range(0, 3)]
})


 この部分で行なわれている処理を、以下にまとめました。

f:id:Pimientito:20190504010643p:plain
tbl_month(年月テーブル)が生成される処理


 relativedelta関数の詳細は、dateutilドキュメント「relativedelta」をご覧ください。

dateutil.readthedocs.io


結合キー(カラム)の作成

 次に、tbl_month(年月テーブル)とtbl_customer(顧客テーブル)の結合キー(カラム)を作成します。

#テーブルに結合キーを持つカラムを作成
tbl_month['j_key'] = 0
tbl_customer['j_key'] = 0


 新規カラム「j_key」を作成した結果は、以下の通りです。

f:id:Pimientito:20190504141549p:plain
tbl_month(年月テーブル)の結合キー作成


f:id:Pimientito:20190504141734p:plain
tbl_customer(顧客テーブル)の結合キー作成


テーブルの全結合

 pandasのmerge関数を使用して、tbl_month(年月テーブル)とtbl_customer(顧客テーブル)を結合します。結合キーは、上記で作成した「j_key」です。結合した結果は、顧客テーブルに集約します。

#tbl_monthとtbl_customerを全結合
tbl_customer = pd.merge(tbl_customer[['customer_id', 'j_key']], tbl_month, on='j_key')


 結合した結果は、以下の通りです。

f:id:Pimientito:20190504143227p:plain
tbl_month(年月テーブル)とtbl_customer(顧客テーブル)を結合した結果


 顧客ID 1つに対して、年月(2019.01〜2019.03)の3ヶ月を結合して、合計9レコードが作成されました。


売上テーブルに結合キーを作成

 売上テーブルの売上日付「sales_date」を元に、結合キーを作成します。結合キーの作成とテーブル結合までの流れは、以下の通りです。



  1. 売上テーブルのカラム「sales_date」(%Y-%m-%d形式)を元に、結合キーのカラム「year_month」(%Y%d形式)を作成

  2. 売上テーブルのカラム「customer_id, year_month」と顧客テーブルの「customer_id, year_month」を結合キーとして、テーブルを結合



 売上テーブルに結合キーを作成するコードは、以下の通りです。

#tbl_salesのカラム「sales_date」を"年"、"月"の形式に変換して新規カラムへ格納
tbl_sales['year_month'] = tbl_sales['sales_date'].apply(lambda x : pd.to_datetime(x, format='%Y-%m-%d').strftime("%Y%m"))


 結合キーを作成した結果は、以下の通りです。

f:id:Pimientito:20190504163035p:plain
結合キー「year_month」作成結果


 この部分で行なわれている処理を、以下にまとめました。

f:id:Pimientito:20190504165851p:plain
結合キー「year_month」作成処理


顧客テーブルと売上テーブルの結合

 最後に、顧客テーブルと売上テーブルを、カラム「customer_id」と「year_month」を、複合キーにして結合します。


 参考資料では、テーブル結合後、sum関数で集計していますが、ここではagg関数を使用して、各顧客IDの「売上金額の合計」と「売上レコード数」を、月ごとに集計しています。

#tbl_customerとtbl_salesを結合,
result = pd.merge( tbl_customer, tbl_sales[['customer_id', 'year_month', 'proceeds']], \
                  on=['customer_id', 'year_month'], how='left').groupby(['customer_id', 'year_month']).agg({'proceeds' : ['sum', 'count']}).reset_index()

#カラム名の再設定
result.columns=['customer_id', 'year_month', 'total_preceeds', 'sales_count']


 結果は、以下の通りです。

f:id:Pimientito:20190504172543p:plain
テーブルの全結合・データの集約の結果


 なお、結果の値が'NaN'の場合、以下のコードを実行します。

result.fillna(0, inplace=True)


 以上が、Pythonで行なうテーブルの全結合でした。


今回の学習

 「今回のテーマ」の内容を踏まえて、最初にテストデータの作成を行います。まず、どのようなデータが、今回の学習に適しているのか検討します。


テストデータの概要

条件

 以下の条件を満たしているデータが、今回の学習に適していると考えます。

  • 複数のテーブルに分かれているデータであること。


テストデータの選択

 上記「条件」の内容をもとに、今回は「東京都 オープンデータ カタログサイト」より「都内鉄道駅におけるだれでもトイレバリアフリー設備情報」を使用して、テストデータを作成します。

f:id:Pimientito:20190506011147p:plain
「東京都 オープンデータ カタログサイト」トップページ


テストデータの加工(前処理の前処理)

 今回使用するオープンデータは、都内鉄道駅に設置されているバリアフリー設備(主に“だれでもトイレ”を指します)の設置状況を記録したものです。


 はじめにダウンロードしたCSVファイルを読み込みます。

#データの読み込み
#東京都 オープンデータ カタログサイト
#http://opendata-portal.metro.tokyo.jp/www/index.html

#だれでもトイレのバリアフリー情報(平成31年1月時点)
#都内鉄道駅におけるだれでもトイレのバリアフリー設備情報
#http://opendata-catalogue.metro.tokyo.jp/dataset/t000010d0000000062/resource/a03fbc68-76c6-4563-ac5e-c1cbe06b4a8e

import pandas as pd

df = pd.read_csv('./data/lesson30/tonaitetsudoueki_barrier-free-wc.csv', encoding='shift_jis')


 データの内容は、以下の通りです。

カラム名 カラム名
管理者種別番号 便座に手すりがある
鉄道駅通し番号 オストメイト用設備がある
鉄道駅内トイレ
通し番号
オストメイト用設備が
温水対応している
鉄道会社名 大型ベッドを備えている
路線名 乳幼児用おむつ交換台等を
備えている
鉄道駅名 乳幼児用椅子を備えている
都道府県 非常用呼び出しボタンを
設置している
市区町村・番地 月曜日
ビル建物名 火曜日
トイレ名 水曜日
設置フロア 木曜日
経度 金曜日
緯度 土曜日
座標系 日曜日
性別の分け 祝日
トイレへの誘導路として点字ブロック
敷設している
その他
トイレの位置等を音声で
案内している
写真データ(トイレの入り口)
戸の形式 写真データ(トイレ内)
車椅子が出入りできる
(出入口の有効幅員80cm以上)
写真データ(トイレ内(別角度))
車椅子が転回できる
(直径150cm以上の円が内接できる)
備考
便座に背もたれがある


 とてもユニークな名称のカラム名が多い印象です。カラム名が、ほぼ「文章」となっているものの大半は「○」か「×」の値が入っています。


 カラム「月曜日」〜「日曜日」と「祝日」には、“始発_終車”の文字列が入っており、これは駅の営業中(終日)は、対象のバリアフリー設備が利用可能であることを意味するのでしょう。


 今回は、このデータセットを使用して「山手線各駅のバリアフリー設備(だれでもトイレ)の設置状況」を確認します。


全結合用補助テーブルの作成

 「今回のテーマ」である“テーブルの全結合”を実現するため、オープンデータの他に2つのデータテーブルを作成します。


  • 山手線駅名テーブル

  • トイレ種別名テーブル


 山手線駅名テーブルは、単に山手線駅名のみを並べたテーブルです。テーブル作成のコードは、以下の通りです。

#「山手線駅名」テーブルの作成
tbl_YMTST = pd.DataFrame({
    '鉄道駅名' : ['大崎', '五反田', '目黒', '恵比寿', '渋谷', 
        '原宿', '代々木', '新宿', '新大久保', '高田馬場', 
        '目白', '池袋', '大塚', '巣鴨', '駒込', '田端', 
        '西日暮里', '日暮里', '鶯谷', '上野', '御徒町', 
        '秋葉原', '神田', '東京', '有楽町', '新橋', '浜松町', 
        '田町', '品川']
}) 


 作成したテーブルを確認した結果は、以下の通りです。

f:id:Pimientito:20190511223710p:plain
「山手線駅名」テーブル


 次に、トイレ種別名テーブルを作成しますが、その前に、今回のオープンデータには、どのようなトイレ名が存在するのか確認します。drop_duplicates関数を使用して、一意のトイレ名を表示します。

df['トイレ名'].drop_duplicates()


 表示結果は、以下の通りです。

f:id:Pimientito:20190511224429p:plain
トイレ名の表示


 トイレ名を確認すると、似通った名称が多いことが分かりました。そこで、トイレ種別名テーブルでは「だれでもトイレ」「多機能トイレ」「多目的トイレ」の三種類のみ使用することにしました。

#「トイレ種別名」テーブルの作成
tbl_TW_type = pd.DataFrame({
    'トイレ名' : ['だれでもトイレ', '多機能トイレ', '多目的トイレ']
    
})


 作成したテーブルを確認した結果は、以下の通りです。

f:id:Pimientito:20190511225322p:plain
「トイレ種別名」テーブル


 最後に、オープンデータ「都内鉄道駅におけるだれでもトイレバリアフリー設備情報」の前処理を行ないます。


必要なカラムの抽出

 データセットのカラム数は41個あるため、その中から必要なカラムだけ抽出します。今回の学習で使用するカラムは、以下の通りです。

カラム名
鉄道会社名
路線名
鉄道駅名
トイレ名


 カラムの抽出には、loc関数を使用します。

#カラムの抽出
tbl_STWC = df.loc[ : , ['鉄道会社名', '路線名', '鉄道駅名', 'トイレ名']].reset_index(drop=True)


 抽出したカラムの結果は、以下の通りです。

f:id:Pimientito:20190511231527p:plain
カラムの抽出結果


各トイレ名の件数を確認

 作成した「トイレ種別名テーブル」では、「だれでもトイレ」「多機能トイレ」「多目的トイレ」の三種類のトイレ名しかありませんが、オープンデータ側の「トイレ名」は類似名が複数あるため「言葉の揺らぎ」をトイレ種別名テーブルに合わせる必要があります。


 まずオープンデータのトイレ名の種類ごとに件数を確認します。

#トイレ名の種類を確認
tbl_STWC.groupby('トイレ名').count().reset_index()


 確認した結果は、以下の通りです。

f:id:Pimientito:20190511233729p:plain
トイレ名種別ごとの件数


 大まかに四種類(緑、黄、赤、青枠)に分けることができます。それぞれ類似する名称を統一します。


言葉の揺らぎを修正

 言葉の揺らぎは、replace関数を使用して修正します。最初にトイレ名の中に、キーワード「多機能」を含んだトイレ名を、すべて「多機能トイレ」に修正しました。

#言葉の揺らぎを修正(多機能〜)
tbl_STWC['トイレ名'].replace(['多機能お手洗', '多機能お手洗い', '多機能お手洗い(共用)(ヒカリエトイレ)', \
                        '多機能お手洗い(共用)(道玄坂トイレ)', '多機能お手洗い(女性用)(ヒカリエトイレ)', \
                        '多機能お手洗い(女性用)(宮益坂トイレ)', '多機能お手洗い(男性用)(宮益坂トイレ)', \
                        '多機能トイレ女子トイレ', '多機能トイレ男子トイレ', '多機能化粧室', '男女共用多機能トイレ'], '多機能トイレ', inplace=True)


 次にトイレ名に、キーワード「多目的」を含んだトイレ名を、すべて「多目的トイレ」に修正します。

#言葉の揺らぎを修正(多目的〜)
tbl_STWC['トイレ名'].replace(['多目的トイレ(共用)(ラウンジトイレ)', '多目的(だれでも)トイレ'], '多目的トイレ', inplace=True)


 残りは、トイレ名の種類を確認したときに、その名称の中に「だれでも〜」「多目的〜」「多機能〜」というキーワードを含んでいなかったトイレ名(図「トイレ名種別ごとの件数」の黄枠部分)について対応します。


 少々、乱暴ですが、今回は黄枠のトイレ名は「多機能トイレ」と「だれでもトイレ」に分類しました。対象行を削除するという考えもありますが、その分、トイレの設置件数が減ってしまうため、対象行の削除は行ないませんでした。


#言葉の揺らぎを修正(化粧室女子用、化粧室男子用)
tbl_STWC['トイレ名'].replace(['化粧室女子用', '化粧室男子用'], '多機能トイレ', inplace=True)


#言葉の揺らぎを修正(ファミリートイレ)
tbl_STWC['トイレ名'].replace(['ファミリートイレ'], 'だれでもトイレ', inplace=True)


 トイレ名の揺らぎを修正した結果は、以下の通りです。

f:id:Pimientito:20190512000510p:plain
言葉の揺らぎ(トイレ名)修正後の結果


 次は駅名の揺らぎを修正します。はじめに鉄道会社を、東日本旅客鉄道(JR東日本)に絞り込みます。

#鉄道会社名が「東日本旅客鉄道(JR東日本)」を絞り込み    
tbl_STWC_JR = tbl_STWC.loc[tbl_STWC['鉄道会社名']=='東日本旅客鉄道', ['鉄道会社名', '路線名', '鉄道駅名', 'トイレ名']]


 JR東日本の情報のみに絞り込んだ結果は、以下の通りです。

f:id:Pimientito:20190512163010p:plain
東日本旅客鉄道(JR東日本)に絞り込んだ結果


 鉄道会社で絞り込んだ結果を見ると、鉄道駅名「東京駅」が、複数の名称で存在することが分かりました。


 今回のデータセットでは、路線名にキーワード「山手線」を含んでいない山手線の駅が、複数散見することにより、路線名を結合キーとして利用できません。その代わりに鉄道駅名を結合キーとして使用します。そのために鉄道駅名から、言葉の揺らぎを修正する必要があります。


#言葉の揺らぎを修正(東京〜)
tbl_STWC_JR['鉄道駅名'].replace(['東京(地下)', '東京(京葉地下)'], '東京', inplace=True)


 東京駅から言葉の揺らぎを修正した結果は、以下の通りです。

f:id:Pimientito:20190512164420p:plain
言葉の揺らぎ(鉄道駅名)修正後の結果


 余談ですが、Jupyter Notebookでデータを表示する時、レコード数が多いと途中のデータの表示が省略されてしまいます。そのような場合は、values属性を指定すると、array型配列として文字列が画面に表示されます。この方法を使って、他の駅名に揺らぎが無いか確認します。

#鉄道駅名の確認
tbl_STWC_JR.values


 確認した結果は、以下の通りです。

f:id:Pimientito:20190512164927p:plain
values属性を指定してデータを確認


 東京駅以外の駅名では、言葉の揺らぎが無いことを確認しました。これで、すべての前処理が完了しました。作成したテーブルを使用して、今回の学習を行ないます。


テストデータを使って学習

 ここからは「今回のテーマ」で取り上げた手順に則って、Pythonで学習を進めます。

結合キーのカラムを新規作成

 「山手線駅名」テーブルと「トイレ種別名」テーブルを全結合するための結合キー(カラム)を新規作成します。

#各テーブルに結合用キーのカラムを作成
tbl_YMTST['j_key'] = 1
tbl_TW_type['j_key'] = 1


 結合キーのカラムを作成した結果は、以下の通りです。

f:id:Pimientito:20190512171050p:plain
結合キー(カラム)の作成結果


 前項「サンプルコード[Python]」では「j_key」の値を"0"に指定していましたが、今回は"1"を指定しています。二つのテーブルの値が同じであれば良いだけで、値の内容は何でも構いません。


テーブルの全結合

 作成した結合キーを元に「鉄道駅名」テーブルと「トイレ種別名」テーブルを全結合します。

#鉄道駅名テーブルとトイレ種別名テーブルを全結合
tbl_YMTST = pd.merge(tbl_YMTST[['鉄道駅名', 'j_key']], tbl_TW_type, on='j_key')


 テーブルを全結合した結果は、以下の通りです。

f:id:Pimientito:20190512172401p:plain
テーブルを全結合した結果


 山手線駅(29駅) × トイレ名(3種) 合計87パターンのレコードが作成されました。


 ここにオープンデータの情報を「鉄道駅名」と「トイレ名」を複合キーにして結合します。なおトイレ設置件数は、レコード件数(j_key)をカウントすることで算出しました。

#バリアフリー設備設置状況を全結合
result = pd.merge(tbl_YMTST, tbl_STWC_JR[['鉄道駅名', 'トイレ名']], on=['鉄道駅名', 'トイレ名'], how='left').groupby(['鉄道駅名', 'トイレ名'])['j_key'].count().reset_index()

#カラム名再設定
result.columns=['鉄道駅名', 'トイレ名', '設置件数']


 オープンデータの情報を関連付けした結果は、以下の通りです。

f:id:Pimientito:20190512173805p:plain
オープンデータの情報を結合した結果


 結合した結果が、少々見づらいため、pivot_table関数を使用して結果を表示します。

#pivot_table関数を使用して結合結果を表示
tips = result.pivot_table('設置件数', index=['鉄道駅名', 'トイレ名'])


 ピボットテーブルで表示した結果は、以下の通りです。

f:id:Pimientito:20190512174325p:plain
ピボットテーブルの表示結果


 今回使用したオープンデータからは、0件データ(駅構内にバリアフリー化されたトイレが存在しない)の確認はできませんでした。(山手線でバリアフリー化されていない方があり得ないかもしれません)


 テーブルの全結合によって、データの全組み合わせを確認できることは、データの傾向を把握するためには、とても有効ですが、反面、処理速度や、リソースの使用率にも大きく影響するため、結合する範囲の絞り込みには、十分気を付ける必要があります。


可視化に挑戦

可視化の目的

 すべてのデータを結合した情報をピボットテーブルに置き換えて表示しましたが、全体(山手線全29駅)を相対的に比較するには、少し難しいため可視化を行ないます。


 今回は、横軸に鉄道駅名、縦軸に設置件数(正確にはレコード件数)を設定して、トイレ種別ごとの棒グラフで可視化しました。


 残念ながら、筆者のスキル不足で、ピボットテーブルを、そのまま可視化することができなかったため、今回は、Seabornライブラリを使用して可視化を行ないました。


[seaborn]

 今回作成したコードは、以下の通りです。

#可視化ライブラリ
import seaborn as sns
import matplotlib.pyplot as plt

#日本語化ライブラリ
import japanize_matplotlib

#グラフ範囲の設定
fig = plt. subplots(figsize = (25, 10)) 

sns.barplot(x='鉄道駅名', y='設置件数', hue='トイレ名', data=result)

#グリッド線の表示
plt.grid()


 プロット図は、以下の通りです。

f:id:Pimientito:20190512180227p:plain
山手線各駅に設置されているバリアフリー設備の件数


 プロット図を確認すると、東京駅の設置件数が圧倒的に多いですが、データの前処理の項で述べましたが、今回のデータでは、路線名で「山手線」を特定することができなかったため、「鉄道駅名」でデータを抽出した結果、複数路線(中央線や京葉線など多数)の情報が入ってしまったために起きた結果です。


 また山手線各駅で「だれでも〜」「多機能〜」「多目的〜」が、最低でも一件ずつカウントされていることも、この結果の信憑性は低いと考えます。(各駅で、最低でも3箇所の設備があり、それぞれ別名で登録されているとは考えづらい)


 使用するデータの整合性確認や、厳密な前処理など、もっと学ぶ必要があります。


今回のまとめ

 全5回に渡って「結合」を学びました。テーブルの結合により、分析対象のデータの内容が多彩になる反面、データ量が爆発的に増加してしまう危険性についても同時に学びました。


 本ブログで学習している範囲では、処理速度が著しく遅くなったり、メモリなどのリソースが枯渇してエラーになっても、特段問題にもなりませんが、もし実務の場で、そのようなことが起きれば、システムインシデントに繋がる恐れもあるでしょう。


 ついつい分析に夢中になる余りに、思わぬ「落とし穴」に落ちないよう、常に「スモール・スタート」を心掛けなければいけないと肝に命じるばかりです。


 今回で「結合」編は、一旦完了となります。次回から「第5章 分割」を学習します。



 今回は、以上です。



【参考資料】

東京都 オープンデータ カタログサイト「都内鉄道駅におけるだれでもトイレバリアフリー設備情報」

opendata-catalogue.metro.tokyo.jp


UnicodeDecodeError: 'utf-8' codec can't decode byte

stackoverflow.com


Qiita「pip install して import するだけで matplotlib を日本語表示対応させる」@uehara1414氏

qiita.com



本ブログに関するお問い合わせ

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。


お問い合わせ先:otoiawase@handson-t-arte.com

【前処理の学習-29】データを学ぶ ~結合~④

 前回は、過去データの結合について学びました。

pimientito-handson-ml.hatenablog.com

 今回は、前回のテーマをPythonコードで学習します。そのため学習内容が前回【前処理の学習-28】と、一部重複しているところもあります。


【今回の目標到達点】

 時系列データを利用して、過去データを結合する(Pythonで学習)

【目次】

参考資料のご紹介

 はじめに、現在、主に参考とさせていただいている書籍をご紹介します。

「前処理大全 データ分析のためのSQL/R/Python実践テクニック」本橋智光氏著(技術評論社)


「データ結合」の概要

 参考資料「前処理大全」の「第4章 結合」冒頭で、データの結合について、著者は、以下のように述べています。

必要なデータが1つのテーブルにすべて入っていることはまれです。業務システムのデータベースは、データの種類ごとにテーブルが分かれているからです。一方、データ分析用のデータは1つのテーブルにまとまった横に長いデータが望ましく、そのようなデータを得るためにはテーブル同士を結合する処理が必要になります。

参考・参照元:第4章「結合」(p.084)より抜粋


 その上で、データの結合について、以下、3つの考え方をご紹介されています。

  • マスタテーブルから情報を取得

  • 条件に応じて結合するマスタテーブルを切り替え

  • 過去データから情報を取得


今回のテーマ

過去データから情報を取得

概要

 参考資料「前処理大全」の「4-3 過去データの結合」の冒頭で、著者は「過去データ」を活用することは、基礎分析や、予測モデルを構築する上で、有用であると述べています。以下、該当の文章を引用します。

基礎分析をするにしても、予測モデルを構築するにしても、過去データを活用することは有用です。筆者は「過去データの前処理を制するものはデータの前処理を制す」ぐらい重要だと考えています。

参考・参照元:第4章「4-3 過去データの結合」(p.103)より抜粋


 反面、時系列データの取り扱いは難しく、処理によっては、意図しない変換や、リーク(予測モデルに使用するデータに、未来のデータが混入すること)が伴うことがあるため、十分な注意が必要とも述べています。


 なお、時系列データを無作為に結合することにより、データ数が大量に増えてしまう危険性についても触れており、以下、二点の対策を利用することを、推奨されています。

1.結合対象とする過去の期間を絞る

2.結合した過去データに集約関数を利用して、データ数を増やさないようにする

参考・参照元:第4章「4-3 過去データの結合」(p.104)より抜粋


 具体的には「過去データ」を扱う際には、不必要に長い期間を指定するのではなく、分析に必要な期間に限定することや、過去データの結合時に、集約関数を使用して、データをまとめることで、結合によるデータ増加(レコード増加)を、抑止することができるようです。


サンプルコード[Python]

 参考資料「4-3 過去データの結合」で紹介されているサンプルコードの内、行または行数を指定して、データを取得する処理を学習します。


  • 行を指定してデータを取得

  • 行数を指定してデータを取得



n件前のデータ取得

 ある行を指定して、過去データを取得する構文は、以下の通りです。なおPythonには、LAG関数が提供されていないため、shift関数を利用して行移動を行う方法を、参考資料では紹介されています。

#① レコードセットをグループ化し、各グループごとに時系列でソートします。
result = data_table.groupby(grouping_column).apply(lambda group:group.sort_values(by=datetime_column, axis=0, inplace=False))


#② shift関数で指定した行位置のデータを取得して、新しいカラムへ値を代入
result[new_column] = pd.Series(result[target_column].shift(periods=n))

参考・参照元:「python_awesome.py」(抜粋)(p.107)を参考に作成


【補足】

項目 概要
result データセットから取得した結果
data_table データセット(データの源泉)
grouping_column グループ化対象カラム
datetime_column 並び替え対象日付型カラム
new_column 新規作成カラム
target_column 取得対象カラム
n shift関数の引数


 参考資料では、sort_values関数について、以下の補足説明がされています。

sort_values関数は引数のbyに指定された行/列名によって、データ行/列の並び替えを行います。axisが0の場合は、指定された列名の値によって行を並び替えます。axisが1の場合は、指定された行名の値によって列を並び替えます。

参考・参照元:第4章「4-3 過去データの結合」(p.108)より抜粋


 もう少し詳細に動作の違いを確認します。なおDataFrame型のsort_values関数には「by」パラメータが存在しますが、Series型のsort_values関数には存在しないようです。各々のパラメータの詳細については、pandasのドキュメントをご覧ください。


【パラメータ「axis」と「by」の組み合わせ】

axis by 並び替え方向
0 列名 行方向
1 行名 列方向


 pandasドキュメントのサンプルコードを参考に、動作確認を行ないます。

import pandas as pd

df = pd.DataFrame({
    'col1' : ['A', 'B', 'C', 'D', 'E', 'F'],
    'col2' : ['F', 'E', 'D', 'C', 'B', 'A'],
    'col3' : [0, 1, 2, 3, 4, 5],
    'col4' : [5, 4, 3, 2, 1, 0],
    'col5' :['あ', 'い', 'う', 'え', 'お', 'か'],
    'col6' :['か', 'お', 'え', 'う', 'い', 'あ'],
})

参考・参照元:pandas 0.24.2 documentation「pandas.DataFrame.sort_values」のサンプルコードを参考にコードを再作成


 サンプルデータテーブルの内容は、以下の通りです。

f:id:Pimientito:20190429130537p:plain
サンプルデータテーブルの内容


 sort_values関数の動きが分かりやすいように、アルファベット、数字、ひらがなを、それぞれ昇順、降順に並べたDataFrameを用意しました。


【サンプルデータテーブル】

index col1 col2 col3 col4 col5 col6
0 A F 0 5
1 B E 1 4
2 C D 2 3
3 D C 3 2
4 E B 4 1
5 F A 5 0


 続いて、sort_values関数のパラメータ「by」と「axis」の組み合わせによって、どのような結果になるのか確認します。


 はじめは「by」パラメータに列名、「axis」には、0を指定して行方向へ並び替えを行います。

df.sort_values(by='col6', axis=0)


 並び替えの結果は、以下の通りです。

f:id:Pimientito:20190429132709p:plain
sort_values関数の結果1


 カラム「col6」を軸として、行を縦断(赤矢印方向)するように昇順で並んでいます。


 続いて「by」パラメータに行名、「axis」には、1を指定して列方向へ並び替えを行います。

df.sort_values(by=[0, 1, 2, 3, 4, 5], axis=1)


 並び替えの結果は、以下の通りです。

f:id:Pimientito:20190429134753p:plain
sort_values関数の結果2


 少々、結果が分かりづらいですが、行名「0」を軸に列を横断(赤矢印方向)するように、左から昇順で並び替えられています。列名のcol1〜col6を見ると、順番が入れ替わっていることが分かります。


 はじめサンプルデータテーブルの列は左から「アルファベット列」「数値列」「ひらがな列」と並んでいましたが、並び替え後は「数値列」「アルファベット列」「ひらがな列」に変わっています。


 今度は「by」パラメータに、上記の逆順で行名を指定してみます。

df.sort_values(by=[5, 4, 3, 2, 1, 0], axis=1)


 並び替えの結果は、以下の通りです。


f:id:Pimientito:20190429140902p:plain
sort_values関数の結果3


 今度は、行名「5」を軸に、列を横断(赤矢印方向)するように、左から昇順で並び替えられています。


 今回、sort_values関数の動作確認の中で気付いたことですが、「by」パラメータの指定では、列名を指定する場合は、1列以上の列名を指定すれば動作しましたが、行名の場合は、2行以上の行名を指定しないと動作しませんでした。ドキュメントで、何か見落としているのかもしれませんが、今回は、このまま進めます。


 続いてshift関数の動作確認をします。先ほど作成したサンプルデータテーブルの値を少し増やし、時系列データのカラムを新規に追加しました。

import pandas as pd
from datetime import datetime as dt

df = pd.DataFrame({
    'col1' : ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
    'col2' : ['J', 'I', 'H', 'G', 'F', 'E', 'D', 'C', 'B', 'A'],
    'col3' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    'col4' : [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
    'col5' :['あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ'],
    'col6' :['こ', 'け', 'く', 'き', 'か', 'お', 'え', 'う', 'い', 'あ'],
    'col7' :[dt(2019, 1, 1), dt(2019, 2, 1), dt(2019, 3, 1), dt(2019, 4, 1), dt(2019, 5, 1), 
                dt(2019, 6, 1), dt(2019, 7, 1), dt(2019, 8, 1), dt(2019, 9, 1), dt(2019, 10, 1)],
})



 データの内容は、以下の通りです。

f:id:Pimientito:20190429181538p:plain
サンプルデータテーブル2の内容


 このテーブルに対しshift関数を用いて、指定した行数前または後のデータを取得して新しいカラムに代入します。shift関数を使用したコードは以下の通りです。

#パラメータ「periods」に正数を指定(古い日付データの取得)
df['periods=2'] = pd.Series(df['col7'].shift(periods=2))

#パラメータ「periods」に負数を指定(新しい日付データの取得)
df['periods=-2'] = pd.Series(df['col7'].shift(periods=-2))

#結果の表示
df

参考・参照元:「python_awesome.py」(抜粋)(p.107)を参考に作成


 shift関数を使用した結果は、以下の通りです。

f:id:Pimientito:20190429182148p:plain
shift関数の結果


 パラメータ「periods」に正数を指定すると、基準位置から上位の行(時系列データ昇順並びの場合、古い日付データ)へ移動し、負数を指定すると基準位置から下位の行(時系列データ昇順並びの場合、新しい日付データ)へ移動します。(図中の'NaT'は、日付型の欠損値(欠測値)を表す'Not a Time'です)


 筆者は、正数値は下位行へ、負数値は上位行へ移動するものと勝手にイメージしていました。pandas 0.24.2 documentation「pandas.DataFrame.shift」を確認したところ、正数を指定すると新しい日付(下位行)へ進むような記載になっていましたが、今回の動作確認では、逆の動きをしていました。


 どちらが正しいのか、現状分かりませんが、思い込みや、記憶違いを避けるためにも、使用する関数は、都度、ドキュメントや資料の確認を怠らないほうが良いでしょう。


過去n件の合計値の取得

 続いて、ある区間の行を指定して、過去データを取得する構文です。


 著者によると、PythonにはWindow関数が提供されていないため、SQLでは容易にできる処理を、Pythonで実現するためには、少々、長いコードになってしまうそうです。


 サンプルコードは、以下の通りです。

result[new_column] = 
    pd.Series(data_table.groupby(grouping_column)
    .apply(lambda x : x.sort_values(by=datetime_column, ascending=True))
    .loc[ : , target_column].rolling(center=False, window=m, min_periods=n)
    .some_function().reset_index(drop=True))

参考・参照元:「python_awesome.py」(抜粋)(p.112)を参考に作成


【補足】

項目 概要
result データセットから取得した結果
new_column 新規作成カラム
data_table データセット(データの源泉)
grouping_column グループ化対象カラム
datetime_column ソート対象日付型カラム
target_column 取得対象カラム
m パラメータ「window」の値
集約関数の処理範囲
n パラメータ「min_periods」の値
最小データ個数
some_function 集約関数


 このサンプルコードでは、以下のように動作します。


  1. カラム「grouping_column」でグループ化

  2. カラム「datetime_column」を時系列に昇順で並び替え

  3. カラム「target_column」を処理対象として、rolling関数でwindow化

  4. windowで分割された範囲に対して、集約関数「some_function」を実行



 サンプルコードだけでは、処理の内容が具体的にイメージできないため、サンプルデータを作成して、実際に動かしてみます。


 はじめに、架空の受注一覧を作成します。

import pandas as pd
from datetime import datetime as dt

df = pd.DataFrame({
    'UserID' : ['A001', 'A002', 'A003', 'A001', 'A002', 'A003', 'A001', 'A002', 'A003', 'A001', 'A002', 'A003'],
    'ProductID' : ['B001', 'D002', 'B001', 'B001', 'B001', 'D002', 'B001', 'D002', 'D002', 'D002', 'D002', 'B001'],
    'Type' : ['Blue-ray', 'DVD', 'Blue-ray', 'Blue-ray', 'Blue-ray', 'DVD', 'Blue-ray', 'DVD', 'DVD', 'DVD', 'DVD', 'Blue-ray'],
    'Price' : [50, 20, 50, 50, 50, 20, 50, 20, 20, 20, 20, 50],
    'Count' :[100, 50, 100, 150, 200, 50, 250, 100, 50, 100, 150, 50],
    'Date' :[dt(2019, 5, 1), dt(2019, 5, 3), dt(2019, 5, 5), dt(2019, 5, 5), dt(2019, 5, 8), dt(2019, 5, 10), 
                dt(2019, 5, 4), dt(2019, 5, 8), dt(2019, 5, 4), dt(2019, 5, 2), dt(2019, 5, 7), dt(2019, 5, 3)],
})


 作成した受注一覧は、以下の通りです。

f:id:Pimientito:20190430172403p:plain
受注一覧(サンプル)


 作成した受注一覧には、商品単価(Price)と受注個数(Count)があり、これらを掛け合わせて、1データレコード単位で合計金額を算出し、新規カラム「Total」へ格納します。

df['Total'] = df['Price'] * df['Count']


 合計金額のカラムを追加した受注一覧は、以下の通りです。

f:id:Pimientito:20190430173308p:plain
受注ごとに合計金額を追加した受注一覧(サンプル)


 次にサンプルコードの前半部分、グループ化と時系列データをソートする部分の結果を表示します。ここでは「UserID(顧客ID)」でグループ化したパターンと、「Type(商品種別)」でグループ化したパターンで確認します。

#UserIDでグループ化し、Dateを昇順でソート
df.groupby('UserID').apply(lambda x : x.sort_values(by='Date', ascending=True))


 「UserID」でグループ化した結果は、以下の通りです。

f:id:Pimientito:20190430183358p:plain
UserIDでグループ化した結果


 赤枠の部分が、UserID単位でグループ化された結果で、青枠の部分が、グループごとに古い日付から新しい日付に並び替えた結果です。


 同じように、今度は「Type」でグループ化します。

df.groupby('Type').apply(lambda x : x.sort_values(by='Date', ascending=True))


 「Type」でグループ化した結果は、以下の通りです。

f:id:Pimientito:20190430185514p:plain
Typeでグループ化した結果


 こちらの結果も、赤枠がグループ化された結果、青枠はグルーブごとに日付順で並び替えた結果となります。以上の結果を踏まえて、次はrolling関数の動きを確認します。


 サンプルコードでは、sum関数が使われていましたが、ここではmean関数(平均)を使用して動作を確認します。主な処理は、以下の通りです。


  • カラム「Type(商品種別)」でグループ化して、日付の古い順に並び替える。

  • 集約対象カラムは「Total(合計)」

  • ウィンドウサイズは3とし、ウィンドウ内に1個でも数値データがあれば平均金額を算出する。

  • rolling関数のパラメータ「center」を設定し、自身と前後1回分の受注、計3回分の受注平均金額を算出する。



 実際のコードは、以下の通りです。

df['mean'] = pd.Series(df.groupby('Type')
                       .apply(lambda x : x.sort_values(by='Date', ascending=True))
                       .loc[ : , 'Total'].rolling(window=3, min_periods=1, center=True).mean()
                       .reset_index(drop=True))


 結果は、以下の通りです。

f:id:Pimientito:20190430201910p:plain
受注3回分の平均受注金額


 rolling関数のwindow概要図は、以下の通りです。

f:id:Pimientito:20190501010345p:plain
rolling関数のwindow概要図


 最後に、サンプルコード末尾にあるreset_index(drop=True)関数ですが、この関数を省略すると集約関数の結果を正しく新しいカラムに格納することができません。実際に、どのような値が返されるのか確認します。


f:id:Pimientito:20190430205317p:plain
reset_index(drop=True)の有無による結果を比較


 今回の学習したサンプルコードの中で使用されている関数について、大まかに動作確認を行ないました。


 それぞれの関数には、今回ご紹介したパラメータ以外にも複数あり、中には公式ドキュメントを何度も読み返し、コーディングしても、なかなか理解できないものもありました。


 すべてを頭に記憶することは難しいですが、都度、ドキュメントを読み、理解するしかありません。


今回の学習

 「今回のテーマ」の内容を踏まえて、最初にテストデータの作成を行います。まず、どのようなデータが、今回の学習に適しているのか検討します。


テストデータの概要

条件

 以下の条件が含まれているデータが、今回の学習に適していると考えます。

  • 連続した日付型データを含んでいること。

  • 時系列で変化する要素を含んでいること。


テストデータの選択

 今回のテストデータは、前回【前処理の学習-28】で使用したカナダ トロントにある「Quandl」の株価情報を引き続き使用します。

f:id:Pimientito:20190317113820j:plain
「Quandl」トップページ


 データの詳細については、前回【前処理の学習-28】をご覧ください。

pimientito-handson-ml.hatenablog.com


テストデータの加工(前処理の前処理)

 今回の学習では、Pythonでコーディングするため、データの加工もPython上で行ないます。


テストデータを使って学習

 前回【前処理の学習-28】では「Quandl」のAPIを使用して、「The Walt Disney Campany」と「Microsoft Corporation」の株価データを取得し、2018年5月1日〜5月31日の1ヶ月間の株価変動を確認しました。今回も、同じデータセットや条件で、学習を進めます。


[Python]

 はじめに「Quandl」APIで、「The Walt Disney Campany」と「Microsoft Corporation」の株価データを取得します。

#【前処理の学習-29】データを学ぶ ~結合~④(Python編)
#過去データから情報を取得

#Quandl API を使用して、株価情報のデータセットを取得

#Quandl API
import quandl

#Quandl API 証明キーのセット(証明キーセットは伏字(*)に置き換えています)
quandl.ApiConfig.api_key = "****"

#データセット読み込み(The Walt Disney Company Stock Prices)
data_DIS = quandl.get('EOD/DIS')

#データセット読み込み(Microsoft Corporation Stock Prices)
data_MSFT = quandl.get('EOD/MSFT')


 取得したデータセットの内容は、以下の通りです。

f:id:Pimientito:20190501211809p:plain
取得した株価情報(The Walt Disney Company(DIS))


 続いてカラム「Date」が、インデックスとして設定されているため、reset_index関数を使って、インデックスを解除(新規インデックスを追加)します。

#reset_indexで、カラム'Date'をインデックス解除
data_DIS = data_DIS.reset_index()
data_MSFT = data_MSFT.reset_index()


 インデックスを解除した結果は、以下の通りです。

f:id:Pimientito:20190501212521p:plain
カラム「Date」からインデックスを解除


 次に、必要な情報のみ抜き出します。

#必要な情報のみ取得
df_DIS = data_DIS[['Date', 'Open', 'High', 'Low', 'Close']]
df_MSFT = data_MSFT[['Date', 'Open', 'High', 'Low', 'Close']]


 抜き出した結果は、以下の通りです。

f:id:Pimientito:20190501213122p:plain
必要項目のみ取得した結果


 使用する期間を絞り込みます。今回は、2018年5月1〜2018年5月31日の株価情報を使用して学習しますが、前週の平均株価との比較を行うため、5月1日〜5月7日までの比較対象として、4月20日の株価情報から取得します。(期間の絞り込み:2018年4月20日〜5月31日)

#2018.04.20〜2018.05.31の株価情報を取得
df_mrkt_2018_May_DIS = \
        df_DIS.loc[(df_DIS['Date'] >= '2018-04-20') & (df_DIS['Date'] < '2018-06-01'), \
                   ['Date', 'Open', 'High', 'Low', 'Close']].reset_index(drop=True)

df_mrkt_2018_May_MSFT = \
        df_MSFT.loc[(df_MSFT['Date'] >= '2018-04-20') & (df_MSFT['Date'] < '2018-06-01'), \
                   ['Date', 'Open', 'High', 'Low', 'Close']].reset_index(drop=True)


 絞り込んだ結果は、以下の通りです。

f:id:Pimientito:20190501214400p:plain
データの期間の絞り込み


 次に、rolling関数を使って集約しますが、rolling関数のwindowは、比較対象行(現在行)まで含んでしまうため、前週一週間のデータと比較するためには、少々手間が必要となります。


 一例ですが、今回は前週の平均株価を算出するために、以下の手順で「前週平均株価」を算出しました。


  1. 「前週一週間+比較対象行(現在行)=8日間」でwindowを生成し、全ての値を合計します。(仮に「Sum_8D」と呼びます)

  2. 続いて「Sum_8D」から、比較対象行(現在行)の株価を減算します。

  3. 残りの「Sum_8D」の値を、7(一週間)で除算します。


 実際に作成したコードは、以下の通りです。はじめに8日間分の株価を、rolling関数を使って集約します。(今回の学習では、groupby関数やsort_values関数を使用する必要が無かったため、割愛しています。)

#過去7日間+1日(計8日間)のClose価格を合算
df_mrkt_2018_May_DIS['Sum_8D'] = \
        round(df_mrkt_2018_May_DIS['Close'].rolling(window=8, min_periods=8).sum(), 2)

df_mrkt_2018_May_MSFT['Sum_8D'] = \
        round(df_mrkt_2018_May_MSFT['Close'].rolling(window=8, min_periods=8).sum(), 2)


 8日間の株価合計は、以下の通りです。

f:id:Pimientito:20190501221925p:plain
8日間の株価合計を算出


 週間平均株価を算出する前に、データの範囲を5月1日〜5月31日に絞り込みました。

#株価情報を2018年5月分のみ再取得
df_mrkt_2018_May_DIS = \
        df_mrkt_2018_May_DIS.loc[(df_mrkt_2018_May_DIS['Date'] >= '2018-05-01') & (df_mrkt_2018_May_DIS['Date'] < '2018-06-01'), \
                   ['Date', 'Open', 'High', 'Low', 'Close', 'Sum_8D']].reset_index(drop=True)

df_mrkt_2018_May_MSFT = \
        df_mrkt_2018_May_MSFT.loc[(df_mrkt_2018_May_MSFT['Date'] >= '2018-05-01') & (df_mrkt_2018_May_MSFT['Date'] < '2018-06-01'), \
                   ['Date', 'Open', 'High', 'Low', 'Close', 'Sum_8D']].reset_index(drop=True)


 株価情報を5月分のみに絞り込んだ結果は、以下の通りです。

f:id:Pimientito:20190501222743p:plain
株価情報(2018年5月分)


 8日間分の「Close(終値)」から、比較対象行(現在行)の「Close」を減算し、7(一週間)で除算した結果を、新規カラム「Avg_lastweek」へ格納します。

#8日間のClose合計値から、比較対象行(現在行)のClose値を減算(過去一週間の株価合計値のみ)
#残った値を、7(一週間)で除算して平均株価を算出

df_mrkt_2018_May_DIS['Avg_lastweek'] = \
       round( (df_mrkt_2018_May_DIS['Sum_8D'] - df_mrkt_2018_May_DIS['Close']) / 7, 2)

df_mrkt_2018_May_MSFT['Avg_lastweek'] = \
       round( (df_mrkt_2018_May_MSFT['Sum_8D'] - df_mrkt_2018_May_MSFT['Close']) / 7, 2)


 結果は、以下の通りです。

f:id:Pimientito:20190501224251p:plain
前週の平均株価の算出


 他の処理に入る前に、不要となったカラム「Sum_8D」を削除します。

#不要なカラム「Sum_8D」を削除
df_mrkt_2018_May_DIS.drop('Sum_8D', axis=1, inplace=True)
df_mrkt_2018_May_MSFT.drop('Sum_8D', axis=1, inplace=True)


 カラム「Sum_8D」を削除した結果は、以下の通りです。パラメータ「inplace」をTrueに設定することで、恒久的に処理結果が、変数に反映されます。(デフォルトは「False」です。変数内の値を更新したくない場合は、Falseのままにします。)


f:id:Pimientito:20190501231137p:plain
カラム「Sum_8D」を削除した結果


 最後に、比較対象行(現在行)のカラム「Close(終値)」から、前週の平均株価を引くことで損益を確認します。

#前週のClose値平均額と比較
df_mrkt_2018_May_DIS['Comp_lastweek'] = \
        df_mrkt_2018_May_DIS['Close'] - df_mrkt_2018_May_DIS['Avg_lastweek']

df_mrkt_2018_May_MSFT['Comp_lastweek'] = \
        df_mrkt_2018_May_MSFT['Close'] - df_mrkt_2018_May_MSFT['Avg_lastweek']


 比較した結果は、以下の通りです。

f:id:Pimientito:20190501231927p:plain
前週との終値の比較結果


 以上で、前回【前処理の学習-28】と同じ学習結果を取得できました。最後に、前回の学習同様に、ロウソク足チャートで、プロットします。


「可視化」に挑戦

可視化の目的

 前回【前処理の学習-28】のプロット図との比較


[matplotlib]ロウソク足チャート

 【前処理の学習-28】のコードに少し手を加えて、今回のデータをプロットしています。

#可視化(【前処理の学習-28】のコードを、一部改変)
#Matplotlib mpl_financeライブラリを利用して可視化

import pandas as pd
import matplotlib.pyplot as plt
from mpl_finance import candlestick2_ohlc
from datetime import datetime as dt


# プロットオブジェクトの初期設定
fig = plt.figure(figsize=(16, 13))
ax1 = plt.subplot(2, 1, 1)
ax2 = plt.subplot(2, 1, 2)


#The Walt Disney Company Stock Prices(May, 2018)
# candlestick2でローソク足を描画
candlestick2_ohlc(ax1, df_mrkt_2018_May_DIS["Open"], df_mrkt_2018_May_DIS["High"], \
                  df_mrkt_2018_May_DIS["Low"], df_mrkt_2018_May_DIS["Close"], \
                  width=0.2, colorup="b", colordown="r", alpha=1)

#前週平均株価
ax1.plot(df_mrkt_2018_May_DIS["Avg_lastweek"])

#タイトル設定
ax1.set_title("The Walt Disney Company")

# X軸の設定
#ラベルの設定(rotation: ラベル表示角度(例:45→45°)
ax1.set_xticklabels(df_mrkt_2018_May_DIS["Date"],rotation=45,fontsize='small')
# 目盛のデータ数が可変の場合、range関数にshape属性の値を渡すと流動的に範囲を変更できます。
ax1.set_xticks(range(df_mrkt_2018_May_DIS.shape[0]))
ax1.set_xlabel("Day")

# 横軸の範囲をデータ個数とする(0〜shape属性の配列[0]番目を指定)
ax1.set_xlim([0, df_mrkt_2018_May_DIS.shape[0]])
ax1.set_ylabel("Price")



#Microsoft Corporation Stock Prices(May, 2018)
candlestick2_ohlc(ax2, df_mrkt_2018_May_MSFT["Open"], df_mrkt_2018_May_MSFT["High"], \
                  df_mrkt_2018_May_MSFT["Low"], df_mrkt_2018_May_MSFT["Close"], \
                  width=0.2, colorup="b", colordown="r", alpha=1)

#前週平均株価
ax2.plot(df_mrkt_2018_May_MSFT["Avg_lastweek"])

ax2.set_title("Microsoft Corporation")

ax2.set_xticklabels(df_mrkt_2018_May_MSFT["Date"], rotation=45,fontsize='small')
ax2.set_xticks(range(df_mrkt_2018_May_MSFT.shape[0]))
ax2.set_xlabel("Day")

ax2.set_xlim([0, df_mrkt_2018_May_MSFT.shape[0]])
ax2.set_ylabel("Price")

#サブプロット間の空白調整
plt.subplots_adjust(wspace=0, hspace=0.5)

#グリッド表示
ax1.grid(True)
ax2.grid(True)

#凡例の表示
ax1.legend()
ax2.legend()

#プロット表示
plt.show()


 出力したプロット図は、以下の通りです。

f:id:Pimientito:20190501234948p:plain
今回作成したプロット図


 前回【前処理の学習-28】で作成したプロット図は、以下の通りです。

f:id:Pimientito:20190323175230p:plain
【前処理の学習-28】で作成したプロット図


 前回も、今回も同じデータセットを使用しているため、全体的に同じようにプロットされていますが、今回は、前週平均株価(平均終値)のプロットも重ねてみました。


今回のまとめ

 前回、今回と二回に渡って、同じテーマをSQLPythonで学習しました。


 作業工数の視点から見た場合、個人的な所感ですが、Pythonだけで進めるには、SQLより手間が掛かる印象が強いです。筆者のPythonの習熟度の低さを加味しても、Pythonによる作業は、ライブラリや関数を、もっと学習する必要があり、SQL以上にハードルが高いと感じました。(ただし、SQLの副問い合わせが、何重にも重なる場合においては、SQLの方が楽とも言い難いと思います)


 著者は、参考資料の中で「SQLでできるところはSQLで」と述べていらっしゃいますが、今の筆者のレベルでは、今後も引き続きSQLPythonも学習しなければ、その違いが分からないと思っています。


 最後に「前処理大全」の著者 本橋智光氏が、執筆時のご苦労を語っていらっしゃる対談記事をご紹介して、今回の学習を終えたいと思います。



  『仕事ではじめる機械学習』&『前処理大全』著者対談

www.oreilly.co.jp




 今回は、以上です。



【参考資料】

pandas 0.24.2 documentation「pandas.Series.sort_values」

pandas.pydata.org


pandas 0.24.2 documentation「pandas.DataFrame.sort_values」

pandas.pydata.org


SciPy.org「numpy.sort」

docs.scipy.org


Youtube「When should I use the "inplace" parameter in pandas ?」

www.youtube.com


note.nkmk.me「pandasで窓関数を適用するrollingを使って移動平均などを算出」

note.nkmk.me


本ブログに関するお問い合わせ先

 筆者自身の「機械学習」の学習のため、参考にさせていただいている資料や、Web情報に対して、情報元の権利を侵害しないよう、できる限り、細心の注意を払って、ブログの更新に努めておりますが、万が一、関係各所の方々に、不利益が生じる恐れがある場合、大変お手数ですが、下記の「お問い合わせ先」まで、ご一報いただけますようお願いいたします。



 お問い合わせ先:otoiawase@handson-t-arte.com