1. 程式人生 > >一文看懂機器學習流程(客戶流失率預測)

一文看懂機器學習流程(客戶流失率預測)

本文轉自:https://my.oschina.net/sizhe/blog/1594791

1 定義問題

客戶流失率問題是電信運營商面臨得一項重要課題,也是一個較為流行的案例。根據測算,招攬新的客戶比保留住既有客戶的花費大得多(通常5-20倍的差距)。因此,如何保留住現在的客戶對運營商而言是一項非常有意義的事情。 本文希望通過一個公開資料的客戶流失率問題分析,能夠帶著大家理解如何應用機器學習預測演算法到實際應用中。

當然, 實際的場景比本文例子複雜的多,如果想具體應用到專案, 還需要針對不同的場景和資料進行具體的分析。

從機器學習的分類來講, 這是一個監督問題中的分類問題。 具體來說, 是一個二分類問題。 所有的資料中包括一些特徵, 最後就是它的分類:流失或者在網。接下來我們就開始具體的處理。

2 分析資料

首先我們來匯入資料, 然後檢視資料的基本情況。

2.1 資料匯入

通過pandas來匯入csv, 然後我們來檢視一下資料的基本情況

from __future__ import division
import pandas as pd
import numpy as np

ds = pd.read_csv('./churn.csv')
col_names = ds.columns.tolist()
print "Column names:"
print col_names
print(ds.shape)

輸出:

Column names:
['State', 'Account Length', 'Area Code', 'Phone', "Int'l Plan", 'VMail Plan', 'VMail Message', 'Day Mins', 'Day Calls', 'Day Charge', 'Eve Mins', 'Eve Calls', 'Eve Charge', 'Night Mins', 'Night Calls', 'Night Charge', 'Intl Mins', 'Intl Calls', 'Intl Charge', 'CustServ Calls', 'Churn?']
(3333, 21)

可以看到, 整個資料集有3333條資料, 20個維度, 最後一項是分類。

2.2 基本資訊以及型別

我們可以列印一些資料, 對資料和取值有一個基本的理解。

peek = data.head(5)
print(peek)

輸出:

   State  Account Length  Area Code     Phone Int'l Plan VMail Plan  \
0     KS             128        415  382-4657         no        yes   
1     OH             107        415
371-7191 no yes 2 NJ 137 415 358-1921 no no 3 OH 84 408 375-9999 yes no 4 OK 75 415 330-6626 yes no Eve Charge Night Mins Night Calls Night Charge Intl Mins Intl Calls \ 0 16.78 244.7 91 11.01 10.0 3 1 16.62 254.4 103 11.45 13.7 3 2 10.30 162.6 104 7.32 12.2 5 3 5.26 196.9 89 8.86 6.6 7 4 12.61 186.9 121 8.41 10.1 3 Intl Charge CustServ Calls Churn? 0 2.70 1 False. 1 3.70 1 False. 2 3.29 0 False. 3 1.78 2 False. 4 2.73 3 False.

我們可以看到, 資料集有20項特徵,分別是州名, 賬戶長度, 區號, 電話號碼, 國際計劃,語音郵箱, 白天通話分鐘數, 白天電話個數, 白天收費, 晚間通話分鐘數,晚間電話個數, 晚間收費, 夜間通話分鐘數,夜間電話個數, 夜間收費, 國際分鐘數, 國際電話個數, 國際收費, 客服電話數,流失與否。

  1. 可以看到這裡面有個人資訊,應該可以看到有些資訊與流失與否關係不大。 州名, 區號可以指明客戶的位置, 和流失有關係麼, 不知道, 具體位置如果不分類, 應該完全沒有關係。 而州名, 也許某個州有了某個強勁的競爭對手? 這也是瞎猜, 暫時意義不大, 刪除。
  2. 賬號長度, 電話號碼, 不需要
  3. 國際計劃, 語音郵箱。 可能有關係, 先留著吧。
  4. 分別統計了白天, 晚間, 夜間的通話分鐘, 電話個數, 收費情況。 這是重要資訊保留
  5. 客服電話, 客戶打電話投訴多那流失率可能會大。 這個是重要資訊保留。
  6. 流失與否。 這是分類結果。

然後我們可以看一下資料的型別, 如下:

ds.info()

輸出:

