In [2]:
# chapter 9 教師なし学習

"""
・クラスタリング
    データをいくつかのクラスに分類する
    顧客セブメンテーションに使用する
     
・主成分分析
    説明変数が多すぎるときに使用する(次元圧縮)

・マーケットバスケット分析(アソシエーションルール)
    どういう組み合わせが大きかを見つける
"""
Out[2]:
'\n・クラスタリング\n    データをいくつかのクラスに分類する\n    顧客セブメンテーションに使用する\n     \n・主成分分析\n    説明変数が多すぎるときに使用する(次元圧縮)\n\n・マーケットバスケット分析(アソシエーションルール)\n    どういう組み合わせが大きかを見つける\n'
In [199]:
# 基本的なモジュール
import numpy as np
import pandas as pd
import scipy as sp

# 可視化モジュール
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
%matplotlib inline
%precision 3

# 機械学習モジュール
import sklearn
In [6]:
# クラスタリング k-means法

"""
クラスタ数を3と仮定した場合

1.全データをプロットする
2.ランダムに3つの点をプロットする
3.各ランダム点を クラスタ1の重心点、
    クラスタ2の重心点、クラスタ3の重心点 と呼ぶ
4.各データはどの重心点に近いか調べ所属させる
5.各クラスタの本当の重心を算出する
6.本当の重心を使って、再び各データをどれかの重心に所属させる
7.これを繰り返していき、重心がずれなくなったところで
    クラスタリング終了
"""
Out[6]:
'\nクラスタ数を3と仮定した場合\n\n1.全データをプロットする\n2.ランダムに3つの点をプロットする\n3.各ランダム点を\u3000クラスタ1の重心点、\n    クラスタ2の重心点、クラスタ3の重心点\u3000と呼ぶ\n4.各データはどの重心点に近いか調べ所属させる\n5.各クラスタの本当の重心を算出する\n6.本当の重心を使って、再び各データをどれかの重心に所属させる\n7.これを繰り返していき、重心がずれなくなったところで\n    クラスタリング終了\n'
In [7]:
# K-meansをやってみる
from sklearn.cluster import KMeans
In [57]:
# maek_blobs サンプルデータを使っていく
from sklearn.datasets import make_blobs

# ランダムステートは 0のときまばらなので,1でやってみる
data = make_blobs(random_state=1) 


# 説明変数は二次元座標データになっている
# サイズを調べる
print(data[0].shape)
"""(100, 2)
100データある
説明変数は2つある
"""

pd.DataFrame(data[0]).head()
(100, 2)
Out[57]:
0 1
0 -0.794152 2.104951
1 -9.151552 -4.812864
2 -11.441826 -4.457814
3 -9.767618 -3.191337
4 -4.536556 -8.401863
In [58]:
# 目的変数
pd.DataFrame(data[1]).head()
Out[58]:
0
0 0
1 1
2 1
3 1
4 2
In [59]:
# 説明変数をプロットしてみる
plt.scatter(data[0][:,0], data[0][:,1])
Out[59]:
<matplotlib.collections.PathCollection at 0x26f9bb38f98>
In [72]:
# クラスタ説明変数をプロットしてみる
plt.scatter(data[0][:,0], data[0][:,1], color='black')
Out[72]:
<matplotlib.collections.PathCollection at 0x26f9bd38908>
In [61]:
# k-means の引数を確認する
help(KMeans)
"""
・クラスター数はデフォルトで8
・重心点の初期位置はk-means++なので、互いに離れた位置からスタートする。
 |  n_clusters : int, optional, default: 8
 |      The number of clusters to form as well as the number of
 |      centroids to generate.
 |  
 |  init : {'k-means++', 'random' or an ndarray}
 |      Method for initialization, defaults to 'k-means++':
 |  
 |      'k-means++' : selects initial cluster centers for k-mean
 |      clustering in a smart way to speed up convergence. See section
 |      Notes in k_init for more details.
 |  
 |      'random': choose k observations (rows) at random from data for
 |      the initial centroids.
"""
Help on class KMeans in module sklearn.cluster.k_means_:

