
一、機(jī)器學(xué)習(xí)預(yù)測(cè)客戶流失
Action,直接進(jìn)入主題,嘗試使用機(jī)器學(xué)習(xí),預(yù)測(cè)客戶流失。那么先把問(wèn)題定義好。
什么是客戶流失:就是客戶T月在網(wǎng)使用,但是T+2月離網(wǎng)了,使用到的數(shù)據(jù)都是T月或者之前的客戶數(shù)據(jù)。
整個(gè)項(xiàng)目主要分為:
- 數(shù)據(jù)收集
- 特征工程
- 模型訓(xùn)練
- 模型使用
二、數(shù)據(jù)收集與預(yù)處理
1、讀取收集數(shù)據(jù)
對(duì)客戶數(shù)據(jù)進(jìn)行收集,根據(jù)業(yè)務(wù)理解,提取盡量多的字段,數(shù)據(jù)保存在CSV文件,需要注意,要把表頭變成英文字符,不能有中文字符,內(nèi)容可以有中文,然后讀取并預(yù)處理,來(lái)段代碼:
# import各種庫(kù)
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
#數(shù)據(jù)保存在number.csv文件中,讀取時(shí)注意,如果文件中有中文的內(nèi)容,需要加上encoding="gbk"
data = pd.read_csv("number.csv",encoding="gbk")
data.shape # (668946, 55),共有66萬(wàn)行,55個(gè)字段
2、預(yù)處理
1. 瀏覽數(shù)據(jù)
data.head(10)

2. 重點(diǎn)看number特征
data['number2'] = (data['number'].apply(str).str[:3]).apply(int) #先轉(zhuǎn)化成string類型,再提取前三位,再轉(zhuǎn)化成int
print("缺失值::",data['number2'].isnull().sum()) #缺失值統(tǒng)計(jì)
print("數(shù)據(jù)shape:",data.shape) #看數(shù)據(jù)量
print(data['number2'].value_counts()) #看統(tǒng)計(jì)

可以看出number列,沒(méi)有缺失值,但前三位有很多異常情況(比如208開頭的number),要洗掉。
# 號(hào)段信息,來(lái)源百度,未必十分準(zhǔn)確。
# 電信號(hào)段:133/153/180/181/189/177;
# 聯(lián)通號(hào)段:130/131/132/155/156/185/186/145/176;
# 移動(dòng)號(hào)段:134/135/136/137/138/139/150/151/152/157/158/159/182/183/184/187/188/147/178。
# drop掉前三位不對(duì)的信息
data2=copy.copy(data[(data.number2>130)&(data.number2<200)])
print("原數(shù)據(jù)行列結(jié)構(gòu):",data.shape)
print("新數(shù)據(jù)行列結(jié)構(gòu):",data2.shape)
補(bǔ)充說(shuō)明:這里刪掉了24萬(wàn)的數(shù)據(jù),都是不正常的號(hào)段,比如2開頭,3開頭等一些不是電話號(hào)碼的信息,可能是一些其他的賬戶信息,比如寬帶、行業(yè)卡等等。

洗掉后,原有行數(shù)由668946減少到423218了。
3. 定義標(biāo)簽
什么是客戶流失:就是客戶T月在網(wǎng)使用,但是T+2月離網(wǎng)了,使用到的數(shù)據(jù)都是T月或者之前的客戶數(shù)據(jù)。 從定義出發(fā):
1)T月在網(wǎng)使用客戶,只保留T月在使用的客戶。
data2 = copy.copy(data[(data.yonghuzhuangtai_201711=="正使用")|
(data.yonghuzhuangtai_201711=="停機(jī)")|
(data.yonghuzhuangtai_201711=="已轉(zhuǎn)換品牌")])
data2.shape
2)標(biāo)簽就是標(biāo)記T+2月離網(wǎng)的客戶
# 標(biāo)記為1的是離網(wǎng)客戶,0為在網(wǎng)客戶。
data2["yonghuzhuangtai"] = data2['yonghuzhuangtai_201801'].map({
'正使用':0,'停機(jī)':0,'已轉(zhuǎn)換品牌':0,
'欠費(fèi)銷戶':1,'正式銷戶':1,'預(yù)約銷戶':1,'null':1})
data2.shape