RangeIndex: 3333 entries, 0 to 3332
Data columns (total 21 columns):
State             3333 non-null object
Account Length    3333 non-null int64
Area Code         3333 non-null int64
Phone             3333 non-null object
Int'l Plan        3333 non-null object
VMail Plan        3333 non-null object
VMail Message     3333 non-null int64
Day Mins          3333 non-null float64
Day Calls         3333 non-null int64
Day Charge        3333 non-null float64
Eve Mins          3333 non-null float64
Eve Calls         3333 non-null int64
Eve Charge        3333 non-null float64
Night Mins        3333 non-null float64
Night Calls       3333 non-null int64
Night Charge      3333 non-null float64
Intl Mins         3333 non-null float64
Intl Calls        3333 non-null int64
Intl Charge       3333 non-null float64
CustServ Calls    3333 non-null int64
Churn?            3333 non-null object
dtypes: float64(8), int64(8), object(5)
memory usage: 546.9+ KB

看見, 有int, float, object。 對於不是資料型的資料, 後面除非決策樹等演算法, 否則應該會轉化成資料行。 所以我們把churn? 結果轉化, 以及"Int’l Plan",“VMail Plan”, 這兩個引數只有yes, no 兩種, 所以也進行轉化成01值。

2.3 描述性統計

describe() 可以返回具體的結果, 對於每一列。

數量 平均值 標準差 25% 分位 50% 分位數 75% 分位數 最大值 很多時候你可以得到NA的數量和比例。

TODO 對於非資料性的是沒有返回的的

 Account Length    Area Code  VMail Message     Day Mins    Day Calls  \
count     3333.000000  3333.000000    3333.000000  3333.000000  3333.000000   
mean       101.064806   437.182418       8.099010   179.775098   100.435644   
std         39.822106    42.371290      13.688365    54.467389    20.069084   
min          1.000000   408.000000       0.000000     0.000000     0.000000   
25%         74.000000   408.000000       0.000000   143.700000    87.000000   
50%        101.000000   415.000000       0.000000   179.400000   101.000000   
75%        127.000000   510.000000      20.000000   216.400000   114.000000   
max        243.000000   510.000000      51.000000   350.800000   165.000000   

        Day Charge     Eve Mins    Eve Calls   Eve Charge   Night Mins  \
count  3333.000000  3333.000000  3333.000000  3333.000000  3333.000000   
mean     30.562307   200.980348   100.114311    17.083540   200.872037   
std       9.259435    50.713844    19.922625     4.310668    50.573847   
min       0.000000     0.000000     0.000000     0.000000    23.200000   
25%      24.430000   166.600000    87.000000    14.160000   167.000000   
50%      30.500000   201.400000   100.000000    17.120000   201.200000   
75%      36.790000   235.300000   114.000000    20.000000   235.300000   
max      59.640000   363.700000   170.000000    30.910000   395.000000   

       Night Calls  Night Charge    Intl Mins   Intl Calls  Intl Charge  \
count  3333.000000   3333.000000  3333.000000  3333.000000  3333.000000   
mean    100.107711      9.039325    10.237294     4.479448     2.764581   
std      19.568609      2.275873     2.791840     2.461214     0.753773   
min      33.000000      1.040000     0.000000     0.000000     0.000000   
25%      87.000000      7.520000     8.500000     3.000000     2.300000   
50%     100.000000      9.050000    10.300000     4.000000     2.780000   
75%     113.000000     10.590000    12.100000     6.000000     3.270000   
max     175.000000     17.770000    20.000000    20.000000     5.400000   

       CustServ Calls  
count     3333.000000  
mean         1.562856  
std          1.315491  
min          0.000000  
25%          1.000000  
50%          1.000000  
75%          2.000000  
max          9.000000  

2.4 圖形化理解你的資料

之前的一些資訊, 只是一些很初步的理解, 但是對於機器學習演算法來講是不夠的。 下面我們從幾個維度去進一步理解你的資料。工具可以用數字表格, 也可以用圖形(matplotlib) 這裡畫圖較多。

  1. 特徵自己的資訊
  2. 特徵和分類之間的關係
  3. 特徵和特徵之間的關係 – 這裡鑑於時間的關係, 有些關係並沒有直接應用於演算法本身, 但是在進一步的演算法提升中是很有意義的, 這裡更多的是一種展示。

2.4.1 特徵本身的資訊

我們先來看一下流失比例, 以及關於打客戶電話的個數分佈

import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.2)  # 設定圖表顏色alpha引數

plt.subplot2grid((2,3),(0,0))             # 在一張大圖裡分列幾個小圖
ds['Churn?'].value_counts().plot(kind='bar')# plots a bar graph of those who surived vs those who did not. 
plt.title(u"stat for churn") # puts a title on our graph
plt.ylabel(u"number")  