class KMeans(sklearn.base.BaseEstimator, sklearn.base.ClusterMixin, sklearn.base.TransformerMixin)
 |  KMeans(n_clusters=8, init='k-means++', n_init=10, max_iter=300, tol=0.0001, precompute_distances='auto', verbose=0, random_state=None, copy_x=True, n_jobs=None, algorithm='auto')
 |  
 |  K-Means clustering
 |  
 |  Read more in the :ref:`User Guide <k_means>`.
 |  
 |  Parameters
 |  ----------
 |  
 |  n_clusters : int, optional, default: 8
 |      The number of clusters to form as well as the number of
 |      centroids to generate.
 |  
 |  init : {'k-means++', 'random' or an ndarray}
 |      Method for initialization, defaults to 'k-means++':
 |  
 |      'k-means++' : selects initial cluster centers for k-mean
 |      clustering in a smart way to speed up convergence. See section
 |      Notes in k_init for more details.
 |  
 |      'random': choose k observations (rows) at random from data for
 |      the initial centroids.
 |  
 |      If an ndarray is passed, it should be of shape (n_clusters, n_features)
 |      and gives the initial centers.
 |  
 |  n_init : int, default: 10
 |      Number of time the k-means algorithm will be run with different
 |      centroid seeds. The final results will be the best output of
 |      n_init consecutive runs in terms of inertia.
 |  
 |  max_iter : int, default: 300
 |      Maximum number of iterations of the k-means algorithm for a
 |      single run.
 |  
 |  tol : float, default: 1e-4
 |      Relative tolerance with regards to inertia to declare convergence
 |  
 |  precompute_distances : {'auto', True, False}
 |      Precompute distances (faster but takes more memory).
 |  
 |      'auto' : do not precompute distances if n_samples * n_clusters > 12
 |      million. This corresponds to about 100MB overhead per job using
 |      double precision.
 |  
 |      True : always precompute distances
 |  
 |      False : never precompute distances
 |  
 |  verbose : int, default 0
 |      Verbosity mode.
 |  
 |  random_state : int, RandomState instance or None (default)
 |      Determines random number generation for centroid initialization. Use
 |      an int to make the randomness deterministic.
 |      See :term:`Glossary <random_state>`.
 |  
 |  copy_x : boolean, optional
 |      When pre-computing distances it is more numerically accurate to center
 |      the data first.  If copy_x is True (default), then the original data is
 |      not modified, ensuring X is C-contiguous.  If False, the original data
 |      is modified, and put back before the function returns, but small
 |      numerical differences may be introduced by subtracting and then adding
 |      the data mean, in this case it will also not ensure that data is
 |      C-contiguous which may cause a significant slowdown.
 |  
 |  n_jobs : int or None, optional (default=None)
 |      The number of jobs to use for the computation. This works by computing
 |      each of the n_init runs in parallel.
 |  
 |      ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
 |      ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
 |      for more details.
 |  
 |  algorithm : "auto", "full" or "elkan", default="auto"
 |      K-means algorithm to use. The classical EM-style algorithm is "full".
 |      The "elkan" variation is more efficient by using the triangle
 |      inequality, but currently doesn't support sparse data. "auto" chooses
 |      "elkan" for dense data and "full" for sparse data.
 |  
 |  Attributes
 |  ----------
 |  cluster_centers_ : array, [n_clusters, n_features]
 |      Coordinates of cluster centers. If the algorithm stops before fully
 |      converging (see ``tol`` and ``max_iter``), these will not be
 |      consistent with ``labels_``.
 |  
 |  labels_ :
 |      Labels of each point
 |  
 |  inertia_ : float
 |      Sum of squared distances of samples to their closest cluster center.
 |  
 |  n_iter_ : int
 |      Number of iterations run.
 |  
 |  Examples
 |  --------
 |  
 |  >>> from sklearn.cluster import KMeans
 |  >>> import numpy as np
 |  >>> X = np.array([[1, 2], [1, 4], [1, 0],
 |  ...               [10, 2], [10, 4], [10, 0]])
 |  >>> kmeans = KMeans(n_clusters=2, random_state=0).fit(X)
 |  >>> kmeans.labels_
 |  array([1, 1, 1, 0, 0, 0], dtype=int32)
 |  >>> kmeans.predict([[0, 0], [12, 3]])
 |  array([1, 0], dtype=int32)
 |  >>> kmeans.cluster_centers_
 |  array([[10.,  2.],
 |         [ 1.,  2.]])
 |  
 |  See also
 |  --------
 |  
 |  MiniBatchKMeans
 |      Alternative online implementation that does incremental updates
 |      of the centers positions using mini-batches.
 |      For large scale learning (say n_samples > 10k) MiniBatchKMeans is
 |      probably much faster than the default batch implementation.
 |  
 |  Notes
 |  -----
 |  The k-means problem is solved using either Lloyd's or Elkan's algorithm.
 |  
 |  The average complexity is given by O(k n T), were n is the number of
 |  samples and T is the number of iteration.
 |  
 |  The worst case complexity is given by O(n^(k+2/p)) with
 |  n = n_samples, p = n_features. (D. Arthur and S. Vassilvitskii,
 |  'How slow is the k-means method?' SoCG2006)
 |  
 |  In practice, the k-means algorithm is very fast (one of the fastest
 |  clustering algorithms available), but it falls in local minima. That's why
 |  it can be useful to restart it several times.
 |  
 |  If the algorithm stops before fully converging (because of ``tol`` or
 |  ``max_iter``), ``labels_`` and ``cluster_centers_`` will not be consistent,
 |  i.e. the ``cluster_centers_`` will not be the means of the points in each
 |  cluster. Also, the estimator will reassign ``labels_`` after the last
 |  iteration to make ``labels_`` consistent with ``predict`` on the training
 |  set.
 |  
 |  Method resolution order:
 |      KMeans
 |      sklearn.base.BaseEstimator
 |      sklearn.base.ClusterMixin
 |      sklearn.base.TransformerMixin
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, n_clusters=8, init='k-means++', n_init=10, max_iter=300, tol=0.0001, precompute_distances='auto', verbose=0, random_state=None, copy_x=True, n_jobs=None, algorithm='auto')
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  fit(self, X, y=None, sample_weight=None)
 |      Compute k-means clustering.
 |      
 |      Parameters
 |      ----------
 |      X : array-like or sparse matrix, shape=(n_samples, n_features)
 |          Training instances to cluster. It must be noted that the data
 |          will be converted to C ordering, which will cause a memory
 |          copy if the given data is not C-contiguous.
 |      
 |      y : Ignored
 |          not used, present here for API consistency by convention.
 |      
 |      sample_weight : array-like, shape (n_samples,), optional
 |          The weights for each observation in X. If None, all observations
 |          are assigned equal weight (default: None)
 |  
 |  fit_predict(self, X, y=None, sample_weight=None)
 |      Compute cluster centers and predict cluster index for each sample.
 |      
 |      Convenience method; equivalent to calling fit(X) followed by
 |      predict(X).
 |      
 |      Parameters
 |      ----------
 |      X : {array-like, sparse matrix}, shape = [n_samples, n_features]
 |          New data to transform.
 |      
 |      y : Ignored
 |          not used, present here for API consistency by convention.
 |      
 |      sample_weight : array-like, shape (n_samples,), optional
 |          The weights for each observation in X. If None, all observations
 |          are assigned equal weight (default: None)
 |      
 |      Returns
 |      -------
 |      labels : array, shape [n_samples,]
 |          Index of the cluster each sample belongs to.
 |  
 |  fit_transform(self, X, y=None, sample_weight=None)
 |      Compute clustering and transform X to cluster-distance space.
 |      
 |      Equivalent to fit(X).transform(X), but more efficiently implemented.
 |      
 |      Parameters
 |      ----------
 |      X : {array-like, sparse matrix}, shape = [n_samples, n_features]
 |          New data to transform.
 |      
 |      y : Ignored
 |          not used, present here for API consistency by convention.
 |      
 |      sample_weight : array-like, shape (n_samples,), optional
 |          The weights for each observation in X. If None, all observations
 |          are assigned equal weight (default: None)
 |      
 |      Returns
 |      -------
 |      X_new : array, shape [n_samples, k]
 |          X transformed in the new space.
 |  
 |  predict(self, X, sample_weight=None)
 |      Predict the closest cluster each sample in X belongs to.
 |      
 |      In the vector quantization literature, `cluster_centers_` is called
 |      the code book and each value returned by `predict` is the index of
 |      the closest code in the code book.
 |      
 |      Parameters
 |      ----------
 |      X : {array-like, sparse matrix}, shape = [n_samples, n_features]
 |          New data to predict.
 |      
 |      sample_weight : array-like, shape (n_samples,), optional
 |          The weights for each observation in X. If None, all observations
 |          are assigned equal weight (default: None)
 |      
 |      Returns
 |      -------
 |      labels : array, shape [n_samples,]
 |          Index of the cluster each sample belongs to.
 |  
 |  score(self, X, y=None, sample_weight=None)
 |      Opposite of the value of X on the K-means objective.
 |      
 |      Parameters
 |      ----------
 |      X : {array-like, sparse matrix}, shape = [n_samples, n_features]
 |          New data.
 |      
 |      y : Ignored
 |          not used, present here for API consistency by convention.
 |      
 |      sample_weight : array-like, shape (n_samples,), optional
 |          The weights for each observation in X. If None, all observations
 |          are assigned equal weight (default: None)
 |      
 |      Returns
 |      -------
 |      score : float
 |          Opposite of the value of X on the K-means objective.
 |  
 |  transform(self, X)
 |      Transform X to a cluster-distance space.
 |      
 |      In the new space, each dimension is the distance to the cluster
 |      centers.  Note that even if X is sparse, the array returned by
 |      `transform` will typically be dense.
 |      
 |      Parameters
 |      ----------
 |      X : {array-like, sparse matrix}, shape = [n_samples, n_features]
 |          New data to transform.
 |      
 |      Returns
 |      -------
 |      X_new : array, shape [n_samples, k]
 |          X transformed in the new space.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from sklearn.base.BaseEstimator:
 |  
 |  __getstate__(self)
 |  
 |  __repr__(self, N_CHAR_MAX=700)
 |      Return repr(self).
 |  
 |  __setstate__(self, state)
 |  
 |  get_params(self, deep=True)
 |      Get parameters for this estimator.
 |      
 |      Parameters
 |      ----------
 |      deep : boolean, optional
 |          If True, will return the parameters for this estimator and
 |          contained subobjects that are estimators.
 |      
 |      Returns
 |      -------
 |      params : mapping of string to any
 |          Parameter names mapped to their values.
 |  
 |  set_params(self, **params)
 |      Set the parameters of this estimator.
 |      
 |      The method works on simple estimators as well as on nested objects
 |      (such as pipelines). The latter have parameters of the form
 |      ``<component>__<parameter>`` so that it's possible to update each
 |      component of a nested object.
 |      
 |      Returns
 |      -------
 |      self
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from sklearn.base.BaseEstimator:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

