一端の何かになれるか

一生懸命は眩しい

Seleniumでクローリングとスクレイピングを試す python

とにかく試して見るシリーズ第5弾。
前回、BeautifulSoupでスクレイピングを試したが、なんだかあまり活用する場面が思い浮かばんなあと思っていた。
同僚の方が普段使っているということで、手ほどきを受けながら挑戦した。
今回はソースコードの公開を控える(どっちみち酷いコードなのですが...)。

なぜやったのか

やっぱりpythonでできそうなことは試しておきたい。
Web系のデータが驚くほど馴染まなかったので、もっとWebを理解していきたい。

試してみたこと

ある商材のスペックと販売価格を取得する。
手順は下記の通り。
1. メーカー単位の商品一覧ページのURLをクローリングして取得する
2. 各メーカーの商品詳細ページのURLをクローリングして取得する
3. 全てやりたいところだが、1メーカーの全商品に絞ってスクレイピングして、目的のデータを取得する

苦労した処理

  • ページネイションをクリックしながら遷移させる処理
    execute_scriptメソッドで、javascriptを実行する
  • メーカー名、商品名の取得
    置換処理も交えて対処
  • 商品スペックの掲載位置が商品によって異なる
    ロジック組んで回避
  • カラム名称として使用する、ヘッダー行をhtmlから生成
    スクリプトでやる必要は全くないが、pythonでやることにこだわってみた。

感想

  • 収集したデータでクラスタリングして、各メーカーがターゲティングとか分析できないだろうか
  • これを機に、JupyterNotebookから少し離れて、pyスクリプトを書く機会を増やしていきたい
  • 相変わらずhtml,cssの知識、プログラミングスキルが圧倒的に足りないー

BeautifulSoupでスクレイピングを試す python

とにかく試して見るシリーズ第4弾。
今はあまり時間をかけられない+業務上必要なわけでもなく必要になりそうもないので、とりあえず触りだけ。

なぜやるのか

pythonでできそうなことは試しておきたい。
それから、ようやくWeb系のデータに触れて行く機会を得られそうなため、サクッと初歩の初歩だけ試してみる。

ウェブスクレイピングとは

ものすごく端的に言って、Webサイトから欲しい情報だけを抽出する手法。
下記のサイトを参考に進めました。

ウェブスクレイピングについて

http://www.fascinatedwithtofu.com/2017/01/08/scraping1/ https://blog.codecamp.jp/webscraping

requestによるHTTP通信について

http://www.yoheim.net/blog.php?q=20170802

www.yoheim.net

Webスクレイピングの知見が詰まったブログ記事

http://vaaaaaanquish.hatenablog.com/entry/2017/06/25/202924#requests

vaaaaaanquish.hatenablog.com

request + BeautifulSoupによるスクレイピング

qiita.com

https://qiita.com/Atupon0302/items/352811a2d92d6ebab8c2

ビジネスでの活用例

金融

iritec.jp

不動産

miyazawataichi.hatenablog.com

旅行

travelocook.hatenablog.com

 本来個人に属する情報を、まとめて提供しているだけの金融の事例は好感を持てる(てっきり全てAPIだと勘違いしていたが...私もヘビーユーザである。)
不動産の記事でも指摘してあるが、その他の事例はちょっと、、個人が倫理観を持って使うなり、企業が戦略を立てるために情報収集に使うには良い技術だと思うけど、スクレイピングで得た情報がそのままビジネスとして再提供されてしまうのはなんとも腑に落ちない。
 例えるなら、真面目に講義に出た学生から情報引っこ抜く講義に出ない不真面目な学生みたいな...こう言う学習記録系の記事も、スクレイピングしてサクッと書き始めたら倫理的にアレだな。

試してみること

今回は、プロ野球ニュースのタイトル一覧をsportsnaviから取得してみる

# requestsのインポート
import requests
# Webページを取得
r = requests.get('https://sports.yahoo.co.jp/news/list?id=npb')
# レスポンスのステータスコードを確認
print(r.status_code)
200

レスポンスステータスコードの一覧