plt.subplot2grid((2,3),(0,2))            
ds['CustServ Calls'].value_counts().plot(kind='bar')# plots a bar graph of those who surived vs those who did not. 
plt.title(u"stat for cusServCalls") # puts a title on our graph
plt.ylabel(u"number")  

plt.show()

在這裡插入圖片描述
很容易理解。

然後呢, 我們的資料的特點是對白天, 晚上, 夜間,國際都有分鐘數, 電話數, 收費三種維度。 那麼我們拿白天的來舉例。

import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.2)  # 設定圖表顏色alpha引數

plt.subplot2grid((2,5),(0,0))             # 在一張大圖裡分列幾個小圖
ds['Day Mins'].plot(kind='kde')    # plots a kernel desnsity estimate of customer 
plt.xlabel(u"Mins")# plots an axis lable
plt.ylabel(u"density") 
plt.title(u"dis for day mins")

plt.subplot2grid((2,5),(0,2))            
ds['Day Calls'].plot(kind='kde')    # plots a kernel desnsity estimate of customer 
plt.xlabel(u"call")# plots an axis lable
plt.ylabel(u"density") 
plt.title(u"dis for day calls")

plt.subplot2grid((2,5),(0,4))           
ds['Day Charge'].plot(kind='kde')   # plots a kernel desnsity estimate of customer 
plt.xlabel(u"Charge")# plots an axis lable
plt.ylabel(u"density") 
plt.title(u"dis for day charge")

plt.show()

在這裡插入圖片描述
可以看到分佈基本上都是高斯分佈, 這也符合我們的預期, 而高斯分佈對於我們後續的一些演算法處理是個好訊息。

2.4.2 特徵和分類的關聯

我們來看一下一些特徵和分類之間的關聯。 比如下面int plan

import matplotlib.pyplot as plt
fig = plt.figure()
fig.set(alpha=0.2)  # 設定圖表顏色alpha引數

int_yes = ds['Churn?'][ds['Int\'l Plan'] == 'yes'].value_counts()
int_no = ds['Churn?'][ds['Int\'l Plan'] == 'no'].value_counts()
df_int=pd.DataFrame({u'int plan':int_yes, u'no int plan':int_no})
df_int.plot(kind='bar', stacked=True)
plt.title(u"statistic between int plan and churn")
plt.xlabel(u"int or not") 
plt.ylabel(u"number")

plt.show()

在這裡插入圖片描述
我們可以看到, 有國際電話的流失率較高。 猜測也許他們有更多的選擇, 或者對服務有更多的要求。 需要特別對待。 也許你需要電話多收集一下意見了。

再來看一下

#檢視客戶服務電話和結果的關聯
fig = plt.figure()
fig.set(alpha=0.2)  # 設定圖表顏色alpha引數

cus_0 = ds['CustServ Calls'][ds['Churn?'] == 'False.'].value_counts()
cus_1 = ds['CustServ Calls'][ds['Churn?'] == 'True.'].value_counts()
df=pd.DataFrame({u'churn':cus_1, u'retain':cus_0})
df.plot(kind='bar', stacked=True)
plt.title(u"Static between customer service call and churn")
plt.xlabel(u"Call service") 
plt.ylabel(u"Num") 

plt.show()

在這裡插入圖片描述
基本上可以看出, 打客戶電話的多少和最終的分類是強相關的, 打電話3次以上的流失率比例急速升高。 這是一個非常關鍵的指標。
基本上可以看出, 打客戶電話的多少和最終的分類是強相關的, 打電話3次以上的流失率比例急速升高。 這是一個非常關鍵的指標。

3 準備資料

好的, 我們已經看了很多,對資料有了一定的理解。 下面我們開始具體對資料進行操作。

3.1 去除無關列

首先, 根據對問題的分析, 我們做第一件事情, 去除三列無關列。 州名, 電話, 區號。

我們和下一步一起做

3.2 轉化成數值型別

對於有些特徵, 本身不是數值型別的, 這些資料是不能被演算法直接使用的, 所以我們來處理一下

# Isolate target data
ds_result = ds['Churn?']
Y = np.where(ds_result == 'True.',1,0)

dummies_int = pd.get_dummies(ds['Int\'l Plan'], prefix='_int\'l Plan')
dummies_voice = pd.get_dummies(ds['VMail Plan'], prefix='VMail')