In [62]:
# 機械学習モデル K-meansのインスタンスを作成する
"""
・クラスター数=3
・重心初期位置はランダムとする
"""
model = KMeans(n_clusters=3, init='random')
In [64]:
# 訓練する(クラスタ重心を算出する)
model.fit(data[0])
Out[64]:
KMeans(algorithm='auto', copy_x=True, init='random', max_iter=300, n_clusters=3,
       n_init=10, n_jobs=None, precompute_distances='auto', random_state=None,
       tol=0.0001, verbose=0)
In [90]:
# クラスタリングされた結果のクラスタ番号を確認する
y_pred = model.predict(data[0])
pd.DataFrame(y_pred,columns=['y']).head()
Out[90]:
y
0 1
1 0
2 0
3 0
4 2
In [103]:
# 結果をデータフレームにまとめる 
data_merge = pd.concat([pd.DataFrame(data[0][:,0]),
                        pd.DataFrame(data[0][:,1]),
                        pd.DataFrame(y_pred)],
                        axis=1)

# columnsをつける
data_merge.columns = ['X0', 'X1', 'y_pred']
"""要注意:ハマりどころ

pd.concatを使用するときは リストとしてdfを入れるので[ ] を忘れないこと。

"""
data_merge.head()
Out[103]:
X0 X1 y_pred
0 -0.794152 2.104951 1
1 -9.151552 -4.812864 0
2 -11.441826 -4.457814 0
3 -9.767618 -3.191337 0
4 -4.536556 -8.401863 2
In [107]:
# 結果を可視化してみる
ax = None
colors =['blue', 'red', 'yellow']

