一端の何かになれるか

一生懸命は眩しい

識別(分類)と回帰

 
分類(識別)回帰を理解するのにちょっと戸惑ったのでメモ。

決定木分析とロジスティック回帰分析の理解度を深めようと振り返りをしていたところつまづいた。

Webで検索してみると書き手によってこちらの解釈が微妙に変わってしまい、ちょっとニュアンスが掴みづらかった。自分なりに調べて解釈した結果をメモっておく。

この記事では、分類(識別)と識別(分類)を意識的に書き分けています。

 

■ 識別(分類)と回帰とは

識別(分類)と回帰のどちらも、統計学において要素を複数のクラス(グループ)に分類するために線引きをすることを指す。

 

−私的な解釈のポイント

  • 分類(識別)と回帰と表現されるとめちゃくちゃ混乱する
    (要素をクラスに分類するための方法に、分類回帰があるとかなに言ってだ)

  • 回帰回帰分析を分けて考えないとまあまあ混乱する
    (回帰で分類するのと、回帰で得られたモデルで予測(分析)を行うのは別の話)

 

○ 識別(分類)

 離散値の目的変数をクラス分けするために線引きすることを識別(分類)という。

  • 識別と覚えた方がイメージが湧きやすい
  • 決定木分析では、訓練データから分類器を生成し、その分類器を利用してデータを分類する

○ 回帰

 連続値の目的変数をクラス分けするために線引きすることを回帰という。
目的変数と説明変数の間にモデルを当てはめて、目的変数をクラスに分類する。

  • 回帰分析では、目的変数と説明変数の間に当てはめたモデルを利用して数値を予測する

 

■ 連続値と離散値

ついでにまとめておく。
連続値とも離散値とも取れる場合があって、ちょっとややこしいけどまあ大丈夫。

○ 連続値

測ることができるデータ、連続していて細かく測ることができる
数直線上に書くと、どこにでも存在しうるデータ

例) 身長、体重、時間、気温など

○ 離散値

数えることができるデータ、それ以上細かくすることができない
数直線上に書くと、目盛の上に存在するデータ

例) 人数、回数など

 

○ 連続、離散どちらとも取れるケース

  • 連続値の場合
    身長や体重は、cmやkgなどの単位で表されることから離散値とも考えられる。便宜的に近似値に直しているとして、基本的には連続値として扱うことが多い。

  • 離散値の場合
    テストの点数をその人の能力を階級値で示すものと考え、75点は74.5〜75.4点を取れる能力とし、連続値として扱う場合がある。

 

参考にしたサイト

分類 (統計学) - Wikipedia

回帰分析 - Wikipedia

なるほど統計学園高等部 | データの種類

 
 

統計検定2級を受験する

f:id:select_from_where:20170930085016j:plain

  • 統計検定とは
    • 試験概要
    • 資格の種類
    • 日程
    • 申し込み方法
    • 2級の合格ラインと合格率
    • (おまけ)成績優秀者
  • 2級の試験内容について詳しく
  • なぜやるのか
    • 曲がりなりにもデータアナリストを名乗りたいから
    • 資格なんて取得しても役に立たない論
      • 異論はない
      • 要は役に立てようとしているかどうか
    • 統計学には普遍的な魅力がある
    • なぜブログに書いたのか
      • 人の目がある気分になれる
  • どう勉強していくのか
    • 使う教材
    • 平日と休日の勉強法
  • 目標
    • 8割とって合格
    • 実務に投影し、活かしていく

統計検定とは

試験概要

一般財団法人 統計質保証推進協会が主催、日本統計学会公式認定・総務省後援の全国統一試験。 統計検定は2011年に発足したということで、比較的新しい資格試験のようだ。

[公式サイト][f6e19f98]によると、

「統計検定」とは、統計に関する知識や活用力を評価する全国統一試験です。 データに基づいて客観的に判断し、科学的に問題を解決する能力は、仕事や研究をするための21世紀型スキルとして国際社会で広く認められています。 日本統計学会は、中高生・大学生・職業人を対象に、各レベルに応じて体系的に国際通用性のある統計活用能力評価システムを研究開発し、統計検定として資格認定します。

資格の種類

いわゆる受験資格は設けられておらず、特に制限なく受験することができる。 試験時間帯の異なる種別であれば、併願することも可能。

こちらも[公式サイト][f6e19f98]から引用。

試験の種別 試験内容
統計調査士 統計に関する基本的知識と利活用
専門統計調査士 調査全般に関わる高度な専門的知識と利活用手法
1級 実社会の様々な分野でのデータ解析を遂行する統計専門力
準1級 統計学の活用力 ─ データサイエンスの基礎
2級 大学基礎統計学の知識と問題解決力
3級 データの分析において重要な概念を身に付け、身近な問題に活かす力
4級 データや表・グラフ、確率に関する基本的な知識と具体的な文脈の中での活用力

RSS/JSS試験なる、英国王立統計学会(Royal Statistical Society)との共同認定試験があったようだが、2017年5月を最後に同形態の試験を終了とのこと

難易度は上から順、今回私が受験する 2級 は、

大学基礎統計学の知識と問題解決力

大学基礎統計学…これは落とせない。

続きを読む

scikit-learnでクラスタリング分析を試す

とにかく試して見るシリーズ第3弾。
有効な分析か否かに関わらず、全試行過程を掲載します。
誰も見てないと思うんですけど、「ここちゃんと意識したほうがいいよ」、「そこわかってないね全然ダメだよ」、とかコメントついたらむちゃくちゃ嬉しいです。

なぜやるのか

職場でうんうん言いながらクラスタリングしてる人がいたので、やって見たくなった。
おじさんもQueryばっか書いてないで分析できるようにならないと。

クラスタリングとは

概要

  • 教師なしデータ分類の手法
  • データの集まりをデータ間の類似度に従って、いつかのグループに分ける
  • 階層的手法と非階層的手法に分類される

階層的手法

各データを1つのクラスタとし、クラスタ間の距離や類似度に基づいてクラスタを逐次的に併合していく手法

  • 最短距離法 (nearest neighbor method)
  • 最長距離法 (furthest neighbor method)
  • 群平均法 (group average method)
  • ウォード法 (Ward’s method)

非階層的手法

データの分割の良さを表す評価関数によって、最適解を探索する手法 階層的手法ではデータが多いと階層構造が複雑になってしまうため、非階層的手法のほうが実用的

  • k-means法(k平均法)

参考にしたサイト*1

参考にしたサイト*2

ビジネスでの活用例

市場細分化に基づくターゲット市場の選定

・顧客をセグメンテーションし、セグメントに適合するマーケティング施策を展開する
・顧客のどの属性をセグメンテーション変数として使うべきかは、分析の目的に合わせて選別する必要がある
コトラーによる顧客市場の主要なセグメンテーション変数には、以下4つがある

  1. 人口統計学的変数 (Demographic Variables)
  2. 地理的変数 (Geographic Variables)
  3. 心理的変数 (Psychographic Variables)
  4. 行動変数 (Behavioral Variables)

製品ポジショニングによる差別化戦略の策定

・競合製品の属性でクラスタ分析を行うことで、競合製品に対する優位性に着目して製品のポジションを確立するなど差別化を図る
・競合が参入していない空白マーケットを発見し、新規市場を開拓する

テストマーケットにおけるマーケティング施策の評価

・マーケットのクラスタ分析を実施することで、テストマーケット※の有効性を保証する
 ※マーケティング施策を展開する前に、少数の顧客からなるテストマーケットでテストを行い、事前評価を行うのが一般的

クラスタ分析を行う際に注意すべきこと

クラスタ分析は探索的な分析手法のため、複数回の分析結果を比較したり、異なる手法で分析結果を確認したりして、
 地道な検証作業を繰り返すことで意思決定に有用な分析結果を導き出さなければならない
・欠損値の有無や標準化の必要性に関する確認が事前作業として不可欠 1. データから外れ値を除外すること 2. 分析データから分類理由を正しく説明できるかを常に考え、分析目的をサポートする属性を見極めること

参考にしたサイト*3

今回使用するデータ

Wholesale customers Data Set (卸売業者の顧客データ)

例によって、カリフォルニア大学アーバイン校(University of California, Irvine)のMachine Learning Repositoryから取得した
事例がいくつも転がっていて、のちにサンプルをいくつか見れて助かることになった

データセットの情報(Data Set Information)

ポルトガルの卸売業者の顧客のデータ(2011年(通年)、通貨単位の年間支出)。
大元のデータベースからサンプリングしたデータセット
卸売の取引額なので、利益ではないことに注意したい。

以下、英訳のニュアンスが難しかったので、誤っている可能性あり。。  

