一、机器学习预测客户流失
Action,直接进入主题,尝试使用机器学习,预测客户流失。那么先把问题定义好。
什么是客户流失:就是客户T月在网使用,但是T+2月离网了,使用到的数据都是T月或者之前的客户数据。
整个项目主要分为:
- 数据收集
- 特征工程
- 模型训练
- 模型使用
二、数据收集与预处理
1、读取收集数据
对客户数据进行收集,根据业务理解,提取尽量多的字段,数据保存在CSV文件,需要注意,要把表头变成英文字符,不能有中文字符,内容可以有中文,然后读取并预处理,来段代码:
# import各种库
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
#数据保存在number.csv文件中,读取时注意,如果文件中有中文的内容,需要加上encoding="gbk"
data = pd.read_csv("number.csv",encoding="gbk")
data.shape # (668946, 55),共有66万行,55个字段
2、预处理
1. 浏览数据
data.head(10)
2. 重点看number特征
data['number2'] = (data['number'].apply(str).str[:3]).apply(int) #先转化成string类型,再提取前三位,再转化成int
print("缺失值::",data['number2'].isnull().sum()) #缺失值统计
print("数据shape:",data.shape) #看数据量
print(data['number2'].value_counts()) #看统计
可以看出number列,没有缺失值,但前三位有很多异常情况(比如208开头的number),要洗掉。
# 号段信息,来源百度,未必十分准确。
# 电信号段:133/153/180/181/189/177;
# 联通号段:130/131/132/155/156/185/186/145/176;
# 移动号段:134/135/136/137/138/139/150/151/152/157/158/159/182/183/184/187/188/147/178。
# drop掉前三位不对的信息
data2=copy.copy(data[(data.number2>130)&(data.number2<200)])
print("原数据行列结构:",data.shape)
print("新数据行列结构:",data2.shape)
补充说明:这里删掉了24万的数据,都是不正常的号段,比如2开头,3开头等一些不是电话号码的信息,可能是一些其他的账户信息,比如宽带、行业卡等等。
洗掉后,原有行数由668946减少到423218了。
3. 定义标签
什么是客户流失:就是客户T月在网使用,但是T+2月离网了,使用到的数据都是T月或者之前的客户数据。 从定义出发:
1)T月在网使用客户,只保留T月在使用的客户。
data2 = copy.copy(data[(data.yonghuzhuangtai_201711=="正使用")|
(data.yonghuzhuangtai_201711=="停机")|
(data.yonghuzhuangtai_201711=="已转换品牌")])
data2.shape
2)标签就是标记T+2月离网的客户
# 标记为1的是离网客户,0为在网客户。
data2["yonghuzhuangtai"] = data2['yonghuzhuangtai_201801'].map({
'正使用':0,'停机':0,'已转换品牌':0,
'欠费销户':1,'正式销户':1,'预约销户':1,'null':1})
data2.shape
最后行数减少到417039行。
小结:
经过对number的清洗和标签的定义,行数由原来的668945行减少到417039行。预处理是一种比较粗的处理,特征工程则会更加的细致。
三、特征工程
经过预处理后,数据基本可用,就进入最重要的两大部分,第一是特征工程,第二部分是模型训练,网上流传:“有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。那特征工程到底是什么呢?顾名思义,其本质是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。”个人理解,落到项目上,就是数据有各种乱七八糟的情况,比如字符(中文或英文)、数值缺失等等,一般不能直接被模型使用,而特征工程就是对数据进行处理,令其可以被模型读取使用,而更好的特征工程,你可以根据原有特征生成新特征或者筛选重要特征等等,从而提升模型的效果。本项目现阶段特征工程的脑图如下:
1.object特征处理
数据类型(dtypes)为object的特征,就是字符(中文或英文)特征
#提取object及其对应的数据
object_columns_df = data.select_dtypes(include=["object"])
#object罗列出来了。
print(object_columns_df.iloc[1])
print(object_columns_df.columns)
#深入了解object的内容是什么
cols = object_columns_df.columns
for object_feature in cols:
print('-------------------------------object_feature:',object_feature,'-------------------------------')
print(data[object_feature].value_counts()) #value_counts()各种取值的统计量
图中是其中一个特征的各种取值的统计量,可以看出,里面有三类中文取值,分别是“正使用”、“停机”、“已转换品牌”。
这种特征叫做定性特征(相对定量特征),现在学到的机器学习算法是不能直接对定性特征进行计算建模的,那就需要用get_dummies对定性特征哑编码,这里要插入简单介绍一下哑编码,就是每类取值变成单独一个特征,然后用0、1标识对应行是否具备该特征,如下图:
哑编码
# 1、将中文换成英文字符
data['yonghuzhuangtai_201711']=data['yonghuzhuangtai_201711'].map({'停机':'stop_using','已转换品牌':'change_brand','正使用':'using',})
# 2、将缺失值用null补上
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、跟原来的数据合并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)
哑编码后,数据的行数保持417039行,但是字段由57变成71列(1个标签,70个特征),最主要的变化是,每个特征里面都是int或者float的数字了,可以被模型读取。
2.缺失值处理
首先,数据经常会出现缺失值,这里采用最简单的处理原则,对于缺失值过多的,比如缺失90%的特征,直接删除,幸好,基本最多缺失占比只有15%,采用最简单的处理方式,填0。
data.fillna(value = 0, axis = 1, inplace = True)
3.特征选择
大白话点说,就是从原有的一堆特征中,选择出那些离散的、相关的特征,从而简化模型和提高效率。明明25个特征可以搞定的,为什么一定要用70个呢。
- 看看有多少特征
data = data.drop(['number'],axis=1) #number是各个客户的ID,不属于特征,删掉。
print(data.shape) #(417039, 70),有41万条记录,70个字段(69个特征,1个标签)
- 离散程度,使用到方差(就是每个数据点距离平均值的差的平方和),如果方差是0,也就是整个特征都是一个取值,要来也没意义,删掉。另外,如果小于一定的值,也可以不要,但是这里需要注意,特征还没经过无量纲化,特征之间的方差大小不能对比的。
#计算显示特征的方差
print(data.var())
#使用VarianceThreshold类进行方差过滤
from sklearn.feature_selection import VarianceThreshold
#要生成这个类的对象,就需要一个参数,就是最小方差的阈值,我们先设置为0.001,然后调用它的transform方法进行特征值的过滤
variancethreshold=VarianceThreshold(threshold = 0.001)
variancethreshold.fit_transform(data)
#使用get_support方法,可以得到选择特征列的序号,然后根据这个序号在原始数据中把对应的列名选择出来即可
variancethreshold.get_support()
print('----------------------------------------')
print('原数据的shape:',data.shape)
print('满足方差大于threshold的特征个数:',data.columns[variancethreshold.get_support()].shape)
print('不满足方差大于threshold的特征:',data.columns[variancethreshold.get_support()==False])
#再次强调,这里只是好奇看看哪些方差这么小,并没有删除方差比0的特征
原有70个字段(其中一个是标签啊!),方差小于0.001的有三个,分别是:'shifoujiatingkuandaiyonghu', 'ruwangqudaoleixing_kefuqudao','ruwangqudaoleixing_null'。
- 基于树模型的特征选择法
直接使用随机森林,对数据进行建模,提取模型中对每个特征的importance系数。
from sklearn.ensemble import RandomForestClassifier
#训练模型,不管任何参数,都用默认的
random_forest = RandomForestClassifier(oob_score=True, random_state=10)
random_forest.fit(X_train,y_train.values.ravel())
#提取模型结果对各个特征的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') #加上标签字段
importance系数较大的特征截图
提炼了69个特征中的30个,其他特征删掉。
- 基于Logistics Regression看特征正负作用
直接使用logistics regression模型,对数据进行训练,提取每个特征对应的系数,从正负号看出每个特征对最
终标签的正负影响。
from sklearn.linear_model import LogisticRegression
#1、训练模型
lr = LogisticRegression(penalty = 'l1') #直接默认参数
lr.fit(X_train,y_train.values.ravel()) #用原始数据训练
#2、提取系数
lr_coef_df = pd.DataFrame(lr.coef_.T,columns=["lr_coef_df"],index=X_train.columns)
#3、跟randomforest的结果合并同一个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,是负数,那么就是对离网是负影响,也就是有这个1标识,越不会离网
yonghuzhuangtai_201711_stop_using,是正数,那么就是对离网是正影响,有这个1标识,就越会离网
基本跟认识一致。
6.后续提升
异常值的详细查看、无量纲化、降维、正态分布/幂律分布分析等并未深入了解,待分析。
补充说明:之前尝试过对数据进行无量纲化,但是卡在两个问题上:
第一:异常值,如果对数据进行归一化的无量纲处理,前提要把异常值踢掉,比如apru值,有500多的少数用户,归一化就会把正常用户压缩在一个小范围内,其实不归一化也会,还是回归到怎么处理异常,怎么定义异常上了。
第二:分布规律,基本接触到的机器学习处理,都默认数据是正态分布的,但是从arpu值、mou值、活跃日数等数据的分布来看,都不是正态,而是幂律分布,那么用到的算法要适当调整吗?
四、模型训练
经常说80%的时间在特征学习,20%的时间在模型训练,还真觉得对啊。另外,强大的sklearn都把模型封装好了,直接用就是。现在的思路是各种ensemble模型轮流来一次看看效果,选几个还不错的继续调参优化。
1. 各种ensemble模型,默认参数跑
(Logistics Regression不是ensemble model)
以随机森林random forest为例跑一次,其他模型大同小异
# 数据准备
#1、将特征X和标签Y拆分开
X = data.loc[:, data.columns != 'yonghuzhuangtai']
y = data.loc[:, data.columns == 'yonghuzhuangtai']
#2、然后随机提取80%的数据作为模型训练用,20%数据留作模型性能测试用
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、生成模型对象,参数都是默认
random_forest = RandomForestClassifier()
#2、用那80%的数据进行模型训练,其实就一句话
random_forest.fit(X_train,y_train.values.ravel())
#3、用训练的模型对剩下的20%数据进行预测,将预测结果和已知标签对比,得出模型性能
y_pred = random_forest.predict(X_test.values)
#4、画出性能图
plt.figure()
plot_confusion_matrix(y_test,y_pred,title='Confusion matrix')
判断标准
结果出来,是一个混淆矩阵,横坐标是预测,纵坐标是实际发生,性能主要看三个指标:
-
recall 召回率,真实流失客户中,多少被预判为流失。
-
precision 准确率,预测为流失的客户中,真的会走的的有多少。
-
f1,综合考虑recall 和 precision的指标。
再解释通俗讲一遍就是,模型预测的1000个客户,有800个确实最后离网了,80%的precision,但是也有另外800个真实离网的没被预测到,50%的recall。预测的都蛮准,但是预测不全。
各个模型的效果对比
默认参数下,GradientBoostingClassifier、RandomForest、XGBClassifier明显效果好一点。
# 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、模型调参
调参是什么,就是一个模型有很多参数,比如random forest 随机森林,有树的数量,树的深度等等,需要设置的,那么设置多少最好呢,都不知道,那就暴力点,每种都试试,这就是调参咯,试出最好的那个参数组,random forest主要要调节的参数有:
- n_estimators
- max_depth
- min_samples_split
- min_samples_leaf
- max_features
对以上5个参数都要进行调参,逐一GridSearchCV,只拿n_estimators调参举例,具体如下,试一下50,70,90,100,120,150中各种取值的效果,提取效果最好的那个参数:
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'
# 这里多说两句:
# param_grid:需要交叉验证的参数,这里有6个数据,分别是50,70,90,100,120,150,究竟哪个参数出来的效果最好呢?
# cv=5,就是5折交叉试验,把数据分为5分,每次提取其中4份跑模型,1份留作验证,5次的平均表现是评定标准。
# 每个参数都跑5次,取平均值再对比才有说服力,那么就总共跑了5*6次,30次了。
# scoring = 'f1',那么效果最好的这个效果是什么标准呢?可以是‘accuracy’,‘roc_auc’,这里选择'f1'
grid.fit(X_train,y_train.values.ravel())
# 记录最好的参数
best_n_estimators = grid.best_estimator_.n_estimators
print(best_n_estimators)
调参明细:
- 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))
这里是最花时间的,每个参数经常要1个小时才能出来最优那个,5个参数基本就是一个晚上,才能出来一组最优参数组,最后f1一般能提升1%-3.5%。本项目random forest模型调参后f1达到0.620960,提升0.919%,GradientBoostingClassifier调参后提升到0.629094,提升了2.514%,比random forest调参后也高0.8134%。
五、模型使用
可以使用历史数据训练出来的模型对当前数据进行预测,具体的预测效果如何呢?试过一个假设,T月的流失客户训练出来的模型,f1是60%,然后用到T+1的全量数据中进行预测,f1是57%,低了3%,数据还算是有延续性,可以预测。
六、使用环境介绍
- 语言:python
- IDE:anaconda
- 写作:简书
小结:由于本人水平有限,本文内容估计有很多错漏的地方,希望各位路过有缘人指出,学习学习。