for i, data in data_merge.groupby('y_pred'):
    
    ax = data.plot.scatter(x='X0', y='X1', 
                           color=colors[i],
                           label=f'cluster_{i}',
                           ax=ax)megamite
"""結果
ちゃんと別れている
"""
In [108]:
# 以上、練習でした。
# 次から、実データでやってみます
In [113]:
# 顧客のクラスタリングをやってみる
# 目的変数(正解データ)が特定されていない場合
import requests, zipfile
import io

url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip'

r = requests.get(url, stream=True)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall()
In [115]:
data = pd.read_csv('bank-full.csv', sep=';')
In [117]:
data.head()
Out[117]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y
0 58 management married tertiary no 2143 yes no unknown 5 may 261 1 -1 0 unknown no
1 44 technician single secondary no 29 yes no unknown 5 may 151 1 -1 0 unknown no
2 33 entrepreneur married secondary no 2 yes yes unknown 5 may 76 1 -1 0 unknown no
3 47 blue-collar married unknown no 1506 yes no unknown 5 may 92 1 -1 0 unknown no
4 33 unknown single unknown no 1 no no unknown 5 may 198 1 -1 0 unknown no
In [133]:
# データの大きさ
print(data.shape)

#説明変数の名前
print(data.keys())
(45211, 17)
Index(['age', 'job', 'marital', 'education', 'default', 'balance', 'housing',
       'loan', 'contact', 'day', 'month', 'duration', 'campaign', 'pdays',
       'previous', 'poutcome', 'y'],
      dtype='object')
In [134]:
with open('bank-names.txt', 'r') as f:
    text = f.read()

print(text)
Citation Request:
  This dataset is public available for research. The details are described in [Moro et al., 2011]. 
  Please include this citation if you plan to use this database:

  [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. 
  In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM'2011, pp. 117-121, Guimarテ」es, Portugal, October, 2011. EUROSIS.

  Available at: [pdf] http://hdl.handle.net/1822/14838
                [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt

1. Title: Bank Marketing

2. Sources
   Created by: Paulo Cortez (Univ. Minho) and Sテゥrgio Moro (ISCTE-IUL) @ 2012
   
3. Past Usage:

  The full dataset was described and analyzed in:

  S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. 
  In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM'2011, pp. 117-121, Guimarテ」es, 
  Portugal, October, 2011. EUROSIS.

4. Relevant Information:

   The data is related with direct marketing campaigns of a Portuguese banking institution. 
   The marketing campaigns were based on phone calls. Often, more than one contact to the same client was required, 
   in order to access if the product (bank term deposit) would be (or not) subscribed. 

   There are two datasets: 
      1) bank-full.csv with all examples, ordered by date (from May 2008 to November 2010).
      2) bank.csv with 10% of the examples (4521), randomly selected from bank-full.csv.
   The smallest dataset is provided to test more computationally demanding machine learning algorithms (e.g. SVM).

   The classification goal is to predict if the client will subscribe a term deposit (variable y).

