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
Webスクレイピングの知見が詰まったブログ記事
http://vaaaaaanquish.hatenablog.com/entry/2017/06/25/202924#requests
request + BeautifulSoupによるスクレイピング
https://qiita.com/Atupon0302/items/352811a2d92d6ebab8c2
ビジネスでの活用例
金融
不動産
旅行
本来個人に属する情報を、まとめて提供しているだけの金融の事例は好感を持てる(てっきり全て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戦
感想
確率を学び直すにあたって
統計検定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)
事象の起こりやすさ(確からしさ)を定量的に表す。定義の仕方はいくつかある。
代表的な確率の三つの定義
同様に確からしい根元事象を想定した古典的な定義(ラプロスの定義)
根元事象はどれも同様に起こりやすいと仮定して計算する方法多数回の試行による頻度に基づく定義
十分に大きい回数試行を反復すると、相対度数が一定の値に近く性質に基づいて定義する方法ベイズ統計学で用いられる主観に基づく定義(主観確率)
反復できない不確実な事象への応用を想定した確率の定義
※適用範囲は広いが、算出者によって値が変わりうるため注意深く適用する
確率の公理(コルモゴロフの公理)
数学的な確率の3つの性質
任意の事象 $A$ に対して $0 \le P(A) \le 1$
全事象 $\Omega$ に対して $P(\Omega) = 1$
$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$ 。確率変数の散らばり具合を示す。
本記事を書くにあたって参考にした書籍、サイト
まずは数式を記述するにあたって
様々理解を進めるために
識別(分類)と回帰
分類(識別)と回帰を理解するのにちょっと戸惑ったのでメモ。
決定木分析とロジスティック回帰分析の理解度を深めようと振り返りをしていたところつまづいた。
Webで検索してみると書き手によってこちらの解釈が微妙に変わってしまい、ちょっとニュアンスが掴みづらかった。自分なりに調べて解釈した結果をメモっておく。
この記事では、分類(識別)と識別(分類)を意識的に書き分けています。
■ 識別(分類)と回帰とは
識別(分類)と回帰のどちらも、統計学において要素を複数のクラス(グループ)に分類するために線引きをすることを指す。
−私的な解釈のポイント
分類(識別)と回帰と表現されるとめちゃくちゃ混乱する
(要素をクラスに分類するための方法に、分類と回帰があるとかなに言ってだ)回帰と回帰分析を分けて考えないとまあまあ混乱する
(回帰で分類するのと、回帰で得られたモデルで予測(分析)を行うのは別の話)
○ 識別(分類)
離散値の目的変数をクラス分けするために線引きすることを識別(分類)という。
- 識別と覚えた方がイメージが湧きやすい
- 決定木分析では、訓練データから分類器を生成し、その分類器を利用してデータを分類する
○ 回帰
連続値の目的変数をクラス分けするために線引きすることを回帰という。
目的変数と説明変数の間にモデルを当てはめて、目的変数をクラスに分類する。
- 回帰分析では、目的変数と説明変数の間に当てはめたモデルを利用して数値を予測する
■ 連続値と離散値
ついでにまとめておく。
連続値とも離散値とも取れる場合があって、ちょっとややこしいけどまあ大丈夫。
○ 連続値
測ることができるデータ、連続していて細かく測ることができる
数直線上に書くと、どこにでも存在しうるデータ
例) 身長、体重、時間、気温など
○ 離散値
数えることができるデータ、それ以上細かくすることができない
数直線上に書くと、目盛の上に存在するデータ
例) 人数、回数など
○ 連続、離散どちらとも取れるケース
連続値の場合
身長や体重は、cmやkgなどの単位で表されることから離散値とも考えられる。便宜的に近似値に直しているとして、基本的には連続値として扱うことが多い。離散値の場合
テストの点数をその人の能力を階級値で示すものと考え、75点は74.5〜75.4点を取れる能力とし、連続値として扱う場合がある。
参考にしたサイト
統計検定2級を受験する
- 統計検定とは
- 試験概要
- 資格の種類
- 日程
- 申し込み方法
- 2級の合格ラインと合格率
- (おまけ)成績優秀者
- 2級の試験内容について詳しく
- なぜやるのか
- 曲がりなりにもデータアナリストを名乗りたいから
- 資格なんて取得しても役に立たない論
- 異論はない
- 要は役に立てようとしているかどうか
- 統計学には普遍的な魅力がある
- なぜブログに書いたのか
- 人の目がある気分になれる
- どう勉強していくのか
- 使う教材
- 平日と休日の勉強法
- 目標
- 8割とって合格
- 実務に投影し、活かしていく
統計検定とは
試験概要
一般財団法人 統計質保証推進協会が主催、日本統計学会公式認定・総務省後援の全国統一試験。 統計検定は2011年に発足したということで、比較的新しい資格試験のようだ。
[公式サイト][f6e19f98]によると、
「統計検定」とは、統計に関する知識や活用力を評価する全国統一試験です。 データに基づいて客観的に判断し、科学的に問題を解決する能力は、仕事や研究をするための21世紀型スキルとして国際社会で広く認められています。 日本統計学会は、中高生・大学生・職業人を対象に、各レベルに応じて体系的に国際通用性のある統計活用能力評価システムを研究開発し、統計検定として資格認定します。
資格の種類
いわゆる受験資格は設けられておらず、特に制限なく受験することができる。 試験時間帯の異なる種別であれば、併願することも可能。
こちらも[公式サイト][f6e19f98]から引用。
試験の種別 | 試験内容 |
---|---|
統計調査士 | 統計に関する基本的知識と利活用 |
専門統計調査士 | 調査全般に関わる高度な専門的知識と利活用手法 |
1級 | 実社会の様々な分野でのデータ解析を遂行する統計専門力 |
準1級 | 統計学の活用力 ─ データサイエンスの基礎 |
2級 | 大学基礎統計学の知識と問題解決力 |
3級 | データの分析において重要な概念を身に付け、身近な問題に活かす力 |
4級 | データや表・グラフ、確率に関する基本的な知識と具体的な文脈の中での活用力 |
※RSS/JSS試験なる、英国王立統計学会(Royal Statistical Society)との共同認定試験があったようだが、2017年5月を最後に同形態の試験を終了とのこと
難易度は上から順、今回私が受験する 2級 は、
大学基礎統計学の知識と問題解決力
大学基礎統計学…これは落とせない。
続きを読むscikit-learnでクラスタリング分析を試す
とにかく試して見るシリーズ第3弾。
有効な分析か否かに関わらず、全試行過程を掲載します。
誰も見てないと思うんですけど、「ここちゃんと意識したほうがいいよ」、「そこわかってないね全然ダメだよ」、とかコメントついたらむちゃくちゃ嬉しいです。
- なぜやるのか
- クラスタリングとは
- 今回使用するデータ
- 試行過程と結果
- 感想
- 参考にしたサイト
なぜやるのか
職場でうんうん言いながらクラスタリングしてる人がいたので、やって見たくなった。
おじさんもQueryばっか書いてないで分析できるようにならないと。
クラスタリングとは
概要
- 教師なしデータ分類の手法
- データの集まりをデータ間の類似度に従って、いつかのグループに分ける
- 階層的手法と非階層的手法に分類される
階層的手法
- 最短距離法 (nearest neighbor method)
- 最長距離法 (furthest neighbor method)
- 群平均法 (group average method)
- ウォード法 (Ward’s method)
非階層的手法
データの分割の良さを表す評価関数によって、最適解を探索する手法 階層的手法ではデータが多いと階層構造が複雑になってしまうため、非階層的手法のほうが実用的
- k-means法(k平均法)
参考にしたサイト*1
参考にしたサイト*2
ビジネスでの活用例
市場細分化に基づくターゲット市場の選定
・顧客をセグメンテーションし、セグメントに適合するマーケティング施策を展開する
・顧客のどの属性をセグメンテーション変数として使うべきかは、分析の目的に合わせて選別する必要がある
・コトラーによる顧客市場の主要なセグメンテーション変数には、以下4つがある
- 人口統計学的変数 (Demographic Variables)
- 地理的変数 (Geographic Variables)
- 心理的変数 (Psychographic Variables)
- 行動変数 (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))
- 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))
- Regionごとでもそれほど分布は変わらない
Milkの分布
Channelごと
plt.hist([df_Channel1['Milk'],df_Channel2['Milk']], bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,40000))
- 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))
- Regionごとではそれほど分布は変わらない
Groceryの分布
Channelごと
plt.hist([df_Channel1['Grocery'],df_Channel2['Grocery']], bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,40000))
- 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))
- 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))
- 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))
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))
- 洗剤、紙製品は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))
- Regionごとではそれほど分布は変わらない
Delicatessenの分布
Channelごと
plt.hist([df_Channel1['Delicassen'],df_Channel2['Delicassen']], bins=15, label=['Horeca','Retail'], color=['#FACC2E','#2E64FE'], rwidth=100, range=(0,20000))
- 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))
- 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 |
- 利益をあげている地域と販売チャネルは
- . Other RegionのHoreca
- . Other RegionのRetail
- . LisbonのHoreca
- 品目単位で利益をあげているのは
- . Other Region,Horeca,Fresh
- . Other Region,Retail,Grocery
- . 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の方が、取引先あたりの卸売額が大きい
- 取引先あたりの卸売額が多いのは
- Lisbon,Retail
- Other Region,Retail
- 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 |
- 取引先の数が多い順に販促をかけるとすると下記をターゲットにするのが良いかもしれない
- Other Region,Horeca
- Other Region,Retail
- Lisbon,Horeca
感想
- 分布を確認はして見たものの、特徴を掴むのにあまり有効な気がしなかった。やり方が悪い気がする
- グループ化は、RegionとChannelを組み合わせたものだけでよかったかも
- このぐらいのデータ量なら、クロス集計で一生懸命紐解いていったほうがわかりやすく有益な情報が得られそうな気がした
- クラスタリング後の考察が甘いが、事業を深く知る手立てがあれば、いろんな切り口で分析するアイデアが湧いてきそうで楽しいなと思えた!
参考にしたサイト
*1:こちらのサイトを参考にしました
http://nlp.dse.ibaraki.ac.jp/~shinnou/zemi2008/Rclustering/r-tanaka-0415.pdf
*2:こちらのサイトを参考にしました
*3:こちらのサイトを参考にしました
scikit-learnでロジスティック回帰分析を試す
とにかく試して見るシリーズ第二弾。
ロジスティック回帰分析とは
概要
ロジスティック回帰分析 (Logistic regression) は、発生確率を予測する手法。
2値しかとりえない値を目的変数とし、説明変数を用いてその発生確率を説明する。
ロジスティック回帰分析でできること
- 予測値を算出する
- 説明変数の目的変数に対する貢献度を算出する
参考にしたサイト*1
ビジネスでの活用例
取り組んだ課題
ロジスティック回帰に適していそうなデータセットを自力で探し、予測モデルを作ってみる
試行過程と結果
使えそうなデータセットを探す
目的変数が0または1の2値になるデータセットを探した結果、過去に利用したサイトにちょうどいいデータセットがあった。
台湾で実施された、6ヶ月間のクレジットカードの支払履歴と翌月の支払状況の調査結果のようだ。
目的変数となる支払状況は0,1の2値で示せるため、ロジスティック回帰に適していそうだ。
(のちにKaggleに出ているのを発見した)
データセットを確認する
データの入手元にあった各項目の説明
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')
# 利用可能枠の分布を見てみる 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')
# 年齢分布を見てみる 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')
# 説明変数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
全データを使用した時と同程度の精度を出すことができた
感想
- 決定木分析に比べてすごく扱いやすく、わかりやすい印象
- 説明変数の設定は、どのように試行して検証するのが良いのかを理解していく必要がある
- データの収集方法として、目的変数を得る前に説明変数を収集する(顧客の属性や行動履歴)のと、目的変数と一緒に説明変数を収集する(アンケート)のとでは、なんとなくだがロジスティック回帰の有効性に影響がありそうな気がする
参考にしたサイト
*1:こちらのサイトを参考にしました
scikit-learnで決定木分析(CART)を試す
とにかく試して見るシリーズ第一弾。
なぜやるのか
いつまでもデータマート拵えおじさんのままではマズいため、比較的難易度が低くビジネスで幅広く応用が効くらしい決定木分析をやってみる
決定木分析とは
概要
決定木分析 (Decision Tree Analysis) は、機械学習の手法の一つ。木を逆にしたようなデータ構造を用いて分類と回帰を行う。
決定木分析の特徴
- 樹木状の構造で学習結果を視覚化でき、ルールをシンプルに表現できるため、論理的な解釈が容易
- データの標準化 (正規化) やダミー変数の作成を必要としないため、前処理の手間がほとんど不要
- カテゴリカルデータと数値データの両方を扱うことが可能
- 検定を行って、作成したモデルの正しさを評価することが可能
参考にしたサイト*1
ビジネスでの活用例
- 顧客別の購買履歴から自社の製品を購入している顧客の特徴を分析
- 金融機関の取引履歴から顧客属性別の貸し倒れリスクを分析
- 機械の動作ログから故障につながる指標を分析
参考にしたサイト*2
取り組んだ課題
Rの標準データセット[HairEyeColor]のデータを使ってHair[髪の色]、Eye Color[瞳の色]から、Sex[性別]を予測する決定木のモデルをPythonで作成する*3
試行過程と結果
1.データセットを確認する
# 編集前の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())
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
*4:こちらのサイトを参考にしましたmemopy.hatenadiary.jp
*5:こちらのサイトを参考にしました
todoa2c.github.io