最后行數(shù)減少到417039行。
小結(jié):
經(jīng)過(guò)對(duì)number的清洗和標(biāo)簽的定義,行數(shù)由原來(lái)的668945行減少到417039行。預(yù)處理是一種比較粗的處理,特征工程則會(huì)更加的細(xì)致。
三、特征工程
經(jīng)過(guò)預(yù)處理后,數(shù)據(jù)基本可用,就進(jìn)入最重要的兩大部分,第一是特征工程,第二部分是模型訓(xùn)練,網(wǎng)上流傳:“有這么一句話在業(yè)界廣泛流傳:數(shù)據(jù)和特征決定了機(jī)器學(xué)習(xí)的上限,而模型和算法只是逼近這個(gè)上限而已。那特征工程到底是什么呢?顧名思義,其本質(zhì)是一項(xiàng)工程活動(dòng),目的是最大限度地從原始數(shù)據(jù)中提取特征以供算法和模型使用。”個(gè)人理解,落到項(xiàng)目上,就是數(shù)據(jù)有各種亂七八糟的情況,比如字符(中文或英文)、數(shù)值缺失等等,一般不能直接被模型使用,而特征工程就是對(duì)數(shù)據(jù)進(jìn)行處理,令其可以被模型讀取使用,而更好的特征工程,你可以根據(jù)原有特征生成新特征或者篩選重要特征等等,從而提升模型的效果。本項(xiàng)目現(xiàn)階段特征工程的腦圖如下:

1.object特征處理
數(shù)據(jù)類型(dtypes)為object的特征,就是字符(中文或英文)特征
#提取object及其對(duì)應(yīng)的數(shù)據(jù)
object_columns_df = data.select_dtypes(include=["object"])
#object羅列出來(lái)了。
print(object_columns_df.iloc[1])
print(object_columns_df.columns)
#深入了解object的內(nèi)容是什么
cols = object_columns_df.columns
for object_feature in cols:
print('-------------------------------object_feature:',object_feature,'-------------------------------')
print(data[object_feature].value_counts()) #value_counts()各種取值的統(tǒng)計(jì)量
圖中是其中一個(gè)特征的各種取值的統(tǒng)計(jì)量,可以看出,里面有三類中文取值,分別是“正使用”、“停機(jī)”、“已轉(zhuǎn)換品牌”。

這種特征叫做定性特征(相對(duì)定量特征),現(xiàn)在學(xué)到的機(jī)器學(xué)習(xí)算法是不能直接對(duì)定性特征進(jìn)行計(jì)算建模的,那就需要用get_dummies對(duì)定性特征啞編碼,這里要插入簡(jiǎn)單介紹一下啞編碼,就是每類取值變成單獨(dú)一個(gè)特征,然后用0、1標(biāo)識(shí)對(duì)應(yīng)行是否具備該特征,如下圖:

