传统机器学习预测客户流失

image.png

一、机器学习预测客户流失


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)
image.png

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())  #看统计
图片.png

可以看出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开头等一些不是电话号码的信息,可能是一些其他的账户信息,比如宽带、行业卡等等。

image.png

洗掉后,原有行数由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
image.png

最后行数减少到417039行。

小结:

经过对number的清洗和标签的定义,行数由原来的668945行减少到417039行。预处理是一种比较粗的处理,特征工程则会更加的细致。


三、特征工程


经过预处理后,数据基本可用,就进入最重要的两大部分,第一是特征工程,第二部分是模型训练,网上流传:“有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。那特征工程到底是什么呢?顾名思义,其本质是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。”个人理解,落到项目上,就是数据有各种乱七八糟的情况,比如字符(中文或英文)、数值缺失等等,一般不能直接被模型使用,而特征工程就是对数据进行处理,令其可以被模型读取使用,而更好的特征工程,你可以根据原有特征生成新特征或者筛选重要特征等等,从而提升模型的效果。本项目现阶段特征工程的脑图如下:

图片.png

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()各种取值的统计量

图中是其中一个特征的各种取值的统计量,可以看出,里面有三类中文取值,分别是“正使用”、“停机”、“已转换品牌”。

image.png

这种特征叫做定性特征(相对定量特征),现在学到的机器学习算法是不能直接对定性特征进行计算建模的,那就需要用get_dummies对定性特征哑编码,这里要插入简单介绍一下哑编码,就是每类取值变成单独一个特征,然后用0、1标识对应行是否具备该特征,如下图:

image.png

哑编码

# 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个呢。

  1. 看看有多少特征
data = data.drop(['number'],axis=1)   #number是各个客户的ID,不属于特征,删掉。
print(data.shape)   #(417039, 70),有41万条记录,70个字段(69个特征,1个标签)
  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'。


image.png
  1. 基于树模型的特征选择法
    直接使用随机森林,对数据进行建模,提取模型中对每个特征的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系数较大的特征截图


image.png

提炼了69个特征中的30个,其他特征删掉。

  1. 基于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标识,就越会离网
基本跟认识一致。

image.png

6.后续提升

异常值的详细查看、无量纲化、降维、正态分布/幂律分布分析等并未深入了解,待分析。
补充说明:之前尝试过对数据进行无量纲化,但是卡在两个问题上:
第一:异常值,如果对数据进行归一化的无量纲处理,前提要把异常值踢掉,比如apru值,有500多的少数用户,归一化就会把正常用户压缩在一个小范围内,其实不归一化也会,还是回归到怎么处理异常,怎么定义异常上了。
第二:分布规律,基本接触到的机器学习处理,都默认数据是正态分布的,但是从arpu值、mou值、活跃日数等数据的分布来看,都不是正态,而是幂律分布,那么用到的算法要适当调整吗?

image.png


四、模型训练


经常说80%的时间在特征学习,20%的时间在模型训练,还真觉得对啊。另外,强大的sklearn都把模型封装好了,直接用就是。现在的思路是各种ensemble模型轮流来一次看看效果,选几个还不错的继续调参优化。

1. 各种ensemble模型,默认参数跑

image.png

(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')

image.png

判断标准

结果出来,是一个混淆矩阵,横坐标是预测,纵坐标是实际发生,性能主要看三个指标:

  1. recall 召回率,真实流失客户中,多少被预判为流失。


    image.png
  2. precision 准确率,预测为流失的客户中,真的会走的的有多少。


    image.png
  1. f1,综合考虑recall 和 precision的指标。


    image.png

再解释通俗讲一遍就是,模型预测的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%。

image.png

五、模型使用


可以使用历史数据训练出来的模型对当前数据进行预测,具体的预测效果如何呢?试过一个假设,T月的流失客户训练出来的模型,f1是60%,然后用到T+1的全量数据中进行预测,f1是57%,低了3%,数据还算是有延续性,可以预测。


六、使用环境介绍


  • 语言:python
  • IDE:anaconda
  • 写作:简书

小结:由于本人水平有限,本文内容估计有很多错漏的地方,希望各位路过有缘人指出,学习学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容