1)FRESH  :生鮮品の年間支出(通貨単位)
2)MILK   :乳製品の年間支出(通貨単位)
3)GROCERY:食料品の年間支出(通貨単位)
4)FROZEN :冷凍品の年間支出(通貨単位)
5)DETERGENTS_PAPER:洗剤、紙製品の年間支出(通貨単位)
6)DELICATESSEN:デリカテッセン(惣菜)の年間支出(通貨単位)
7)CHANNEL:販売チャネル - Horeca(ホテル/レストラン/カフェ)またはその他の販売チャネル
8)REGION :消費地域 - リスボン、ポルト、その他

統計量:

Product Minimum Maximum Mean Std Deviation
FRESH 3 112151 12000.30 12647.329
MILK 55 73498 5796.27 7380.377
GROCERY 3 92780 7951.28 9503.163
FROZEN 25 60869 3071.93 4854.673
DETERGENTS_PAPER 3 40827 2881.49 4767.854
DELICATESSEN 3 47943 1524.87 2820.106
REGION Frequency
Lisbon 77
Oporto 47
Other Region 316
Total 440
CHANNEL Frequency
Horeca 298
Retail 142
Total 440

試行過程と結果

まずはデータを見てみる

データをダウンロードしてjupyter notebookにインポートする

rawデータを確認

import pandas as pd
import_df = pd.read_csv('Wholesale customers data.csv')
import_df.head()
Channel Region Fresh Milk Grocery Frozen Detergents_Paper Delicassen
0 2 3 12669 9656 7561 214 2674 1338
1 2 3 7057 9810 9568 1762 3293 1776
2 2 3 6353 8808 7684 2405 3516 7844
3 1 3 13265 1196 4221 6404 507 1788
4 2 3 22615 5410 7198 3915 1777 5185

ChannelとRegionの値が何を示しているのか不明なので特定する

まずはChannel

import_df.groupby(['Channel']).count()
Region Fresh Milk Grocery Frozen Detergents_Paper Delicassen
Channel
1 298 298 298 298 298 298 298
2 142 142 142 142 142 142 142

データセットの情報と突き合わせると

  • 1:Hoterica
  • 2:Retail
    であることがわかる。

Regionはどうだろうか?

import_df.groupby(['Region']).count()
Channel Fresh Milk Grocery Frozen Detergents_Paper Delicassen
Region
1 77 77 77 77 77 77 77
2 47 47 47 47 47 47 47
3 316 316 316 316 316 316 316

こちらもデータセットの情報と突き合わせて

  • 1:Lisbon
  • 2:Oporto
  • 3:Other Region
    であることがわかった。

データの特徴量

import_df.describe()
Channel Region Fresh Milk Grocery Frozen Detergents_Paper Delicassen
count 440.000000 440.000000 440.000000 440.000000 440.000000 440.000000 440.000000 440.000000
mean 1.322727 2.543182 12000.297727 5796.265909 7951.277273 3071.931818 2881.493182 1524.870455
std 0.468052 0.774272 12647.328865 7380.377175 9503.162829 4854.673333 4767.854448 2820.105937
min 1.000000 1.000000 3.000000 55.000000 3.000000 25.000000 3.000000 3.000000
25% 1.000000 2.000000 3127.750000 1533.000000 2153.000000 742.250000 256.750000 408.250000
50% 1.000000 3.000000 8504.000000 3627.000000 4755.500000 1526.000000 816.500000 965.500000
75% 2.000000 3.000000 16933.750000 7190.250000 10655.750000 3554.250000 3922.000000 1820.250000
max 2.000000 3.000000 112151.000000 73498.000000 92780.000000 60869.000000 40827.000000 47943.000000
  • 欠損値はない
  • 卸売額は、Fresh, Grocery, Milk, Frozen, Detergents_Paper, Delicassenの順で大きい
  • 小口から大口まで、多様な顧客を持っている

データの分布を見てみる

とりかかる前に、Channel, Regionごとのデータフレームに分割しておく

Channelごとのデータフレーム

# Horeca
df_Channel1 = import_df[import_df['Channel']==1].drop(['Channel'],axis=1)
# Retail
df_Channel2 = import_df[import_df['Channel']==2].drop(['Channel'],axis=1)

Regionごとのデータフレーム

# Lisbon
df_Region1 = import_df[import_df['Region']==1].drop(['Region'],axis=1)
# Oporto
df_Region2 = import_df[import_df['Region']==2].drop(['Region'],axis=1)
# Other Region
df_Region3 = import_df[import_df['Region']==3].drop(['Region'],axis=1)
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')
%matplotlib inline

Freshの分布

まずはChannelごと

plt.hist([df_Channel1['Fresh'],df_Channel2['Fresh']],
         bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], range=(0,60000))

f:id:select_from_where:20170924200316p:plain
Fresh_Channel

  • Channelごとでそれほど分布は変わらない

次はRegionごと

plt.hist([df_Region1['Fresh'],df_Region2['Fresh'],df_Region3['Fresh']],
         bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,60000))

f:id:select_from_where:20170924200401p:plain
Fresh_Region

  • Regionごとでもそれほど分布は変わらない

Milkの分布

Channelごと

plt.hist([df_Channel1['Milk'],df_Channel2['Milk']],
         bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,40000))

f:id:select_from_where:20170924200504p:plain
Milk_Channel

  • MilkはRetailの方が、取引先あたりの卸売額が大きい傾向がある

Regionごと

plt.hist([df_Region1['Milk'],df_Region2['Milk'],df_Region3['Milk']],
         bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,40000))

f:id:select_from_where:20170924200543p:plain
Milk_Region

  • Regionごとではそれほど分布は変わらない

Groceryの分布

Channelごと

plt.hist([df_Channel1['Grocery'],df_Channel2['Grocery']],
        bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,40000))

f:id:select_from_where:20170924200622p:plain
Grocery_Channel

  • GroceryはRetailの方が、取引先あたりの卸売額が大きい傾向がある

Regionごと

plt.hist([df_Region1['Grocery'],df_Region2['Grocery'],df_Region3['Grocery']],
        bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,40000))

f:id:select_from_where:20170924200655p:plain
Grocery_Region

  • RegionごとではOther Region方が、取引先あたりの卸売額が大きい傾向がある

Frozenの分布

Channelごと

plt.hist([df_Channel1['Frozen'],df_Channel2['Frozen']],
        bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,20000))

f:id:select_from_where:20170924200728p:plain
Frozen_Channel

  • FrozenはHorecaの方が、取引先あたりの卸売額が大きい傾向がある

Regionごと

plt.hist([df_Region1['Frozen'],df_Region2['Frozen'],df_Region3['Frozen']],
        bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,20000))

f:id:select_from_where:20170924200811p:plain
Frozen_Region

Detergents_Paperの分布

Channelごと

plt.hist([df_Channel1['Detergents_Paper'],df_Channel2['Detergents_Paper']],
        bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,25000))

f:id:select_from_where:20170924200845p:plain
Paper_Channel

  • 洗剤、紙製品はRetailの方が、取引先あたりの卸売額が大きい傾向がある

Regionごと

plt.hist([df_Region1['Detergents_Paper'],df_Region2['Detergents_Paper'],df_Region3['Detergents_Paper']],
        bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,25000))

f:id:select_from_where:20170924200934p:plain
Paper_Region

  • Regionごとではそれほど分布は変わらない

Delicatessenの分布

Channelごと

plt.hist([df_Channel1['Delicassen'],df_Channel2['Delicassen']],
        bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,20000))

f:id:select_from_where:20170924201008p:plain
Dericatessen_Channel

  • Channelごとではそれほど分布は変わらない

Regionごと

plt.hist([df_Region1['Delicassen'],df_Region2['Delicassen'],df_Region3['Delicassen']],
        bins=15, label=['Lisbon','Oporto','Other Region'], color=['#2EFE2E','#FE2E2E','#F7FE2E'], rwidth=100, range=(0,20000))

f:id:select_from_where:20170924201044p:plain
Dericatessen_Region

  • Regionごとではそれほど分布は変わらない

グループ化してクロス集計してみる

Regionでグループ化

データの情報にあったが、取引先の数を見てみる。合計行を追加しておく

import_df['Total'] = import_df[['Fresh','Milk','Grocery','Frozen','Detergents_Paper','Delicassen']].sum(axis=1)
import_df[['Region','Total']].groupby(['Region']).count()
Total
Region
1 77
2 47
3 316
  • 取引先の数は、Other Region, Lisbon, Oportoの順で多い

取引額は、取引先の数に比例しそうだがどうだろうか

import_df.drop('Channel',axis=1).groupby(['Region']).sum()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Region
1 854833 422454 570037 231026 204136 104327 2386813
2 464721 239144 433274 190132 173311 54506 1555088
3 3960577 1888759 2495251 930492 890410 512110 10677599
  • 取引額も、Other Region, Lisbon, Oportoの順で多い
  • 卸業者なので、ロジスティクスを考えると取引先との距離と関係が深いかもしれない