啞編碼
# 1、將中文換成英文字符
data['yonghuzhuangtai_201711']=data['yonghuzhuangtai_201711'].map({'停機(jī)':'stop_using','已轉(zhuǎn)換品牌':'change_brand','正使用':'using',})
# 2、將缺失值用null補(bǔ)上
data['yonghuzhuangtai_201711'].fillna(value = 'null', inplace = True)
# 3、get_dummies,那data_yonghuzhuangtai_201711就有啞編碼后的三列,分別是
# 'yonghuzhuangtai_201711_change_brand', 'yonghuzhuangtai_201711_stop_using', 'yonghuzhuangtai_201711_using'
data_yonghuzhuangtai_201711 = pd.get_dummies(data['yonghuzhuangtai_201711'], prefix= 'yonghuzhuangtai_201711')
# 4、跟原來(lái)的數(shù)據(jù)合并concat
data_concat = pd.concat([data, data_yonghuzhuangtai_201711], axis=1)
# 5、刪除被啞編碼的特征
data_concat = data_concat.drop(['yonghuzhuangtai_201711'],axis=1)
data = copy.copy(data_concat)
data.shape #(417039, 71)
啞編碼后,數(shù)據(jù)的行數(shù)保持417039行,但是字段由57變成71列(1個(gè)標(biāo)簽,70個(gè)特征),最主要的變化是,每個(gè)特征里面都是int或者float的數(shù)字了,可以被模型讀取。
2.缺失值處理
首先,數(shù)據(jù)經(jīng)常會(huì)出現(xiàn)缺失值,這里采用最簡(jiǎn)單的處理原則,對(duì)于缺失值過(guò)多的,比如缺失90%的特征,直接刪除,幸好,基本最多缺失占比只有15%,采用最簡(jiǎn)單的處理方式,填0。
data.fillna(value = 0, axis = 1, inplace = True)
3.特征選擇
大白話點(diǎn)說(shuō),就是從原有的一堆特征中,選擇出那些離散的、相關(guān)的特征,從而簡(jiǎn)化模型和提高效率。明明25個(gè)特征可以搞定的,為什么一定要用70個(gè)呢。
- 看看有多少特征
data = data.drop(['number'],axis=1) #number是各個(gè)客戶的ID,不屬于特征,刪掉。
print(data.shape) #(417039, 70),有41萬(wàn)條記錄,70個(gè)字段(69個(gè)特征,1個(gè)標(biāo)簽)
- 離散程度,使用到方差(就是每個(gè)數(shù)據(jù)點(diǎn)距離平均值的差的平方和),如果方差是0,也就是整個(gè)特征都是一個(gè)取值,要來(lái)也沒(méi)意義,刪掉。另外,如果小于一定的值,也可以不要,但是這里需要注意,特征還沒(méi)經(jīng)過(guò)無(wú)量綱化,特征之間的方差大小不能對(duì)比的。
#計(jì)算顯示特征的方差
print(data.var())
#使用VarianceThreshold類進(jìn)行方差過(guò)濾
from sklearn.feature_selection import VarianceThreshold
#要生成這個(gè)類的對(duì)象,就需要一個(gè)參數(shù),就是最小方差的閾值,我們先設(shè)置為0.001,然后調(diào)用它的transform方法進(jìn)行特征值的過(guò)濾
variancethreshold=VarianceThreshold(threshold = 0.001)
variancethreshold.fit_transform(data)
#使用get_support方法,可以得到選擇特征列的序號(hào),然后根據(jù)這個(gè)序號(hào)在原始數(shù)據(jù)中把對(duì)應(yīng)的列名選擇出來(lái)即可
variancethreshold.get_support()
print('----------------------------------------')
print('原數(shù)據(jù)的shape:',data.shape)
print('滿足方差大于threshold的特征個(gè)數(shù):',data.columns[variancethreshold.get_support()].shape)
print('不滿足方差大于threshold的特征:',data.columns[variancethreshold.get_support()==False])
#再次強(qiáng)調(diào),這里只是好奇看看哪些方差這么小,并沒(méi)有刪除方差比0的特征
原有70個(gè)字段(其中一個(gè)是標(biāo)簽?。。?,方差小于0.001的有三個(gè),分別是:'shifoujiatingkuandaiyonghu', 'ruwangqudaoleixing_kefuqudao','ruwangqudaoleixing_null'。

- 基于樹模型的特征選擇法
直接使用隨機(jī)森林,對(duì)數(shù)據(jù)進(jìn)行建模,提取模型中對(duì)每個(gè)特征的importance系數(shù)。
from sklearn.ensemble import RandomForestClassifier
#訓(xùn)練模型,不管任何參數(shù),都用默認(rèn)的
random_forest = RandomForestClassifier(oob_score=True, random_state=10)
random_forest.fit(X_train,y_train.values.ravel())
#提取模型結(jié)果對(duì)各個(gè)特征的importance----feature_importances_
feature_importances_df_1 = pd.DataFrame(random_forest.feature_importances_,columns=["importances"],index=X_train.columns)
print(feature_importances_df_1.sort_values(axis = 0,ascending = False, by = 'importances'))
#提取importance大于平均值的特征
mean_importances = feature_importances_df_1.mean().importances
feature_importances_df_max_1 = copy.copy(feature_importances_df_1[feature_importances_df_1['importances']>mean_importances])
predictors = feature_importances_df_max_1.index.tolist()
predictors.append('yonghuzhuangtai') #加上標(biāo)簽字段
importance系數(shù)較大的特征截圖

提煉了69個(gè)特征中的30個(gè),其他特征刪掉。
- 基于Logistics Regression看特征正負(fù)作用
直接使用logistics regression模型,對(duì)數(shù)據(jù)進(jìn)行訓(xùn)練,提取每個(gè)特征對(duì)應(yīng)的系數(shù),從正負(fù)號(hào)看出每個(gè)特征對(duì)最
終標(biāo)簽的正負(fù)影響。
from sklearn.linear_model import LogisticRegression
#1、訓(xùn)練模型
lr = LogisticRegression(penalty = 'l1') #直接默認(rèn)參數(shù)
lr.fit(X_train,y_train.values.ravel()) #用原始數(shù)據(jù)訓(xùn)練
#2、提取系數(shù)
lr_coef_df = pd.DataFrame(lr.coef_.T,columns=["lr_coef_df"],index=X_train.columns)
#3、跟randomforest的結(jié)果合并同一個(gè)dataframe
df = pd.merge(feature_importances_df_max_2, lr_coef_df, left_index=True, right_index=True, how='left')
print(df)
yonghuzhuangtai_201711_using,是負(fù)數(shù),那么就是對(duì)離網(wǎng)是負(fù)影響,也就是有這個(gè)1標(biāo)識(shí),越不會(huì)離網(wǎng)
yonghuzhuangtai_201711_stop_using,是正數(shù),那么就是對(duì)離網(wǎng)是正影響,有這個(gè)1標(biāo)識(shí),就越會(huì)離網(wǎng)
基本跟認(rèn)識(shí)一致。

6.后續(xù)提升
異常值的詳細(xì)查看、無(wú)量綱化、降維、正態(tài)分布/冪律分布分析等并未深入了解,待分析。
補(bǔ)充說(shuō)明:之前嘗試過(guò)對(duì)數(shù)據(jù)進(jìn)行無(wú)量綱化,但是卡在兩個(gè)問(wèn)題上:
第一:異常值,如果對(duì)數(shù)據(jù)進(jìn)行歸一化的無(wú)量綱處理,前提要把異常值踢掉,比如apru值,有500多的少數(shù)用戶,歸一化就會(huì)把正常用戶壓縮在一個(gè)小范圍內(nèi),其實(shí)不歸一化也會(huì),還是回歸到怎么處理異常,怎么定義異常上了。
第二:分布規(guī)律,基本接觸到的機(jī)器學(xué)習(xí)處理,都默認(rèn)數(shù)據(jù)是正態(tài)分布的,但是從arpu值、mou值、活躍日數(shù)等數(shù)據(jù)的分布來(lái)看,都不是正態(tài),而是冪律分布,那么用到的算法要適當(dāng)調(diào)整嗎?

四、模型訓(xùn)練
經(jīng)常說(shuō)80%的時(shí)間在特征學(xué)習(xí),20%的時(shí)間在模型訓(xùn)練,還真覺(jué)得對(duì)啊。另外,強(qiáng)大的sklearn都把模型封裝好了,直接用就是?,F(xiàn)在的思路是各種ensemble模型輪流來(lái)一次看看效果,選幾個(gè)還不錯(cuò)的繼續(xù)調(diào)參優(yōu)化。
1. 各種ensemble模型,默認(rèn)參數(shù)跑

(Logistics Regression不是ensemble model)
以隨機(jī)森林random forest為例跑一次,其他模型大同小異
# 數(shù)據(jù)準(zhǔn)備
#1、將特征X和標(biāo)簽Y拆分開
X = data.loc[:, data.columns != 'yonghuzhuangtai']
y = data.loc[:, data.columns == 'yonghuzhuangtai']
#2、然后隨機(jī)提取80%的數(shù)據(jù)作為模型訓(xùn)練用,20%數(shù)據(jù)留作模型性能測(cè)試用
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size = 0.2, random_state = 0)
from sklearn.ensemble import RandomForestClassifier
#1、生成模型對(duì)象,參數(shù)都是默認(rèn)
random_forest = RandomForestClassifier()
#2、用那80%的數(shù)據(jù)進(jìn)行模型訓(xùn)練,其實(shí)就一句話
random_forest.fit(X_train,y_train.values.ravel())
#3、用訓(xùn)練的模型對(duì)剩下的20%數(shù)據(jù)進(jìn)行預(yù)測(cè),將預(yù)測(cè)結(jié)果和已知標(biāo)簽對(duì)比,得出模型性能
y_pred = random_forest.predict(X_test.values)
#4、畫出性能圖
plt.figure()
plot_confusion_matrix(y_test,y_pred,title='Confusion matrix')
image.png