5. Number of Instances: 45211 for bank-full.csv (4521 for bank.csv)

6. Number of Attributes: 16 + output attribute.

7. Attribute information:

   For more information, read [Moro et al., 2011].

   Input variables:
   # bank client data:
   1 - age (numeric)
   2 - job : type of job (categorical: "admin.","unknown","unemployed","management","housemaid","entrepreneur","student",
                                       "blue-collar","self-employed","retired","technician","services") 
   3 - marital : marital status (categorical: "married","divorced","single"; note: "divorced" means divorced or widowed)
   4 - education (categorical: "unknown","secondary","primary","tertiary")
   5 - default: has credit in default? (binary: "yes","no")
   6 - balance: average yearly balance, in euros (numeric) 
   7 - housing: has housing loan? (binary: "yes","no")
   8 - loan: has personal loan? (binary: "yes","no")
   # related with the last contact of the current campaign:
   9 - contact: contact communication type (categorical: "unknown","telephone","cellular") 
  10 - day: last contact day of the month (numeric)
  11 - month: last contact month of year (categorical: "jan", "feb", "mar", ..., "nov", "dec")
  12 - duration: last contact duration, in seconds (numeric)
   # other attributes:
  13 - campaign: number of contacts performed during this campaign and for this client (numeric, includes last contact)
  14 - pdays: number of days that passed by after the client was last contacted from a previous campaign (numeric, -1 means client was not previously contacted)
  15 - previous: number of contacts performed before this campaign and for this client (numeric)
  16 - poutcome: outcome of the previous marketing campaign (categorical: "unknown","other","failure","success")

  Output variable (desired target):
  17 - y - has the client subscribed a term deposit? (binary: "yes","no")

8. Missing Attribute Values: None

In [ ]:
 
In [138]:
# 欠損データの確認
print(data.isnull().sum())
"""
結果:データの欠損はない
"""
age          0
job          0
marital      0
education    0
default      0
balance      0
housing      0
loan         0
contact      0
day          0
month        0
duration     0
campaign     0
pdays        0
previous     0
poutcome     0
y            0
dtype: int64
Out[138]:
'\n結果:データの欠損はない\n'
In [164]:
# 使用するデータを3つだけ使用する
data_sub = data[['age', 'balance', 'campaign', 'previous']]

# データを標準化する 
from sklearn.preprocessing import StandardScaler

# StandardScalerのインスタンスを作成
standard_scaler = StandardScaler()

# 標準化の実行
standard_scaler.fit(data_sub)
data_sub_std = standard_scaler.transform(data_sub) 
print(pd.DataFrame(data_sub_std, 
                   columns=['age', 'balance', 'campaign', 'previous']).head())
print(data_sub.info())
        age   balance  campaign  previous
0  1.606965  0.256419 -0.569351  -0.25194
1  0.288529 -0.437895 -0.569351  -0.25194
2 -0.747384 -0.446762 -0.569351  -0.25194
3  0.571051  0.047205 -0.569351  -0.25194
4 -0.747384 -0.447091 -0.569351  -0.25194
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45211 entries, 0 to 45210
Data columns (total 4 columns):
age         45211 non-null int64
balance     45211 non-null int64
campaign    45211 non-null int64
previous    45211 non-null int64
dtypes: int64(4)
memory usage: 1.4 MB
None
In [155]:
# K-meansクラスタリングモデルのインスタンスを作成する
model = KMeans()

# 重心を計算する(訓練する)
model.fit(data_sub_std)
Out[155]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
       n_clusters=8, n_init=10, n_jobs=None, precompute_distances='auto',
       random_state=None, tol=0.0001, verbose=0)
In [170]:
# 分類結果を確認する
y_pred = model.predict(data_sub_std)
print(pd.DataFrame(y_pred).head())
   0
0  2
1  2
2  1
3  2
4  1
In [191]:
# では可視化してみましょう。
"""
さっきのような2次元グラフでは、もはや表現できません。

"""

#クラスター番号を取得する
"""
 上記の y_pred = model.predict(data_sub_std) と同じ
"""
labels = pd.Series(model.labels_, name='cluster_number')

# クラスター番号とそれのデータ数を算出する
df = pd.DataFrame(labels.value_counts(sort=False))
df
Out[191]:
cluster_number
0 701
1 22868
2 13746
3 2533
4 1382
5 3762
6 218
7 1
In [210]:
# バーチャートで可視化する
plt.bar(df.index, df['cluster_number'])

"""結果
・クラスタ1が一番多い、ついでクラスタ2が多い。
・その他は少し
"""

"""今後

今回クラスはデフォルト8でやりましたが、そもそもそれが良かったのかもわかりません。
よって、クラスタ数を最初に推定する必要があります。
"""
Out[210]:
'今後\n\n今回クラスはデフォルト8でやりましたが、そもそもそれが良かったのかもわかりません。\nよって、クラスタ数を最初に推定する必要があります。\n'
In [211]:
# クラスタ数を推定する エルボ法