取引先あたりの卸売額を見てみる

import_df.drop('Channel',axis=1).groupby(['Region']).mean()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Region
1 11101.727273 5486.415584 7403.077922 3000.337662 2651.116883 1354.896104 30997.571429
2 9887.680851 5088.170213 9218.595745 4045.361702 3687.468085 1159.702128 33086.978723
3 12533.471519 5977.085443 7896.363924 2944.594937 2817.753165 1620.601266 33789.870253
  • Grocery, Frozen, Detergents_Paperは取引先あたりの卸売額順が、Regionごとの卸売額順と異なる
  • OportoのGrocery, Frozen, Detergents_Paperは、Regionごとの卸売額の割に取引先あたりの卸売額が大きい

Channelでグループ化

取引先の数を見てみる

import_df[['Channel','Total']].groupby(['Channel']).count()
Total
Channel
1 298
2 142
  • 取引先の数は、Horecaが倍近く多い

取引額は、取引先の数に比例しそうだがどうだろうか。

import_df.drop('Region',axis=1).groupby(['Channel']).sum()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Channel
1 4015717 1028614 1180717 1116979 235587 421955 7999569
2 1264414 1521743 2317845 234671 1032270 248988 6619931
  • Horecaは、Retailに対して倍近い取引があるにもかかわらず、卸売額はそれほど大きな差がない
  • ポルトガルの生活感がわからないので、データから解釈するには想像の域を出ないことが多い
  • 両方を比べると、Horecaの卸売額がRetailを上回るのは、Fresh, Frozen, Dericatessen
    日本の飲食店でバイトした経験から、Milkは専門の別卸売業者から、Groceryは加工食品のため少ないものと想像
  • その他の販売チャネルでFreshの額がHorecaと比べて小さいのは、Freshを専売している商店の集まった市場が多いのかも

  • Horeca

  • Fresh, Grocery, Frozenの順で卸売額が多い
  • Detergents_Paperは、特にHotelで専門の卸売業者から仕入れているか、もしくはRetailほど数は出ないということか
  • Delicatessenは、調理したものを客に出すため少ないのだろう

  • その他の販売チャネル(スーパーマーケット、商店等?)

  • Grocery, Milk, Freshの順で卸売額が多い

取引先あたりの卸売額を見てみる

import_df.drop('Region',axis=1).groupby(['Channel']).mean()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Channel
1 13475.560403 3451.724832 3962.137584 3748.251678 790.560403 1415.956376 26844.191275
2 8904.323944 10716.500000 16322.852113 1652.612676 7269.507042 1753.436620 46619.232394
  • 取引先ごとの取引額でみると、Delicatessenは取引先あたりの卸売額順が、販売チャネルごとの卸売額順と異なる
RegionとChannelでグループ化してクロス集計

取引先の数を見てみる

import_df[['Region','Channel','Total']].groupby(['Region','Channel']).count()
Total
Region Channel
1 1 59
2 18
2 1 28
2 19
3 1 211
2 105
  • 全取引先に対する地域ごとのHorecaの比率はリスボンが高い

取引額はどうだろうか

import_df.groupby(['Region','Channel']).sum()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Region Channel
1 1 761233 228342 237542 184512 56081 70632 1538342
2 93600 194112 332495 46514 148055 33695 848471
2 1 326215 64519 123074 160861 13516 30965 719150
2 138506 174625 310200 29271 159795 23541 835938
3 1 2928269 735753 820101 771606 165990 320358 5742077
2 1032308 1153006 1675150 158886 724420 191752 4935522
  • 利益をあげている地域と販売チャネルは
    1. . Other RegionのHoreca
    2. . Other RegionのRetail
    3. . LisbonのHoreca
  • 品目単位で利益をあげているのは
    1. . Other Region,Horeca,Fresh
    2. . Other Region,Retail,Grocery
    3. . Other Region,Retail,Milk
  • ポルトのHotericaは全体の特徴と異なり、生鮮品、冷凍品、食料品の順で卸売額が多い

取引先あたりの卸売額はどうだろうか

import_df.groupby(['Region','Channel']).mean()
Fresh Milk Grocery Frozen Detergents_Paper Delicassen Total
Region Channel
1 1 12902.254237 3870.203390 4026.135593 3127.322034 950.525424 1197.152542 26073.593220
2 5200.000000 10784.000000 18471.944444 2584.111111 8225.277778 1871.944444 47137.277778
2 1 11650.535714 2304.250000 4395.500000 5745.035714 482.714286 1105.892857 25683.928571
2 7289.789474 9190.789474 16326.315789 1540.578947 8410.263158 1239.000000 43996.736842
3 1 13878.052133 3486.981043 3886.734597 3656.900474 786.682464 1518.284360 27213.635071
2 9831.504762 10981.009524 15953.809524 1513.200000 6899.238095 1826.209524 47004.971429
  • Retailの方が、取引先あたりの卸売額が大きい
  • 取引先あたりの卸売額が多いのは
    1. Lisbon,Retail
    2. Other Region,Retail
    3. Oporto,Retail

分析の目的を定める

本音はただクラスタリングを試したいだけなのだが、分析が必要になった背景を仮定したい。
新たに現在取り扱っていない多品目を生産している生産者から生鮮食品を仕入れられることになった。
クラスタリングした結果から、新たな生鮮品の発注を取れそうな見込み顧客を割り出したいと仮定する。

クラスタリングを試す

クラスタリングに利用する項目を選定

MilkとDetergents_PaperはFreshと相関関係が薄いということにして除外しておく - Region - Channel - Fresh - Grocery - Frosen - Delicatessen

cluster_df = import_df.drop(['Milk','Detergents_Paper','Total'],axis=1)
cluster_df.head()
Channel Region Fresh Grocery Frozen Delicassen
0 2 3 12669 7561 214 1338
1 2 3 7057 9568 1762 1776
2 2 3 6353 7684 2405 7844
3 1 3 13265 4221 6404 1788
4 2 3 22615 7198 3915 5185

行列 (Array) に変換して転置する

import numpy as np
cluster_array = np.array([cluster_df['Channel'].tolist(),
                          cluster_df['Region'].tolist(),
                          cluster_df['Fresh'].tolist(),
                          cluster_df['Grocery'].tolist(),
                          cluster_df['Frozen'].tolist(),
                          cluster_df['Delicassen'].tolist()
                         ], np.int32)
cluster_array = cluster_array.T
cluster_array
array([[    2,     3, 12669,  7561,   214,  1338],
       [    2,     3,  7057,  9568,  1762,  1776],
       [    2,     3,  6353,  7684,  2405,  7844],
       ..., 
       [    2,     3, 14531, 30243,   437,  1867],
       [    1,     3, 10290,  2232,  1038,  2125],
       [    1,     3,  2787,  2510,    65,    52]], dtype=int32)

クラスタ分析を実行(本来はこれを何度も試行するが、今回は一回で)

from sklearn.cluster import KMeans
cluster = KMeans(n_clusters=6).fit_predict(cluster_array)
cluster
array([1, 3, 3, 1, 1, 3, 1, 3, 3, 2, 3, 1, 0, 1, 1, 3, 3, 3, 1, 3, 1, 3, 0,
       0, 1, 1, 3, 1, 2, 0, 1, 3, 1, 0, 3, 3, 0, 1, 2, 0, 1, 1, 2, 2, 3, 2,
       2, 5, 3, 2, 3, 3, 0, 3, 1, 3, 2, 3, 1, 3, 3, 5, 3, 2, 3, 2, 3, 1, 3,
       3, 1, 2, 3, 1, 3, 1, 3, 2, 3, 3, 3, 2, 3, 1, 1, 5, 2, 0, 3, 1, 1, 1,
       2, 1, 3, 3, 3, 3, 3, 3, 1, 2, 3, 0, 1, 1, 3, 2, 3, 2, 1, 2, 1, 1, 1,
       3, 3, 3, 1, 3, 1, 3, 1, 3, 0, 4, 1, 1, 3, 0, 3, 3, 1, 3, 3, 3, 3, 3,
       1, 3, 1, 0, 0, 3, 1, 2, 3, 3, 3, 0, 1, 3, 1, 3, 3, 2, 2, 1, 3, 2, 3,
       1, 1, 2, 3, 2, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 0, 1, 3, 3, 1, 4, 3, 0,
       3, 3, 3, 3, 3, 3, 1, 1, 3, 2, 3, 1, 0, 3, 1, 3, 2, 2, 1, 3, 3, 2, 3,
       3, 3, 2, 1, 2, 3, 3, 3, 2, 2, 1, 2, 3, 1, 3, 3, 3, 3, 1, 1, 3, 3, 3,
       1, 3, 1, 3, 1, 3, 3, 1, 3, 0, 1, 1, 1, 3, 3, 2, 3, 1, 1, 3, 3, 2, 3,
       0, 3, 0, 3, 3, 0, 0, 3, 3, 1, 3, 2, 3, 2, 1, 2, 1, 3, 3, 3, 0, 3, 3,
       0, 3, 1, 1, 3, 1, 0, 1, 4, 0, 3, 1, 1, 0, 3, 3, 3, 3, 1, 3, 1, 3, 3,
       3, 1, 2, 3, 2, 2, 3, 2, 1, 3, 3, 3, 0, 2, 3, 3, 3, 3, 3, 1, 2, 3, 3,
       1, 1, 1, 0, 3, 3, 1, 3, 3, 2, 1, 5, 1, 1, 1, 3, 3, 3, 3, 3, 3, 2, 3,
       3, 2, 1, 3, 2, 3, 2, 3, 2, 1, 3, 1, 2, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3,
       1, 3, 0, 1, 3, 1, 3, 3, 3, 0, 3, 3, 1, 1, 0, 3, 2, 1, 3, 1, 3, 3, 3,
       3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 0, 1, 1, 1, 3, 1, 2, 3, 3, 3, 3, 3, 3,
       3, 3, 3, 3, 2, 3, 3, 1, 1, 1, 1, 1, 1, 0, 3, 3, 2, 3, 1, 3, 1, 0, 0,
       2, 3, 3], dtype=int32)