[https://developer.mozilla.org/ja/docs/Web/HTTP/Status:title]

# BeautifulSoupで要素を抽出
from bs4 import BeautifulSoup
soup = BeautifulSoup(r.text, "lxml")
soup
    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <title>ニュース一覧 - スポーツナビ</title>
    <meta charset="utf-8"/>
    <meta content="telephone=no" name="format-detection"/>
    <meta content="スポーツ総合サイト、スポーツナビのニュース一覧。野球、サッカー、競馬をはじめ、最新のスポーツニュースを掲載しています。" name="description"/>
以下、略
# 記事のタイトルだけを抜き取る
import re
elems = soup.find_all('a', class_='linkMain')
for e in elems:
    print(e.getText())
<プロ野球CS>4安打3打点も悔しそう 阪神・大山
阪神-DeNA、悪天候でも強行開催した理由とは
<プロ野球CS>「勝ちたい気持ちだけ」DeNA・筒香
甲子園、砂3袋をグラウンドに=プロ野球CS
阪神糸原3カ月ぶり安打 右膝靱帯損傷から復活
阪神桑原6失点「やられただけ」中継ぎ陣機能せず
阪神金本監督「考えることが多くて」継投策が裏目に
乙坂、ここぞで3ラン=プロ野球CS・DeNA
<プロ野球CS>「勝利の方程式」雨中でほころび 阪神
<プロ野球CS>待ち望んだ舞台で大仕事 DeNA・乙坂
【阪神】雨中の闘いで大敗、金本監督「選手は気の毒」
中日又吉が秋季練習初ブルペン「全然ダメだった」
乱調にも言い訳せず=プロ野球CS・阪神
雨の甲子園で強行開催…杵渕セ統括「CSという試合の性質も考えて判断」
DeNA打線、本領発揮=21安打猛攻で勢い-プロ野球セCS
<プロ野球CS>「一回から全開」楽天・岸、久しぶりの白星
阪神・金本監督、雨中の逆転負けに悔いも「通常ならば中止…選手が気の毒」
阪神・金本監督、雨天強行開催は「選手が気の毒。ほんと申し訳ない」
<プロ野球CS>西武、元チームメートに脱帽
DeNA、楽天が雪辱しともに1勝…CS第2戦

感想

  • htmlもcssもよく知らないので、まずそっからキャッチアップしないとスクレイピングは無理
  • クローリングできるようになるにはもっとスクリプトかけるようにならないと...
  • 情報の収集、比較、保存の効率化にめちゃくちゃ寄与しそうなので、使えそうな場面があれば、苦労を伴ってもコード書いていきたい

確率を学び直すにあたって

統計検定2級の学習を進めているが、基本的なところから曖昧な理解のままだったなと痛感。
なんとなくわかっていたつもりのことも、自分で手を動かしたり、誰かに説明しようと思うと理解の浅さに気づく。

今回は確率と確率分布について、基本概念部分のキーワードとポイントだけを、できる限り自分の言葉でまとめておく(読み返してしっくりこないところは徐々に更新かける。)

教材は「統計学基礎 日本統計学会編」を使用しています。誤った解釈になっている可能性が多分にあるため、検索して参照したサイトを最後にまとめておきます。

統計学の2つの分野

確率の学習に着手する前に。
統計学には記述統計と推測統計の2分野がある。

記述統計(descriptive statistics)

データ(標本)の属する母集団の特徴を要約し、記述する。
特徴を表現するのに、下記の方法がある。

方法
図・表 度数分布表、ヒストグラム
数値 平均値、中央値、標準偏差、四分位数、相関係数
回帰直線

推測統計(inferential statistics)

データ(標本)そのものではなく、母集団について推測する。
下記は推論統計における重要なポイント2つ - データ(標本)を取る段階で母集団から無作為に抽出しておく - データ(標本)から各種の統計量に基づき、母集団の情報を推理・推論する

標本、サンプル(sample)

実験や調査によって、実際に得られるデータ

母集団(population)

データの属する集団

 

事象に関わる言葉の定義

試行(trial)

偶然に左右される実験や観測の1回ごとの結果

根元事象、素事象(elementary event)、標本点(sampling point)

試行によって起こりうる個々の結果

例)1つのサイコロで観測できる1〜6の目が出る結果一つ一つが根元事象

事象(event)

根元事象の集合

例)1つのサイコロで観測できる根元事象の集合が事象

全事象(whole event)、標本空間(sample space)[$\Omega$]

全ての根元事象の集合

例)2つのサイコロで起こる根源事象を組み合わせて観測する事象が、全事象

和事象

事象 $A_1$ , $A_2$ , $A_3$ ,..., $A_n$ のうち、少なくとも1つが起きる事象

$\displaystyle A_1 \cup A_2 \cup A_3 \cup ... \cup A_n$

積事象

事象 $A_1$ , $A_2$ , $A_3$ ,..., $A_n$ が同時に起こる事象

$\displaystyle A_1 \cap A_2 \cap A_3 \cap ... \cap A_n$

空事象

何も起こらない事象 $\emptyset$

余事象

全事象の中で、 $A$ に含まれていない根元事象からなる事象 $Ac$

$\displaystyle A \cup A^c = \Omega$
$\displaystyle A \cap A^c = \emptyset$

が成り立つ

排反

同時に起こらない事象
事象 $A_1$ , $A_2$ , $A_3$ ,..., $A_n$ のうち、 $A_i$ , $A_j$ , $(i \neq j)$ のとき、

$\displaystyle A_i \cap A_j = \emptyset$

 

確率に関わる言葉の定義

確率(probability)

事象の起こりやすさ(確からしさ)を定量的に表す。定義の仕方はいくつかある。