"""
クラスタを1つから増やしていく過程では、
クラスタに所属する各データとクラスタ重心との距離の総和 (distortion) は、減少していくと推測できる。
クラスタ数が最適数を超えると、距離の総和の減少スピードが小さくなると推測できる。
距離の総和の減少スピードが小さくなるときのクラスタ数を採用する

距離の総和をグラフにすると、途中から折れ曲がるのでエルボ法と呼ばれている。

"""
Out[211]:
'\nクラスタを1つから増やしていく過程では、\nクラスタに所属する各データとクラスタ重心との距離の総和は、減少していくと推測できる。\nクラスタ数が最適数を超えると、距離の総和の減少スピードが小さくなると推測できる。\n距離の総和の減少スピードが小さくなるときのクラスタ数を採用する\n\n'
In [222]:
# ブロブデータでエルボ法を試してみよう
# 距離の総和 : inertia_ に保持される

# ブロブデータ
from sklearn.datasets import make_blobs

distortion_list = []
data = make_blobs(random_state=1) 
X = data[0] # 説明変数
pd.DataFrame(X).head()

distortions = []
for n_cluster in range(1,11):

    model = KMeans(n_clusters=n_cluster,init='random',random_state=0)
    model.fit(X)

    # distortion
    distortions.append(model.inertia_)
    
print(distortions)
[4118.153777704471, 661.5698490972003, 156.28289251170003, 130.96121900774804, 116.54563084299525, 97.01791420345081, 84.961839171795, 70.52115768856302, 62.65840813882036, 57.186978620797646]
In [225]:
plt.plot(range(1,11), distortions)

"""結果
クラスター数3で急激にdistortionの減少が小さくなっているので、3がベスト
"""
Out[225]:
'結果\nクラスター数3で急激にdistortionの減少が小さくなっているので、3がベスト\n'
In [229]:
# では これを 顧客のクラスタリングに適用してみる
import requests, zipfile
import io

# データセットを用意する
url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip'

r = requests.get(url, stream=True)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall()

data = pd.read_csv('bank-full.csv', sep=';')

# 使用するデータを4つだけ使用する
data_sub = data[['age', 'balance', 'campaign', 'previous']]

# データを標準化する 
from sklearn.preprocessing import StandardScaler

# StandardScalerのインスタンスを作成
standard_scaler = StandardScaler()

# 標準化の実行
standard_scaler.fit(data_sub)
data_sub_std = standard_scaler.transform(data_sub) 
print(pd.DataFrame(data_sub_std, 
                   columns=['age', 'balance', 'campaign', 'previous']).head())
print(data_sub.info())
        age   balance  campaign  previous
0  1.606965  0.256419 -0.569351  -0.25194
1  0.288529 -0.437895 -0.569351  -0.25194
2 -0.747384 -0.446762 -0.569351  -0.25194
3  0.571051  0.047205 -0.569351  -0.25194
4 -0.747384 -0.447091 -0.569351  -0.25194
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45211 entries, 0 to 45210
Data columns (total 4 columns):
age         45211 non-null int64
balance     45211 non-null int64
campaign    45211 non-null int64
previous    45211 non-null int64
dtypes: int64(4)
memory usage: 1.4 MB
None
In [235]:
# 訓練をする
distortions =[]
for n_cluster in range(1,21):
    
    print('現在のクラスタ数 : ', n_cluster)
    
    # K-meansクラスタリングモデルのインスタンスを作成する
    model = KMeans(n_clusters=n_cluster,init='random',random_state=0)

    # 重心を計算する(訓練する)
    model.fit(data_sub_std)
    
    distortions.append(model.inertia_)

print('訓練完了')
print(distortions)

"""
クラスタ数が多くなってくると、
訓練に時間がかかるようになった。
なるべくクラスタ数は小さいほうが良いようだ。
10以上は避けるべきだと考える。
"""
現在のクラスタ数 :  1
現在のクラスタ数 :  2
現在のクラスタ数 :  3
現在のクラスタ数 :  4
現在のクラスタ数 :  5
現在のクラスタ数 :  6
現在のクラスタ数 :  7
現在のクラスタ数 :  8
現在のクラスタ数 :  9
現在のクラスタ数 :  10
現在のクラスタ数 :  11
現在のクラスタ数 :  12
現在のクラスタ数 :  13
現在のクラスタ数 :  14
現在のクラスタ数 :  15
現在のクラスタ数 :  16
現在のクラスタ数 :  17
現在のクラスタ数 :  18
現在のクラスタ数 :  19
現在のクラスタ数 :  20
訓練完了
[180844.0, 148734.3144086882, 123478.75929725463, 101680.3500051579, 84412.51805248375, 76422.15821096877, 69436.60990649737, 64176.412970801626, 57757.93355292684, 54199.875799899295, 51985.82497495736, 40980.805589710064, 37556.180402399, 35407.77835407567, 33623.42165185649, 31540.38406833519, 30241.21643712217, 28421.357773862754, 27782.097598096054, 27162.135684252564]
In [237]:
plt.plot(range(1,21), distortions, marker='o')
plt.xlabel('The mumber of the clusters')
plt.ylabel('Distortion')
plt.grid()
plt.show()
"""結果
クラスタ数は5程度が良さそう

"""
In [238]:
# ということで エルボ法よりクラスタ数は5と決定したので
# 再度 顧客セクメンテーションをやってみる
import requests, zipfile
import io