参考にしたサイト((こちらのサイトを参考にしました

[http://pythondatascience.plavox.info/scikit-learn/%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E5%88%86%E6%9E%90-k-means:title] ))

元のデータフレームにクラスタリングした番号を付与する

cluster_df['Cluster'] = cluster
cluster_df.head()
Channel Region Fresh Grocery Frozen Delicassen Cluster
0 2 3 12669 7561 214 1338 1
1 2 3 7057 9568 1762 1776 3
2 2 3 6353 7684 2405 7844 3
3 1 3 13265 4221 6404 1788 1
4 2 3 22615 7198 3915 5185 1

クラスタに分類されたサンプルの数

cluster_df[['Cluster','Fresh']].groupby(['Cluster']).count()
Fresh
Cluster
0 37
1 120
2 60
3 216
4 3
5 4

クラスタの卸売額合計

cluster_df.drop(['Region','Channel'],axis=1).groupby('Cluster').sum()
Fresh Grocery Frozen Delicassen
Cluster
0 1396192 239382 282851 142625
1 2129961 553601 426496 168718
2 330513 1329555 92192 118564
3 1061036 1062918 495943 216521
4 257339 37859 41975 11874
5 105090 275247 12193 12641

クラスタの取引先あたりの卸売額に卸売額の合計、顧客数を付与

cluster_df1 = cluster_df.drop(['Channel','Region'],axis=1).groupby('Cluster').mean()
count1 = np.array(cluster_df[['Cluster','Fresh']].groupby(['Cluster']).count())
Fresh_sum = np.array(cluster_df[['Cluster','Fresh']].groupby('Cluster').sum())
Grocery_sum = np.array(cluster_df[['Cluster','Grocery']].groupby('Cluster').sum())
Frozen_sum = np.array(cluster_df[['Cluster','Frozen']].groupby('Cluster').sum())
Delicassen_sum = np.array(cluster_df[['Cluster','Delicassen']].groupby('Cluster').sum())
cluster_df1['Count'] = count1
cluster_df1['Fresh_sum'] = Fresh_sum
cluster_df1['Grocery_sum'] = Grocery_sum
cluster_df1['Frozen_sum'] = Frozen_sum
cluster_df1['Delicassen_sum'] = Delicassen_sum
cluster_df1
Fresh Grocery Frozen Delicassen Count Fresh_sum Grocery_sum Frozen_sum Delicassen_sum
Cluster
0 37734.918919 6469.783784 7644.621622 3854.729730 37 1396192 239382 282851 142625
1 17749.675000 4613.341667 3554.133333 1405.983333 120 2129961 553601 426496 168718
2 5508.550000 22159.250000 1536.533333 1976.066667 60 330513 1329555 92192 118564
3 4912.203704 4920.916667 2296.032407 1002.412037 216 1061036 1062918 495943 216521
4 85779.666667 12619.666667 13991.666667 3958.000000 3 257339 37859 41975 11874
5 26272.500000 68811.750000 3048.250000 3160.250000 4 105090 275247 12193 12641
  • Freshの卸売額が最も多く、Groceryの卸売額が少ない顧客層であるCluster'0'は、新たな生鮮品に反応する可能性が高く、一番の狙い目かもしれない

そこで Cluster'0'に絞って、RegionとChannelから販促をかける対象をより絞ってみる

cluster_df2 = cluster_df[cluster_df['Cluster']==0]
cluster_df2.head()
Channel Region Fresh Grocery Frozen Delicassen Cluster
12 2 3 31714 11757 287 2931 0
22 1 3 31276 4469 9408 4334 0
23 2 3 26373 22019 5154 16523 0
29 1 3 43088 2609 1200 823 0
33 1 3 29729 7326 6130 1083 0
cluster_df2[['Region','Channel','Fresh']].groupby(['Region','Channel']).count()
Fresh
Region Channel
1 1 6
2 1 2
3 1 25
2 4
cluster_df3 = cluster_df2.drop('Cluster',axis=1).groupby(['Region','Channel']).mean()
cluster_df3
count2 = np.array(cluster_df2[['Region','Channel','Fresh']].groupby(['Region','Channel']).count())
Fresh_sum2 = np.array(cluster_df2[['Region','Channel','Fresh']].groupby(['Region','Channel']).sum())
Grocery_sum2 = np.array(cluster_df2[['Region','Channel','Grocery']].groupby(['Region','Channel']).sum())
Frozen_sum2 = np.array(cluster_df2[['Region','Channel','Frozen']].groupby(['Region','Channel']).sum())
Delicassen_sum2 = np.array(cluster_df2[['Region','Channel','Delicassen']].groupby(['Region','Channel']).sum())
cluster_df3['Count'] = count2
cluster_df3['Fresh_sum'] = Fresh_sum2
cluster_df3['Grocery_sum'] = Grocery_sum2
cluster_df3['Frozen_sum'] = Frozen_sum2
cluster_df3['Delicassen_sum'] = Delicassen_sum2
cluster_df3
Fresh Grocery Frozen Delicassen Count Fresh_sum Grocery_sum Frozen_sum Delicassen_sum
Cluster
0 37734.918919 6469.783784 7644.621622 3854.729730 37 1396192 239382 282851 142625
1 17749.675000 4613.341667 3554.133333 1405.983333 120 2129961 553601 426496 168718
2 5508.550000 22159.250000 1536.533333 1976.066667 60 330513 1329555 92192 118564
3 4912.203704 4920.916667 2296.032407 1002.412037 216 1061036 1062918 495943 216521
4 85779.666667 12619.666667 13991.666667 3958.000000 3 257339 37859 41975 11874
5 26272.500000 68811.750000 3048.250000 3160.250000 4 105090 275247 12193 12641
  • 取引先の数が多い順に販促をかけるとすると下記をターゲットにするのが良いかもしれない
    1. Other Region,Horeca
    2. Other Region,Retail
    3. Lisbon,Horeca

感想

  • 分布を確認はして見たものの、特徴を掴むのにあまり有効な気がしなかった。やり方が悪い気がする
  • グループ化は、RegionとChannelを組み合わせたものだけでよかったかも
  • このぐらいのデータ量なら、クロス集計で一生懸命紐解いていったほうがわかりやすく有益な情報が得られそうな気がした
  • クラスタリング後の考察が甘いが、事業を深く知る手立てがあれば、いろんな切り口で分析するアイデアが湧いてきそうで楽しいなと思えた!

参考にしたサイト

*1:こちらのサイトを参考にしました

http://nlp.dse.ibaraki.ac.jp/~shinnou/zemi2008/Rclustering/r-tanaka-0415.pdf

*2:こちらのサイトを参考にしました

www.kamishima.net

*3:こちらのサイトを参考にしました

enterprisezine.jp

scikit-learnでロジスティック回帰分析を試す

とにかく試して見るシリーズ第二弾。

ロジスティック回帰分析とは

概要

ロジスティック回帰分析 (Logistic regression) は、発生確率を予測する手法。
2値しかとりえない値を目的変数とし、説明変数を用いてその発生確率を説明する。

ロジスティック回帰分析でできること

  • 予測値を算出する
  • 説明変数の目的変数に対する貢献度を算出する

参考にしたサイト*1

ビジネスでの活用例

  • 顧客がある特定のキャンペーンに対して反応を示すか否か、を他の変数を利用して予測する
    マーケティングの潜在顧客推定に使用されている
  • 金融機関では、リスク予測に使用されている
  • 医療分野では、複数の因子からある症状を発症する可能性を予測するのに使われていた
    → これがマーケティングに応用されるようになったようだ

取り組んだ課題

ロジスティック回帰に適していそうなデータセットを自力で探し、予測モデルを作ってみる

試行過程と結果

使えそうなデータセットを探す

目的変数が0または1の2値になるデータセットを探した結果、過去に利用したサイトにちょうどいいデータセットがあった。
台湾で実施された、6ヶ月間のクレジットカードの支払履歴と翌月の支払状況の調査結果のようだ。
目的変数となる支払状況は0,1の2値で示せるため、ロジスティック回帰に適していそうだ。
(のちにKaggleに出ているのを発見した)

データセットを確認する

Datasetはここから入手した

データの入手元にあった各項目の説明

default payment next month:翌月滞納 (Yes=1,No=0)  
  
   LIMIT_BAL  :利用可能枠 (台湾新ドル、家族カードの利用可能枠を含む)  
   SEX        :性別 (1=male,2=female)  
   EDUCATION  :最終学歴 (1=graduate school,2=university,3=high school,4=others)  
   MARRIAGE   :既婚/未婚/その他 (1=married,2=single,3=others)  
   AGE        :年齢 (year)  
   PAY_0-9    :支払歴 (-1=遅延なく支払,1=1ヶ月延滞,...,8=8ヶ月延滞,9=9ヶ月以上延滞)  
   BILL_AMT1-6:請求額 (BILL_AMT1=請求額(2005年9月),BILL_AMT2=請求額(2005年8月),...BILL_AMT6=請求額(2005年4月))  
   PAY_AMT1-6 :支払額 (PAY_AMT1=支払額(2005年9月),PAY_AMT2=支払額(2005年8月),...PAY_AMT6=支払額(2005年4月))  
# .xlsファイルを読込んで、rawを見てみる
import pandas as pd
dframe_in = pd.read_excel('default of credit card clients.xls', sheetname='Data')
dframe_in.head(5)
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 ... X15 X16 X17 X18 X19 X20 X21 X22 X23 Y
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 PAY_5 ... BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default payment next month
1 20000 2 2 1 24 2 2 -1 -1 -2 ... 0 0 0 0 689 0 0 0 0 1
2 120000 2 2 2 26 -1 2 0 0 0 ... 3272 3455 3261 0 1000 1000 1000 0 2000 1
3 90000 2 2 2 34 0 0 0 0 0 ... 14331 14948 15549 1518 1500 1000 1000 1000 5000 0
4 50000 2 2 1 37 0 0 0 0 0 ... 28314 28959 29547 2000 2019 1200 1100 1069 1000 0

5 rows × 24 columns

# 確認できなかった列を参照
from pandas import DataFrame
dframe_in[['X11','X12','X13','X14']].head(5)
X11 X12 X13 X14
ID PAY_6 BILL_AMT1 BILL_AMT2 BILL_AMT3
1 -2 3913 3102 689
2 2 2682 1725 2682
3 0 29239 14027 13559
4 0 46990 48233 49291

データの説明について、誤っていることがわかったこと

PAY_0-9[支払歴]は、PAY_0,(飛んで),PAY_2,PAY_3,PAY_4,PAY_5,PAY_6しか存在しない。
欠損値も確認しておく

# 欠損値を確認する
dframe_in.isnull().any().any()
False

欠損値はない

# ヘッダー行、ID列を読み飛ばして再度読み込み
# 紛らわしいため、'PAY_0'は'PAY_1'に置換、'default payment next month'も長いため省略
dframe_e1 = pd.read_excel('default of credit card clients.xls', header=1, sheetname='Data')
dframe_e1 = dframe_e1.drop("ID",axis=1)
dframe_e1.rename(columns={'PAY_0':'PAY_Sep',
                          'PAY_2':'PAY_Aug',
                          'PAY_3':'PAY_Jul',
                          'PAY_4':'PAY_Jun',
                          'PAY_5':'PAY_May',
                          'PAY_6':'PAY_Apr',
                          'BILL_AMT1':'BILL_AMT_Sep',
                          'BILL_AMT2':'BILL_AMT_Aug',
                          'BILL_AMT3':'BILL_AMT_Jul',
                          'BILL_AMT4':'BILL_AMT_Jun',
                          'BILL_AMT5':'BILL_AMT_May',
                          'BILL_AMT6':'BILL_AMT_Apr',
                          'PAY_AMT1':'PAY_AMT_Sep',
                          'PAY_AMT2':'PAY_AMT_Aug',
                          'PAY_AMT3':'PAY_AMT_Jul',
                          'PAY_AMT4':'PAY_AMT_Jun',
                          'PAY_AMT5':'PAY_AMT_May',
                          'PAY_AMT6':'PAY_AMT_Apr',
                          'default payment next month':'DEF_PAY'}, inplace=True)
dframe_e1.head(5)
LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_Sep PAY_Aug PAY_Jul PAY_Jun PAY_May ... BILL_AMT_Jun BILL_AMT_May BILL_AMT_Apr PAY_AMT_Sep PAY_AMT_Aug PAY_AMT_Jul PAY_AMT_Jun PAY_AMT_May PAY_AMT_Apr DEF_PAY
0 20000 2 2 1 24 2 2 -1 -1 -2 ... 0 0 0 0 689 0 0 0 0 1
1 120000 2 2 2 26 -1 2 0 0 0 ... 3272 3455 3261 0 1000 1000 1000 0 2000 1
2 90000 2 2 2 34 0 0 0 0 0 ... 14331 14948 15549 1518 1500 1000 1000 1000 5000 0
3 50000 2 2 1 37 0 0 0 0 0 ... 28314 28959 29547 2000 2019 1200 1100 1069 1000 0
4 50000 1 2 1 57 -1 0 -1 0 0 ... 20940 19146 19131 2000 36681 10000 9000 689 679 0

5 rows × 24 columns

PAY_Apr〜PAY_Sepに'-2'と'0'の説明にない値があるため、規則性を探って見る

# 請求額、支払歴、支払額の順に並べ替える(前月請求額が翌月支払われている模様)
dframe_e2 = dframe_e1[['PAY_AMT_Apr',
                       'PAY_Apr',
                       'BILL_AMT_Apr',
                       'PAY_AMT_May',
                       'PAY_May',
                       'BILL_AMT_May',
                       'PAY_AMT_Jun',
                       'PAY_Jun',
                       'BILL_AMT_Jun',
                       'PAY_AMT_Jul',
                       'PAY_Jul',
                       'BILL_AMT_Jul',
                       'PAY_AMT_Aug',
                       'PAY_Aug',
                       'BILL_AMT_Aug',
                       'PAY_AMT_Sep',
                       'PAY_Sep',
                       'BILL_AMT_Sep'
                     ]]
# -1から-2に転じているデータを見てみる
dframe_e3 = dframe_e2[dframe_e2['PAY_Jun']>-2]
dframe_e3 = dframe_e3[dframe_e3['PAY_Jul']<-1]
dframe_e3.head(5)
PAY_AMT_Apr PAY_Apr BILL_AMT_Apr PAY_AMT_May PAY_May BILL_AMT_May PAY_AMT_Jun PAY_Jun BILL_AMT_Jun PAY_AMT_Jul PAY_Jul BILL_AMT_Jul PAY_AMT_Aug PAY_Aug BILL_AMT_Aug PAY_AMT_Sep PAY_Sep BILL_AMT_Sep
65 1000 2 7918 0 2 8198 300 -1 8174 8222 -2 144076 0 -2 148751 0 -2 152519
68 13899 -1 7319 7319 2 10161 0 -1 10311 20161 -2 -9850 0 -2 -9850 0 1 -190
198 15816 -1 1151 1151 -1 1206 1206 -1 1251 1251 -2 2299 2299 -2 138 138 -2 412
232 2000 0 46557 1747 -1 45567 45567 -1 2624 2624 -2 0 0 0 0 0 0 102800
265 316 -1 316 316 -1 316 316 -1 316 316 -2 0 0 -1 0 0 -1 6156

請求額と支払額が一致しないレコードがあるが、分割かリボルビング払いだろうか?

わからないので検索して見る

そもそも推測なんて意味ないので、調べて見るべきだろう。
Kaggleでデータセットを作成した教授にe-mailして聞いて見たという投稿を発見
https://www.kaggle.com/uciml/default-of-credit-card-clients-dataset/discussion/34608

-2: No consumption(請求なし);
0: The use of revolving credit(リボルビング払い);

やはりリボ払いか…
請求額があって、支払歴が'-2(請求額なし)‘のレコードもあるがこっちは分割払いだろうか…
いずれにせよリボルビング払いを含むのであれば、特定6ヶ月のみを切り出した支払額、請求額は説明変数に適さないだろう

rawを見て再整理した各項目の説明

default payment next month:翌月滞納 (Yes=1,No=0)  
  
   LIMIT_BAL       :利用可能枠 (台湾新ドル、家族カードの利用可能枠を含む)  
   SEX             :性別 (1=male,2=female)  
   EDUCATION       :最終学歴 (1=graduate school,2=university,3=high school,4=others)  
   MARRIAGE        :既婚/未婚/その他 (1=married,2=single,3=others)   
   AGE             :年齢 (year)  
   PAY_Sep-Apr     :支払歴 (-2=請求なし,-1=遅延なく支払,0=リボルビング払い,1=1ヶ月延滞,...,8=8ヶ月延滞,9=9ヶ月以上延滞)  
   BILL_AMT_Sep-Apr:請求額 (BILL_Sep=請求額(2005年9月),BILL_AMT_Aug=請求額(2005年8月),...BILL_AMT_Apr=請求額(2005年4月))  
   PAY_AMT_Sep-Apr :支払額 (PAY_AMT_Sep=支払額(2005年9月),PAY_AMT_Aug=支払額(2005年8月),...PAY_AMT_Apr=支払額(2005年4月))

データの特徴を確認する

# データセットの特徴量を見てみる
dframe_e1.describe()
LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_Sep PAY_Aug PAY_Jul PAY_Jun PAY_May ... BILL_AMT_Jun BILL_AMT_May BILL_AMT_Apr PAY_AMT_Sep PAY_AMT_Aug PAY_AMT_Jul PAY_AMT_Jun PAY_AMT_May PAY_AMT_Apr DEF_PAY
count 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 30000.000000 ... 30000.000000 30000.000000 30000.000000 30000.000000 3.000000e+04 30000.00000 30000.000000 30000.000000 30000.000000 30000.000000
mean 167484.322667 1.603733 1.853133 1.551867 35.485500 -0.016700 -0.133767 -0.166200 -0.220667 -0.266200 ... 43262.948967 40311.400967 38871.760400 5663.580500 5.921163e+03 5225.68150 4826.076867 4799.387633 5215.502567 0.221200
std 129747.661567 0.489129 0.790349 0.521970 9.217904 1.123802 1.197186 1.196868 1.169139 1.133187 ... 64332.856134 60797.155770 59554.107537 16563.280354 2.304087e+04 17606.96147 15666.159744 15278.305679 17777.465775 0.415062
min 10000.000000 1.000000 0.000000 0.000000 21.000000 -2.000000 -2.000000 -2.000000 -2.000000 -2.000000 ... -170000.000000 -81334.000000 -339603.000000 0.000000 0.000000e+00 0.00000 0.000000 0.000000 0.000000 0.000000
25% 50000.000000 1.000000 1.000000 1.000000 28.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 ... 2326.750000 1763.000000 1256.000000 1000.000000 8.330000e+02 390.00000 296.000000 252.500000 117.750000 0.000000
50% 140000.000000 2.000000 2.000000 2.000000 34.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 19052.000000 18104.500000 17071.000000 2100.000000 2.009000e+03 1800.00000 1500.000000 1500.000000 1500.000000 0.000000
75% 240000.000000 2.000000 2.000000 2.000000 41.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 54506.000000 50190.500000 49198.250000 5006.000000 5.000000e+03 4505.00000 4013.250000 4031.500000 4000.000000 0.000000
max 1000000.000000 2.000000 6.000000 3.000000 79.000000 8.000000 8.000000 8.000000 8.000000 8.000000 ... 891586.000000 927171.000000 961664.000000 873552.000000 1.684259e+06 896040.00000 621000.000000 426529.000000 528666.000000 1.000000

8 rows × 24 columns

# 翌月の延滞有無でグループ分けしてみる
dframe_e1.groupby('DEF_PAY').mean()
LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_Sep PAY_Aug PAY_Jul PAY_Jun PAY_May ... BILL_AMT_Jul BILL_AMT_Jun BILL_AMT_May BILL_AMT_Apr PAY_AMT_Sep PAY_AMT_Aug PAY_AMT_Jul PAY_AMT_Jun PAY_AMT_May PAY_AMT_Apr
DEF_PAY
0 178099.726074 1.614150 1.841337 1.558637 35.417266 -0.211222 -0.301917 -0.316256 -0.355633 -0.389488 ... 47533.365605 43611.165254 40530.445343 39042.268704 6307.337357 6640.465074 5753.496833 5300.529319 5248.220296 5719.371769
1 130109.656420 1.567058 1.894665 1.528029 35.725738 0.668174 0.458258 0.362116 0.254521 0.167872 ... 45181.598855 42036.950573 39540.190476 38271.435503 3397.044153 3388.649638 3367.351567 3155.626733 3219.139542 3441.482068

2 rows × 23 columns

グループ分けしてみた結果、翌月の延滞と高い相関関係がありそうなのは、
LIMIT_BAL :利用可能枠
PAY_0-9 :支払歴

グループ分けでは、平均値にそれほど違いのなかった項目の分布を見てみる

# 最終学歴分布を見てみる
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
sns.set_style('whitegrid')
sns.set_context("paper")
plt.figure(figsize=(10,6))
sns.countplot('EDUCATION',data=dframe_e1.sort_values(by='EDUCATION'),hue='DEF_PAY',palette='coolwarm')

f:id:select_from_where:20170918095701p:plain
最終学歴分布

# 利用可能枠の分布を見てみる
plt.figure(figsize=(16,6))
sns.set_style('whitegrid')
sns.set_context("paper")
sns.countplot('AGE',data=dframe_e1.sort_values(by='AGE'),hue='DEF_PAY',palette='coolwarm')

f:id:select_from_where:20170918095715p:plain
利用可能枠

# 年齢分布を見てみる
plt.figure(figsize=(16,6))
sns.set_style('whitegrid')
sns.set_context("paper")
sns.countplot('LIMIT_BAL',data=dframe_e1.sort_values(by='LIMIT_BAL'),hue='DEF_PAY',palette='coolwarm')

f:id:select_from_where:20170918095710p:plain
年齢分布

# 説明変数Xを設定
X = DataFrame(dframe_e1, columns=['LIMIT_BAL','SEX','EDUCATION','MARRIAGE','AGE','PAY_Apr','PAY_May','PAY_Jun','PAY_Jul','PAY_Aug','PAY_Sep'])
X.head(5)
LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_Apr PAY_May PAY_Jun PAY_Jul PAY_Aug PAY_Sep
0 20000 2 2 1 24 -2 -2 -1 -1 2 2
1 120000 2 2 2 26 2 0 0 0 2 -1
2 90000 2 2 2 34 0 0 0 0 0 0
3 50000 2 2 1 37 0 0 0 0 0 0
4 50000 1 2 1 57 0 0 0 -1 0 -1
# 説明変数Yを設定
Y = dframe_e1['DEF_PAY']
Y.head()
0    1
1    1
2    0
3    0
4    0
Name: DEF_PAY, dtype: int64
# 説明変数は1次元に変換する
Y = Y.values
Y
array([1, 1, 0, ..., 1, 1, 1])

ロジスティック回帰のモデルを作成する

from sklearn.linear_model import LogisticRegression
# LogisticRegressionクラスのインスタンスを作成
log_model1 = LogisticRegression()
# 目的変数、説明変数を使って、モデルを作成。たったのこれだけ。
log_model1.fit(X,Y)
# モデルの精度を確認
log_model1.score(X,Y)
0.77880000000000005
# 'DEF_PAY'の平均値から割り出した、翌月延滞する確率は?
1 - Y.mean()
0.77879999999999994

ロジスティック回帰によって、より高い精度で予測できるとは言えないようだ。

# 変数名とその係数を可視化
coeff_df1 = DataFrame([X.columns, log_model1.coef_[0]]).T
coeff_df1
0 1
0 LIMIT_BAL -5.26002e-06
1 SEX -0.000514679
2 EDUCATION -0.000644289
3 MARRIAGE -0.000552566
4 AGE -0.00907855
5 PAY_Apr 0.000406061
6 PAY_May 0.000428339
7 PAY_Jun 0.000445863
8 PAY_Jul 0.000478645
9 PAY_Aug 0.000547339
10 PAY_Sep 0.00069057

LIMIT_BAL[利用可能枠]は、予測結果に大きく影響する。
(大きくなるほど翌月の延滞の可能性は下がり、小さくなるほど翌月の延滞の可能性は上がる)
支払歴は、最近のものほど翌月の支払に影響を与えているようだ

# LIMIT_BALを除いて、新しい説明変数X2を作成
X2 = DataFrame(X, columns=['SEX','EDUCATION','MARRIAGE','AGE','PAY_Apr','PAY_May','PAY_Jun','PAY_Jul','PAY_Aug','PAY_Sep'])
X2.head(5)
SEX EDUCATION MARRIAGE AGE PAY_Apr PAY_May PAY_Jun PAY_Jul PAY_Aug PAY_Sep
0 2 2 1 24 -2 -2 -1 -1 2 2
1 2 2 2 26 2 0 0 0 2 -1
2 2 2 2 34 0 0 0 0 0 0
3 2 2 1 37 0 0 0 0 0 0
4 1 2 1 57 0 0 0 -1 0 -1
# 新しいモデルの作成
log_model2 = LogisticRegression()
log_model2.fit(X2,Y)
# 新しいモデルの精度を確認
log_model2.score(X2,Y)
0.80983333333333329

精度が向上した!

# 変数名とその係数を可視化
coeff_df2 = DataFrame([X2.columns, log_model2.coef_[0]]).T
coeff_df2
0 1
0 SEX -0.104595
1 EDUCATION -0.055019
2 MARRIAGE -0.134139
3 AGE 0.00447237
4 PAY_Apr 0.0015857
5 PAY_May 0.0348254
6 PAY_Jun 0.0211476
7 PAY_Jul 0.0868766
8 PAY_Aug 0.0852904
9 PAY_Sep 0.613086
# 今度は支払歴だけで予測してみる
X3 = DataFrame(X, columns=['PAY_Apr','PAY_May','PAY_Jun','PAY_Jul','PAY_Aug','PAY_Sep'])
X3.head(5)
PAY_Apr PAY_May PAY_Jun PAY_Jul PAY_Aug PAY_Sep
0 -2 -2 -1 -1 2 2
1 2 0 0 0 2 -1
2 0 0 0 0 0 0
3 0 0 0 0 0 0
4 0 0 0 -1 0 -1
# 新しいモデルの作成
log_model3 = LogisticRegression()
log_model3.fit(X3,Y)
# 新しいモデルの精度を確認
log_model3.score(X3,Y)
0.80876666666666663

若干精度は下がった

# より精度の高かったモデルで、直近3ヶ月の支払履歴に絞って予測して見る
X4 = DataFrame(X, columns=['SEX','EDUCATION','MARRIAGE','AGE','PAY_Jul','PAY_Aug','PAY_Sep'])
X4.head(5)
SEX EDUCATION MARRIAGE AGE PAY_Jul PAY_Aug PAY_Sep
0 2 2 1 24 -1 2 2
1 2 2 2 26 0 2 -1
2 2 2 2 34 0 0 0
3 2 2 1 37 0 0 0
4 1 2 1 57 -1 0 -1
# 新しいモデルの作成
log_model4 = LogisticRegression()
log_model4.fit(X4,Y)
# 新しいモデルの精度を確認
log_model4.score(X4,Y)
0.80936666666666668

試した中で、ベストな目的変数は下記だった
‘SEX’,‘EDUCATION’,‘MARRIAGE’,‘AGE’,‘PAY_Apr’,‘PAY_May’,‘PAY_Jun’,‘PAY_Jul’,‘PAY_Aug’,‘PAY_Sep’

データを学習用とテスト用に分けて、モデルの性能を確認

from sklearn.cross_validation import train_test_split
from sklearn import metrics
# train_test_splitを使う
X_train, X_test, Y_train, Y_test = train_test_split(X2, Y)
# 新しいモデルを作成
log_model5 = LogisticRegression()
# 学習用のデータだけ学習
log_model5.fit(X_train, Y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
# テスト用データを使って予測
class_predict = log_model5.predict(X_test)
# 予測の精度を確認
print(metrics.accuracy_score(Y_test,class_predict))
0.814

全データを使用した時と同程度の精度を出すことができた




感想

  • 決定木分析に比べてすごく扱いやすく、わかりやすい印象
  • 説明変数の設定は、どのように試行して検証するのが良いのかを理解していく必要がある
  • データの収集方法として、目的変数を得る前に説明変数を収集する(顧客の属性や行動履歴)のと、目的変数と一緒に説明変数を収集する(アンケート)のとでは、なんとなくだがロジスティック回帰の有効性に影響がありそうな気がする

参考にしたサイト

scikit-learnで決定木分析(CART)を試す

とにかく試して見るシリーズ第一弾。

なぜやるのか

 いつまでもデータマート拵えおじさんのままではマズいため、比較的難易度が低くビジネスで幅広く応用が効くらしい決定木分析をやってみる

決定木分析とは

概要

決定木分析 (Decision Tree Analysis) は、機械学習の手法の一つ。木を逆にしたようなデータ構造を用いて分類と回帰を行う。

決定木分析の特徴

  • 樹木状の構造で学習結果を視覚化でき、ルールをシンプルに表現できるため、論理的な解釈が容易
  • データの標準化 (正規化) やダミー変数の作成を必要としないため、前処理の手間がほとんど不要
  • カテゴリカルデータと数値データの両方を扱うことが可能
  • 検定を行って、作成したモデルの正しさを評価することが可能

参考にしたサイト*1

ビジネスでの活用例

  • 顧客別の購買履歴から自社の製品を購入している顧客の特徴を分析
  • 金融機関の取引履歴から顧客属性別の貸し倒れリスクを分析
  • 機械の動作ログから故障につながる指標を分析

参考にしたサイト*2

取り組んだ課題

Rの標準データセット[HairEyeColor]のデータを使ってHair[髪の色]、Eye Color[瞳の色]から、Sex[性別]を予測する決定木のモデルをPythonで作成する*3

試行過程と結果

1.データセットを確認する

Datasetはここから入手した

# 編集前のcsvファイルを読み込んで、DataFrame化
import pandas as pd
import_df = pd.read_csv('HairEyeColor.csv')
import_df.head()
Unnamed: 0 Hair Eye Sex Freq
0 1 Black Brown Male 32
1 2 Brown Brown Male 53
2 3 Red Brown Male 10
3 4 Blond Brown Male 3
4 5 Black Blue Male 11

決定木分析をするにあたり、このデータセットでは2工程の下処理が必要

  • 各レコードをFreqの数だけ生成する
  • String型の各項目をint型に置換する

2.CSVデータを加工する

参考にしたサイト*4

in_file  = open("HairEyeColor.csv","r")
out_file = open("HairEyeColor_Edited.csv","w")

# アウトプットファイルにヘッダーを書き込み
out_file.write("Hair,Eye,Sex\n")

# インプットファイルのヘッダーを読み飛ばす
in_file.readline()
# インプットファイルの全レコードを読み込み
lines = in_file.readlines()

# for文で1行ずつ処理
for line in lines:
    # 改行コードはブランクに置換
    line = line.replace("\n","")
    # カンマ区切りでリストに変換する
    line = line.split(",")
    # 変換処理した値を、更にカンマ区切りへ変換
    row = "{},{},{}\n".format(line[1],line[2],line[3])
    # 書き出し用のファイルに"Freq"の数だけ出力
    freq = int(line[4])
    for i in range(0,freq):
        out_file.write(row)

in_file.close()
out_file.close()
# 編集後のcsvファイルを読み込んで、DataFrame化
import_df = pd.read_csv('HairEyeColor_Edited.csv')
import_df.head()
Hair Eye Sex
0 Black Brown Male
1 Black Brown Male
2 Black Brown Male
3 Black Brown Male
4 Black Brown Male
# 加工前との整合性を確認する
check = import_df.groupby(['Hair','Eye','Sex']).size()
check
Hair   Eye    Sex   
Black  Blue   Female     9
              Male      11
       Brown  Female    36
              Male      32
       Green  Female     2
              Male       3
       Hazel  Female     5
              Male      10
Blond  Blue   Female    64
              Male      30
       Brown  Female     4
              Male       3
       Green  Female     8
              Male       8
       Hazel  Female     5
              Male       5
Brown  Blue   Female    34
              Male      50
       Brown  Female    66
              Male      53
       Green  Female    14
              Male      15
       Hazel  Female    29
              Male      25
Red    Blue   Female     7
              Male      10
       Brown  Female    16
              Male      10
       Green  Female     7
              Male       7
       Hazel  Female     7
              Male       7
dtype: int64
# データの特徴を見てみる
import_df.describe()
Hair Eye Sex
count 592 592 592
unique 4 4 2
top Brown Brown Female
freq 286 220 313
# String型の各項目をint型に置換するための辞書を作って変換
dict = {'Hair':{'Black' :1,'Blond':2,'Brown':3,'Red'  :4},
        'Eye' :{'Blue'  :1,'Brown':2,'Green':3,'Hazel':4},
        'Sex' :{'Female':1,'Male' :2}}

import_df['Hair'] = import_df["Hair"].map(dict['Hair'])
import_df['Eye']  = import_df["Eye"].map(dict['Eye'])
import_df['Sex']  = import_df["Sex"].map(dict['Sex'])
import_df.head()
Hair Eye Sex
0 1 2 2
1 1 2 2
2 1 2 2
3 1 2 2
4 1 2 2

3.決定木の分類器を作成して可視化する

参考にしたサイト*5

from sklearn import tree
# 説明変数は'Hair','Eye'
variables = ['Hair','Eye']

# 決定木の分類器を作成
classifier = tree.DecisionTreeClassifier()

# 目的変数は'Sex'
# サンプルデータで学習
classifier = classifier.fit(import_df[variables],import_df['Sex'])
# 作成した決定木を可視化する
import pydotplus
from sklearn.externals.six import StringIO
dot_data = StringIO()
tree.export_graphviz(classifier, out_file=dot_data,
                     filled=True,rounded=True)

graph = pydotplus.graph_from_dot_data(dot_data.getvalue())

from IPython.display import Image
Image(graph.create_png())

f:id:select_from_where:20170910163541p:plain
Decision Tree

4.交差検証

今回は決定木の深さだけを変えて交差検証してみる

# 交差検証1(max_depth指定なし)
import numpy as np
from sklearn import cross_validation as cv

data = import_df.reindex(np.random.permutation(import_df.index))
variables = ['Hair','Eye']

classifier = tree.DecisionTreeClassifier()
scores = cv.cross_val_score(classifier, data[variables], data['Sex'], cv=5)

print(scores.mean(), scores)
0.517007950507 [ 0.48739496  0.57142857  0.47058824  0.49152542  0.56410256]
# 交差検証2(max_depth=3)
data = import_df.reindex(np.random.permutation(import_df.index))
variables = ['Hair','Eye']

classifier = tree.DecisionTreeClassifier(max_depth=3)
scores = cv.cross_val_score(classifier, data[variables], data['Sex'], cv=5)

print(scores.mean(), scores)
0.545807753784 [ 0.46218487  0.52941176  0.56302521  0.59322034  0.58119658]
# 交差検証3(max_depth=4)
data = import_df.reindex(np.random.permutation(import_df.index))
variables = ['Hair','Eye']

classifier = tree.DecisionTreeClassifier(max_depth=4)
scores = cv.cross_val_score(classifier, data[variables], data['Sex'], cv=5)

print(scores.mean(), scores)
0.526948093449 [ 0.58823529  0.47058824  0.55462185  0.50847458  0.51282051]

感想

  • インプットしたHair, EyeのColorに対して、Sexをアウトプットする仕組みを作れないと意味がない
  • パラメータ設定によるチューニングができるほどの理解には及んでいないため、もう少し理解を深めたい
  • Grid Searchなるものを実行できるようになれば、パラメータを最適化できるらしい(理解度浅くても使えるということか)
  • レコメンドに応用するとして、説明変数と目的変数の粒度をどうするのかが重要そうだ(ファッションのように品目がかなり細かい商品を扱うならカテゴリで、ラインナップがファッションなどに比べて限定的な金融商品などなら商品そのものでも分類できそう)
  • 顧客の属性情報を説明変数に様々な切り口で分析をかければ、マーケティング対象をより詳細に理解して施策を練るのに役立てられそう

参考にしたサイト

*1:こちらのサイトを参考にしました

scikit-learn で決定木分析 (CART 法) – Python でデータサイエンス

*2:こちらのサイトを参考にしましたanalytics-news.jp

*3:こちらの問題を参考にしていますcodeiq.jp

*4:こちらのサイトを参考にしましたmemopy.hatenadiary.jp

*5:こちらのサイトを参考にしました
todoa2c.github.io

このままでは一端の何者にもなれない

 ブログを始める。 何かに深い知見があるわけでもなく、不特定多数の誰かの為になるような記事をすぐには書けそうもない。

 システムエンジニアからデータアナリストに転身してはや数ヶ月が経った。日常業務をこなすのに必要な最低限のキャッチアップを終え、すっかりデータマート拵えおじさんと化している。

 このままではいかん。この危機感はなんなんだ。この記事では転職を少しだけ振返り、このブログを始めた経緯を綴っておく。

転職したきっかけ

事業を理解して自ら課題を発見し、仕事を作れる人間になりたい

 新卒でシステムエンジニアになったのは、「チームで達成感を共有できる」「顧客の課題に対し、企画・提案ができる」と考えたからだった。

 しかし、私のいた現場は古くから基幹業務システムを抱えている大企業、配属された部署は汎用機系アプリケーションの保守案件が多かったこともあり、大きなミスマッチがあった。状況の打開には、転職して環境を変える他に選択肢はないと考えたので転職した。

「チームで達成感を共有できる」か?

 ほとんどできなかった。職場を見渡しても仕事で達成感(≒ やりがい)を感じる瞬間があって、それがあるから活き活きと働けてる、みたいな人はいなかった。私自身もこれから取り組もうという仕事に、よし!やるか!みたいな気持ちも抱くこともなくなっていった。
 なぜあんなにやりがいを感じられない仕事だったのだろう。。これは次項と深い関係があるというのが私の持論である。

「顧客の課題に対し、企画・提案ができる」か?

 これが最も辛かったポイントで、そもそも課題をヒアリングして深掘りし、解決策を提案したり協議したりするという仕事ではなかった。実態は、事業側で解決策まで検討してシステム改善要望を提示する。我々は工数内に収まるように要望を削ぎ落としながら要件定義することが仕事。基幹業務システムの保守案件をメインに請け負っており、案件はいくらでもある状態だったため仕方がないのだが、顧客が求めていることに理由をみつけて断っていく作業の繰り返しは前向きではないし、大方の人にとって(おそらく)気持ちの良いものではないと思う。

 加えて事業サイド、情報システム部門各部署と長い歴史で策定された障害を防ぐための細かいルール、開発を担当するパートナー企業との間を柵に足を取られながら行ったり来たり、、みな年々疲弊していく。その仕事に自分の意思はなくなっていく、次々に降りてくる仕事に面倒な折衝をこなす日々。自分が何に貢献する仕事をしているのかわからなくなる、やりがいを見失っていく。。。私は人一倍自分の意思が反映されるかに価値を置いている人間であることを自覚しているが、同僚の方々も少なからず同じような感覚を抱いていたのではないかと思う。

転職で実現したかったこと

データ分析スキルが身につく職に就くこと

 課題を見つけ解決策を打ち出す力をつけるために、人並み以上のデータ分析スキルを身につけたいと考えた。マーケティングに興味(オンラインよりもオフライン寄りなのだが...)があり、自分でデータを自在に操り、施策を生み出せる人に憧れを抱いていた。そして、特定の会社に依存せず、どこへ行っても通用する力をつけたいと考えた。

企画・提案ができる

 個人的な価値観として、自分の手がけることに自分の意志が反映されているか否かは、達成感ややりがいに大きく影響を与えている。そのため、転職先ですぐに実現はできなくても、降りて来る仕事をこなすのではなく、仕事を作る側に回るための道筋となる転職にしたかった。

「チームで達成感を共有できる」は?

 これは追い求めなくなった。仕事に達成感を求めていない人も多い気がするし、優れたビジネスモデルの元で自分でチームを作って率いれるようにでもならない限りは運次第としか言いようがない。これは夢に格上げ?格下げ?することにした。

転職してみて

データ分析スキルは身についたか

 最低限のデータの加工スキルは身についたと思う。このままいけば、正確さとスピードを上げるための技術習得と経験は得られるだろう。「データ分析」のスキルという意味では全く身につけていない。データアナリストというよりは、データマート拵えおじさんなのである。

企画・提案ができているか

 マーケティングに近い領域で、事業に深く踏み込めそうな仕事につくには、スキルセットが足りなかったようで妥協した部分もある転職になった。そのため、企画・提案の機会が乏しいことも想定していた。  まずはデータ分析基盤を理解し、データを扱う技術を身に付けさせたいという上長の意向もあり、企画や提案の機会は少ない部署に業務委託として客先常駐で配置されている。データマート拵えおじさんなのである。

危機感はどこから来ているのか

環境の変化

 常駐先の企業も絡んだ職場の天変地異により人員が大きく減り、データアナリスト歴数ヶ月の自分一人で他社パートナーと仕事をしなければならない環境が目の前に迫っていることが大きい。仕事をこなすスキルは身についたが、+αを上乗せして提案できる技術も知識も持ち合わせておらず、降りて来る仕事をこなすだけ状態に陥りそうなのだ。

成長の鈍化

 自己研鑽への意欲も翳り、成長が鈍化し始めている。インプットしても現場で活かす機会が見当たらず、アウトプットの機会に乏しいことから知識が定着していかない。そもそも自己学習の戦略そのものがうまくない気がしているが、それはまた別の機会に。

ブログを始める理由

 今抱えている危機感は、決して環境のせいだけでなく自分の意識づけの問題も大いにあると思う。
とにかくアウトプットの場が必要、ということでブログを始めることにした。

 キャリアについて考えていること、技術的なメモなど、巷に星の数ほど溢れるブログに仲間入りするとしよう。

文章を綴るのは好きだったのだが、すっかり衰えているなと実感した。。。