ds_tmp=pd.concat([ds, dummies_int, dummies_voice], axis=1)

# We don't need these columns
to_drop = ['State','Area Code','Phone','Churn?', 'Int\'l Plan', 'VMail Plan']
df = ds_tmp.drop(to_drop,axis=1)

print "after convert "
print df.head(5)

輸出:

after convert 01
   Account Length  VMail Message  Day Mins  Day Calls  Day Charge  Eve Mins  \
0             128             25     265.1        110       45.07     197.4   
1             107             26     161.6        123       27.47     195.5   
2             137              0     243.4        114       41.38     121.2   
3              84              0     299.4         71       50.90      61.9   
4              75              0     166.7        113       28.34     148.3   

   Eve Calls  Eve Charge  Night Mins  Night Calls  Night Charge  Intl Mins  \
0         99       16.78       244.7           91         11.01       10.0   
1        103       16.62       254.4          103         11.45       13.7   
2        110       10.30       162.6          104          7.32       12.2   
3         88        5.26       196.9           89          8.86        6.6   
4        122       12.61       186.9          121          8.41       10.1   

   Intl Calls  Intl Charge  CustServ Calls  _int'l Plan_no  _int'l Plan_yes  \
0           3         2.70               1               1                0   
1           3         3.70               1               1                0   
2           5         3.29               0               1                0   
3           7         1.78               2               0                1   
4           3         2.73               3               0                1   

   VMail_no  VMail_yes  
0         0          1  
1         0          1  
2         1          0  
3         1          0  
4         1          0 

我們可以看到結果, 所有的資料都是數值型的, 而且除去了對我們沒有意義的列。

3.3 scale 資料範圍

我們需要做一些scale的工作。 就是有些屬性的scale 太大了。

  1. 對於邏輯迴歸和梯度下降來說, 個屬性的scale 差距太大, 會對收斂速度有很大的影響。
  2. 我們這裡對所有的都做, 其實可以對一些突出的特徵做這種處理。
#scale
X = df.as_matrix().astype(np.float)

# This is important
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)

print "Feature space holds %d observations and %d features" % X.shape
print "Unique target labels:", np.unique(y)

輸出:

Feature space holds 3333 observations and 19 features
Unique target labels: [0 1]

其他的呢, 還可以考慮降維等各種方式。 但是再實際使用中, 我們往往首先做出一個模型, 得到一個參考結果, 然後逐步優化。 所以我們準備資料就到這裡。

4 評估演算法

我們會使用多個演算法來計算結果, 然後選擇較好的。 如下

# prepare models
models = []
models.append(('LR', LogisticRegression()))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))
models.append(('NB', GaussianNB()))
models.append(('SVM', SVC()))
# evaluate each model in turn
results = []
names = []
scoring = 'accuracy'
for name, model in models:
    kfold = KFold(n_splits=10, random_state=7)
    cv_results = cross_val_score(model, X, Y, cv=kfold, scoring=scoring)
    results.append(cv_results)
    names.append(name)
    msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
    print(msg)
# boxplot algorithm comparison
fig = pyplot.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(names)
pyplot.show()
LR: 0.860769 (0.021660)
LDA: 0.852972 (0.021163)
KNN: 0.896184 (0.016646)
CART: 0.920491 (0.012471)
NB: 0.857179 (0.015487)
SVM: 0.921091 (0.016828)

在這裡插入圖片描述
可以看到什麼呢, 看到SVM 和 CART 效果相對較好。

5 提升結果

提升的部分, 如何使用提升演算法。 比如隨機森林: xgboost

from sklearn.ensemble import RandomForestClassifier
num_trees = 100
max_features = 3
kfold = KFold(n_splits=10, random_state=7)
model = RandomForestClassifier(n_estimators=num_trees, max_features=max_features)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
# 0.954696013379
from sklearn.ensemble import GradientBoostingClassifier
seed = 7
num_trees = 100
kfold = KFold(n_splits=10, random_state=seed)
model = GradientBoostingClassifier(n_estimators=num_trees, random_state=seed)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
# 0.953197209185

可以看到, 這兩種演算法對單個演算法的提升還是很明顯的。 進一步的, 也可以繼續調整tree的數目, 但是效果應該差不多了。

6 展示結果

這裡展示瞭如何儲存這個演算法, 以及如何取出然後應用。

#store
from sklearn.model_selection import train_test_split
from pickle import dump
from pickle import load

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=7)
from sklearn.ensemble import GradientBoostingClassifier
seed = 7
num_trees