代表的な確率の三つの定義

  1. 同様に確からしい根元事象を想定した古典的な定義(ラプロスの定義)
    根元事象はどれも同様に起こりやすいと仮定して計算する方法

  2. 多数回の試行による頻度に基づく定義
    十分に大きい回数試行を反復すると、相対度数が一定の値に近く性質に基づいて定義する方法

  3. ベイズ統計学で用いられる主観に基づく定義(主観確率
    反復できない不確実な事象への応用を想定した確率の定義
    ※適用範囲は広いが、算出者によって値が変わりうるため注意深く適用する

確率の公理(コルモゴロフの公理)

数学的な確率の3つの性質

  1. 任意の事象 $A$ に対して $0 \le P(A) \le 1$

  2. 全事象 $\Omega$ に対して $P(\Omega) = 1$

  3. $A_1$,$A_2$,が互いに排反な事象なら、
    $\displaystyle P(A_1 \cup A_2 \cup ...) = P(A_1) + P(A_2) + ...$

加法定理(addition theorem)

和事象の確率に関する定理

事象 $A$ 、事象 $B$ が、互いに排反なとき

$\displaystyle P(A \cup B) = P(A) + P(B)$

事象 $A$ 、事象 $B$ が排反でないとき

$\displaystyle P(A \cup B) = P(A) + P(B) - P(A \cap B)$

条件付き確率(conditional probability)

事象 $A$ 、事象 $B$ が排反でないとき、
$A$ が起こるという条件のもとで、 $B$ の起こる確率

$\displaystyle P(B|A)= \frac{P(A \cap B)}{P(A)}  P(A) \neq 0$

乗法定理(multiplication theorem)

条件付き確率の式に、 $P(A)$ をかけて変形したもの

$\displaystyle P(A \cap B) = P(A)P(B|A)$

$\displaystyle P(B)$ について、同様に

$\displaystyle P(A \cap B) = P(B)P(A|B)$

となる

独立性(Independence)

以下の式が成り立つとき、事象 $A$ と事象 $B$ は独立である

$\displaystyle P(B|A)=P(B), P(A|B)=P(A)$

このとき、乗法定理を適用すると

$\displaystyle P(A \cap B) = P(A)P(B)$

ベイズの定理(bayes' theorem)

これはわかったようなわからないような...自分の言葉にしきれないので、公式だけ

$\displaystyle P(H_i|A) = \frac{P(H_i)P(A|H_i)}{\displaystyle \sum_{j=1}^n P(H_j)P(A|H_i)}$

いつも拝見している
https://mathtrain.jp/bayes
がイメージ掴みやすかったです。

事前確率(prior probability)

事象 $H_i$ が起こる確率

$\displaystyle P(H_i)$

事後確率(posterior probability)

事象 $A$ が起こった後に、事象 $H_i$ が起こる確率

$\displaystyle P(H_i|A)$

確率変数(random varible)

ある事象の取りうる値全体。離散型と連続型がある

例えば、サイコロの場合は離散型であり

確率変数 $X = 1,2,3,4,5,6$

全ての $X$ について

$\displaystyle \displaystyle P(X)= \frac{1}{6}$

である

離散型(discrete type)の確率変数

確率変数 $X$ の取りうる値が離散値

連続型(continuous type)の確率変数

確率変数 $X$ の取りうる値が連続値

連続値と離散値についてはこちら!

確率分布(probability distribution)

確率変数 $X$ の取りうる値とその確率の対応関係

例)サイコロの場合

             
出目 1 2 3 4 5 6
確率 1/6 1/6 1/6 1/6 1/6 1/6

確率関数(probability function)

離散型確率変数 $X$ の確率関数

$\displaystyle P(X = x_i) = f(x_i)  i=(1,2,...)$

確率密度関数(probability density function)

連続型確率変数 $X$ の確率関数

$\displaystyle P(a \le X \le b) = \int_a^b f(x) dx$

積分布関数(cumulative distribution function)分布関数(distribution function)

確率変数 $X$ がある値 $x$ 以下($X \leq x$)の値をとる確率を表す関数

・離散型の時 $\displaystyle F(x)=P(X \le x) = \sum_{X \le x} P(X)$

・連続型の時 $\displaystyle F(x)=P(X \le x) = \int_{-\infty}^u f(u) du$

期待値(expectation)

試行で得られうるすべての値と、それが起こる確率の積を足し合わせたもの

・離散型の時、確率変数 $X$ の期待値は

$\displaystyle E[X] \equiv \sum_ix_if(x_i) = \mu$

・連続型の時、確率変数 $X$ の期待値は

$\displaystyle E[X] \equiv \int_{-\infty}^\infty xf(x)dx = \mu$

分散(variance)

確率分布の散らばりの指標

・離散型の時、確率変数 $X$ の分散は

$\displaystyle V[X] \equiv E[(X-\mu)^2]=\sum_i(x_i-\mu)^2 f(x_i)=$

・連続型の時、確率変数 $X$ の分散は

$\displaystyle V[X] \equiv E[(X-\mu)|^2]=\int_{-\infty}^\infty(x-\mu)^2f(x)dx=$

標準偏差(standard deviation)

分散の平方根 $\sigma$ 。確率変数の散らばり具合を示す。

本記事を書くにあたって参考にした書籍、サイト

まずは数式を記述するにあたって

www.tcom242242.site

www.latex-cmd.com

様々理解を進めるために

mathtrain.jp

bellcurve.jp

識別(分類)と回帰

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

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

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

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




感想

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

参考にしたサイト