# データセットを用意する
url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip'

r = requests.get(url, stream=True)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall()

data = pd.read_csv('bank-full.csv', sep=';')

# 使用するデータを4つだけ使用する
data_sub = data[['age', 'balance', 'campaign', 'previous']]

# データを標準化する 
from sklearn.preprocessing import StandardScaler

# StandardScalerのインスタンスを作成
standard_scaler = StandardScaler()

# 標準化の実行
standard_scaler.fit(data_sub)
data_sub_std = standard_scaler.transform(data_sub) 
print(pd.DataFrame(data_sub_std, 
                   columns=['age', 'balance', 'campaign', 'previous']).head())
print(data_sub.info())

# 訓練をする
n_cluster = 5 # エルボ法より

# K-meansクラスタリングモデルのインスタンスを作成する
model = KMeans(n_clusters=n_cluster,init='random',random_state=0)

# 重心を計算する(訓練する)
model.fit(data_sub_std)

distortions.append(model.inertia_)

print('訓練完了')
        age   balance  campaign  previous
0  1.606965  0.256419 -0.569351  -0.25194
1  0.288529 -0.437895 -0.569351  -0.25194
2 -0.747384 -0.446762 -0.569351  -0.25194
3  0.571051  0.047205 -0.569351  -0.25194
4 -0.747384 -0.447091 -0.569351  -0.25194
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45211 entries, 0 to 45210
Data columns (total 4 columns):
age         45211 non-null int64
balance     45211 non-null int64
campaign    45211 non-null int64
previous    45211 non-null int64
dtypes: int64(4)
memory usage: 1.4 MB
None
訓練完了
In [239]:
# 分類結果を確認する
y_pred = model.predict(data_sub_std)
print(pd.DataFrame(y_pred).head())
   0
0  3
1  3
2  1
3  3
4  1
In [240]:
# では可視化してみましょう。
"""
さっきのような2次元グラフでは、もはや表現できません。

"""

#クラスター番号を取得する
"""
 上記の y_pred = model.predict(data_sub_std) と同じ
"""
labels = pd.Series(model.labels_, name='cluster_number')

# クラスター番号とそれのデータ数を算出する
df = pd.DataFrame(labels.value_counts(sort=False))
df
Out[240]:
cluster_number
0 1700
1 25401
2 1393
3 15410
4 1307
In [241]:
# バーチャートで可視化する
plt.bar(df.index, df['cluster_number'])

"""結果
・クラスタ1が一番多い、ついでクラスタ3が多い。
・その他は少し
"""
Out[241]:
'今後\n\n今回クラスはデフォルト8でやりましたが、そもそもそれが良かったのかもわかりません。\nよって、クラスタ数を最初に推定する必要があります。\n'
In [243]:
# クラスタリング結果の解釈
# 元データにクラスタリング結果を統合する

data_with_cluster_num = pd.concat([data, labels], axis=1)
data_with_cluster_num.head()
Out[243]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y cluster_number
0 58 management married tertiary no 2143 yes no unknown 5 may 261 1 -1 0 unknown no 3
1 44 technician single secondary no 29 yes no unknown 5 may 151 1 -1 0 unknown no 3
2 33 entrepreneur married secondary no 2 yes yes unknown 5 may 76 1 -1 0 unknown no 1
3 47 blue-collar married unknown no 1506 yes no unknown 5 may 92 1 -1 0 unknown no 3
4 33 unknown single unknown no 1 no no unknown 5 may 198 1 -1 0 unknown no 1
In [280]:
# クラスター別の年齢層を確認してみる
"""
ピン結合とピボットを使用する
"""

# 区切り
bins = [i for i in range(15,101,5)]
print(bins)

# 年齢ageを binsで qcutする
qcut_age = pd.cut(data_with_cluster_num.age, bins, right=False)
df_qcut_age = pd.DataFrame(qcut_age)
df_qcut_age.columns=['qcut_age']
df_qcut_age.head()
[15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]
Out[280]:
qcut_age
0 [55, 60)
1 [40, 45)
2 [30, 35)
3 [45, 50)
4 [30, 35)
In [281]:
# クラスタ番号と年齢層を結合
df = pd.concat([data_with_cluster_num, df_qcut_age], axis=1)
df.head()
print(df.keys())
Index(['age', 'job', 'marital', 'education', 'default', 'balance', 'housing',
       'loan', 'contact', 'day', 'month', 'duration', 'campaign', 'pdays',
       'previous', 'poutcome', 'y', 'cluster_number', 'qcut_age'],
      dtype='object')