判斷標(biāo)準(zhǔn)
結(jié)果出來(lái),是一個(gè)混淆矩陣,橫坐標(biāo)是預(yù)測(cè),縱坐標(biāo)是實(shí)際發(fā)生,性能主要看三個(gè)指標(biāo):
-
recall 召回率,真實(shí)流失客戶中,多少被預(yù)判為流失。
image.png -
precision 準(zhǔn)確率,預(yù)測(cè)為流失的客戶中,真的會(huì)走的的有多少。
image.png
-
f1,綜合考慮recall 和 precision的指標(biāo)。
image.png
再解釋通俗講一遍就是,模型預(yù)測(cè)的1000個(gè)客戶,有800個(gè)確實(shí)最后離網(wǎng)了,80%的precision,但是也有另外800個(gè)真實(shí)離網(wǎng)的沒(méi)被預(yù)測(cè)到,50%的recall。預(yù)測(cè)的都蠻準(zhǔn),但是預(yù)測(cè)不全。
各個(gè)模型的效果對(duì)比
默認(rèn)參數(shù)下,GradientBoostingClassifier、RandomForest、XGBClassifier明顯效果好一點(diǎn)。
# GradientBoostingClassifier LogisticRegression RandomForest \
# 0.1 0.579078 0.414419 0.506922
# 0.2 0.647005 0.498207 0.602151
# 0.25 0.653581 0.525630 0.602367
# 0.3 0.657226 0.550074 0.629484
# 0.35 0.654224 0.524365 0.629584
# 0.4 0.649972 0.499580 0.631519
# 0.45 0.646009 0.485224 0.631757
# 0.5 0.641439 0.469859 0.615846
# 0.6 0.616350 0.418735 0.587390
# 0.65 0.593640 0.361297 0.587639
# 0.7 0.566000 0.276610 0.552564
# 0.75 0.536923 0.190199 0.552240
# 0.8 0.501413 0.114034 0.466256
# 0.85 0.442625 0.064789 0.466127
# 0.9 0.341222 0.034093 0.322901
# time 66.251000 4.351000 15.549000
# AdaBoostClassifie ExtraTreesClassifier XGBClassifier MLPClassifier
# 0.1 0.125537 0.501672 0.570283 0.008401
# 0.2 0.125537 0.597911 0.643224 0.008401
# 0.25 0.125537 0.597909 0.650471 0.008401
# 0.3 0.125537 0.617750 0.651945 0.008401
# 0.35 0.125537 0.618192 0.647942 0.008401
# 0.4 0.125537 0.613068 0.644493 0.008401
# 0.45 0.125770 0.612934 0.641509 0.008401
# 0.5 0.623428 0.591254 0.635485 0.008401
# 0.6 0.000000 0.563729 0.610550 0.008401
# 0.65 0.000000 0.563729 0.585824 0.008401
# 0.7 0.000000 0.520726 0.561148 0.008401
# 0.75 0.000000 0.520726 0.536541 0.008401
# 0.8 0.000000 0.450475 0.490799 0.008401
# 0.85 0.000000 0.450450 0.426007 0.008401
# 0.9 0.000000 0.320902 0.332662 0.008401
# time 33.884000 5.247000 38.831000 28.706000
2、模型調(diào)參
調(diào)參是什么,就是一個(gè)模型有很多參數(shù),比如random forest 隨機(jī)森林,有樹的數(shù)量,樹的深度等等,需要設(shè)置的,那么設(shè)置多少最好呢,都不知道,那就暴力點(diǎn),每種都試試,這就是調(diào)參咯,試出最好的那個(gè)參數(shù)組,random forest主要要調(diào)節(jié)的參數(shù)有:
- n_estimators
- max_depth
- min_samples_split
- min_samples_leaf
- max_features
對(duì)以上5個(gè)參數(shù)都要進(jìn)行調(diào)參,逐一GridSearchCV,只拿n_estimators調(diào)參舉例,具體如下,試一下50,70,90,100,120,150中各種取值的效果,提取效果最好的那個(gè)參數(shù):
randomforest_param_grid = {'n_estimators':list((50,70,90,100,120,150))}
grid = GridSearchCV(RandomForestClassifier(min_samples_split=100,min_samples_leaf=20,max_depth=8,\
max_features='sqrt',oob_score=True,random_state=10),\
param_grid=randomforest_param_grid, cv=5 ,scoring = 'f1')#scoring = 'roc_auc' or 'f1'
# 這里多說(shuō)兩句:
# param_grid:需要交叉驗(yàn)證的參數(shù),這里有6個(gè)數(shù)據(jù),分別是50,70,90,100,120,150,究竟哪個(gè)參數(shù)出來(lái)的效果最好呢?
# cv=5,就是5折交叉試驗(yàn),把數(shù)據(jù)分為5分,每次提取其中4份跑模型,1份留作驗(yàn)證,5次的平均表現(xiàn)是評(píng)定標(biāo)準(zhǔn)。
# 每個(gè)參數(shù)都跑5次,取平均值再對(duì)比才有說(shuō)服力,那么就總共跑了5*6次,30次了。
# scoring = 'f1',那么效果最好的這個(gè)效果是什么標(biāo)準(zhǔn)呢?可以是‘a(chǎn)ccuracy’,‘roc_auc’,這里選擇'f1'
grid.fit(X_train,y_train.values.ravel())
# 記錄最好的參數(shù)
best_n_estimators = grid.best_estimator_.n_estimators
print(best_n_estimators)
調(diào)參明細(xì):
- n_estimators: list((50,70,90,100,120,150))
- max_depth: list((3,7,11,15))
- min_samples_split: list((50,75,100,135,150))
- min_samples_leaf: list((20,40,60,80))
- max_features: list((5,6,7,8,9,11,15,20,25))
這里是最花時(shí)間的,每個(gè)參數(shù)經(jīng)常要1個(gè)小時(shí)才能出來(lái)最優(yōu)那個(gè),5個(gè)參數(shù)基本就是一個(gè)晚上,才能出來(lái)一組最優(yōu)參數(shù)組,最后f1一般能提升1%-3.5%。本項(xiàng)目random forest模型調(diào)參后f1達(dá)到0.620960,提升0.919%,GradientBoostingClassifier調(diào)參后提升到0.629094,提升了2.514%,比random forest調(diào)參后也高0.8134%。

五、模型使用
可以使用歷史數(shù)據(jù)訓(xùn)練出來(lái)的模型對(duì)當(dāng)前數(shù)據(jù)進(jìn)行預(yù)測(cè),具體的預(yù)測(cè)效果如何呢?試過(guò)一個(gè)假設(shè),T月的流失客戶訓(xùn)練出來(lái)的模型,f1是60%,然后用到T+1的全量數(shù)據(jù)中進(jìn)行預(yù)測(cè),f1是57%,低了3%,數(shù)據(jù)還算是有延續(xù)性,可以預(yù)測(cè)。
六、使用環(huán)境介紹
- 語(yǔ)言:python
- IDE:anaconda
- 寫作:簡(jiǎn)書
小結(jié):由于本人水平有限,本文內(nèi)容估計(jì)有很多錯(cuò)漏的地方,希望各位路過(guò)有緣人指出,學(xué)習(xí)學(xué)習(xí)。