In [285]:
# クラスタ番号と年齢層を軸に集計 → 年齢層を列に設定
cross_cluster_qcut_age = df.groupby(['cluster_number', 'qcut_age']).size().unstack().fillna(0)

print(cross_cluster_qcut_age)
qcut_age        [15, 20)  [20, 25)  [25, 30)  [30, 35)  [35, 40)  [40, 45)  \
cluster_number                                                               
0                    2.0      14.0     146.0     389.0     353.0     266.0   
1                   45.0     716.0    4109.0    8780.0    7464.0    4287.0   
2                    0.0      21.0     134.0     330.0     311.0     189.0   
3                    0.0       0.0       0.0       0.0       0.0    1232.0   
4                    0.0      11.0      75.0     241.0     221.0     211.0   

qcut_age        [45, 50)  [50, 55)  [55, 60)  [60, 65)  [65, 70)  [70, 75)  \
cluster_number                                                               
0                  223.0     160.0     117.0      24.0       1.0       1.0   
1                    0.0       0.0       0.0       0.0       0.0       0.0   
2                  147.0     118.0      71.0      38.0      10.0      11.0   
3                 4940.0    4066.0    3575.0     864.0     234.0     224.0   
4                  160.0     144.0     159.0      48.0      11.0      18.0   

qcut_age        [75, 80)  [80, 85)  [85, 90)  [90, 95)  [95, 100)  
cluster_number                                                     
0                    2.0       0.0       1.0       0.0        1.0  
1                    0.0       0.0       0.0       0.0        0.0  
2                    8.0       4.0       0.0       1.0        0.0  
3                  155.0      92.0      21.0       6.0        1.0  
4                    5.0       2.0       1.0       0.0        0.0  
In [287]:
# 各年齢層qcut_age で何人いるかカウントする
hist_age = pd.value_counts(qcut_age)
hist_agecross_cluster_qcut_age 
Out[287]:
[30, 35)     9740
[35, 40)     8349
[40, 45)     6185
[45, 50)     5470
[50, 55)     4488
[25, 30)     4464
[55, 60)     3922
[60, 65)      974
[20, 25)      762
[65, 70)      256
[70, 75)      254
[75, 80)      170
[80, 85)       98
[15, 20)       47
[85, 90)       23
[90, 95)        7
[95, 100)       2
Name: age, dtype: int64
In [288]:
# ヒートマップを描いてみる
sns.heatmap(cross_cluster_qcut_age)
Out[288]:
<matplotlib.axes._subplots.AxesSubplot at 0x26fa13c96a0>
In [294]:
# 全体に対する割合で書き直してみる
sns.heatmap(cross_cluster_qcut_age.apply(lambda x : x / x.sum(), axis=1),cmap='Blues')

"""
クラスタ1は30-35歳くらいに固まっていて、
クラスタ3は45-50歳くらいに固まっている。

→このクラスタは年齢層に偏りを持っていることがわかった
"""
Out[294]:
'\nクラスタ1は30-35歳くらいに固まっていて、\nクラスタ2は45-50歳くらいに固まっている。\n\n→このクラスタは年齢層に偏りを持っていることがわかった\n'
In [298]:
# 職業job についても確認してみる
# 数値ではなくカテゴリ変数なのでどうやったらよいか。
cross_cluster_job = data_with_cluster_num.groupby(['cluster_number', 'job']).size().unstack().fillna(0)
cross_cluster_job
Out[298]:
job admin. blue-collar entrepreneur housemaid management retired self-employed services student technician unemployed unknown
cluster_number
0 178 373 66 42 387 36 66 156 18 324 32 22
1 3174 5778 753 445 5375 57 886 2646 839 4638 730 80
2 196 244 41 21 338 53 43 114 54 251 33 5
3 1514 3155 572 695 2935 2039 511 1169 5 2186 459 170
4 109 182 55 37 423 79 73 69 22 198 49 11
In [299]:
# ヒートマップを書いてみる
sns.heatmap(cross_cluster_job)
Out[299]:
<matplotlib.axes._subplots.AxesSubplot at 0x26fa4358f98>
In [301]:
# 全体に対する割合で書き直してみる
sns.heatmap(cross_cluster_job.apply(lambda x : x / x.sum(), axis=1),cmap='Blues')

"""
クラスタ1は30-35歳くらいに固まっていて、blue-collarが多い
クラスタ3は45-50歳くらいに固まっていて、blue-collarが多い
クラスタ4にmanagementが特に多いが年齢は30-45くらいと幅が広い

→このクラスタは年齢層に偏りを持っていることがわかった
"""
Out[301]:
<matplotlib.axes._subplots.AxesSubplot at 0x26fa4638048>
In [ ]: