重启篇-第一篇(preprocessing),这是对前传的重启,随着对机器学习的理解,进行迭代。
简介:重启篇将会分为三部曲:
- 第一部是预处理(preprocessing),也就是本篇的核心内容,强调一下,铺垫很重要。
- 第二部是传统机器学习(tradition),使用相对传统的机器学习算法解决。
- 第三部是神经网络(又名深度学习,Neural Network),使用时髦的机器学习算法解决。
重启开始!
要解决的问题是什么
客户流失的自定义:客户T月在网使用,但是T+2月离网了。溜走的都是收入啊!
怎么解决
机器学习,使用T月和T月之前的客户数据,对数据进行预处理后,放入到模型进行训练,再放入新的数据时,模型给出它的预判结果。
预处理
提取数据后,要进行预处理,本文的重点。
一、为什么要预处理(preprocessing)
我想说,只要你看一眼就知道为什么了?原始的数据会有很多的杂质,比如有些样本都不是电话号码,有些明显的异常值(outlier),有些缺失值,还有各个特征的量纲相差上百倍等等,预处理就是要解决以上的这些问题。好比做饭前也要洗米,洗掉灰尘、挑出砂石等等。
二、预处理分为哪些步骤?
筛选出真实的号码样本、打标记定义标签
特征工程,包括:
- 定性特征哑编码(one-hot)
- 缺失值处理
- 标准化
- 降维
三、筛选出真实的号码样本、定义标签
原始样本中含有不少非正常用户号码的样本,需要清理掉,接下来就是根据定义对号码进行标记,打标签。
1、筛选出真实的号码样本
# 先读取样本文件,放入到data,此处省略对应语句。
data['number2'] = (data['number'].apply(str).str[:3]).apply(int) #先转化成string类型,再提取,再转化成int
print("缺失值::",data['number2'].isnull().sum()) #缺失值统计
print(data.shape)
print(data['number2'].value_counts())
可以看出number前三位有很多异常情况,是一些明显不是正常客户使用的号码,直接删掉。
#可以看出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。
data2=copy.copy(data[(data.number2>130)&(data.number2<200)])
print("原数据行列结构:",data.shape)
print("新数据行列结构:",data2.shape)
print("----------------------------------")
print(data2['number2'].value_counts())
删掉了24万的数据,都是不正常的号段,比如2开头,3开头等一些不是电话号码的信息,可能是一些其他的账户信息,比如宽带、行业卡等等。
2、定义标签
流失客户的定义:客户T月在网使用,但是T+2月离网了。详细定义:T月客户(正使用、停机、已转换品牌的状态),在T+2月流失了(除了正使用、停机、已转换品牌的状态),字段yonghuzhuangtai为1的是流失。
T_zhuangtai = "yonghuzhuangtai_201711"
T_2_zhuangtai = "yonghuzhuangtai_201801"
print(data[T_zhuangtai].value_counts())
print("缺失值:",data[T_zhuangtai].isnull().sum()) #缺失值统计
print("-------------------------")
print(data[T_2_zhuangtai].value_counts())
print("缺失值:",data[T_2_zhuangtai].isnull().sum()) #缺失值统计
1)T月在网使用客户,只保留T月在使用的客户,同时剔除一些异常的字段。
# 只保留T月客户状态为(正使用、停机、已转换品牌的状态)的
data2 = copy.copy(data[(data.yonghuzhuangtai_201711=="正使用")|
(data.yonghuzhuangtai_201711=="停机")|
(data.yonghuzhuangtai_201711=="已转换品牌")])
# 删除一些异常字段,比如yonghuzhuangtai_201801为“已开通未售出”、“临时生成资料”、“有效期到期销户”
data2 = copy.copy(data2[(data2.yonghuzhuangtai_201801!="已开通未售出")&
(data2.yonghuzhuangtai_201801!="临时生成资料")&
(data2.yonghuzhuangtai_201801!="有效期到期销户")])
data2.shape
2)标签就是标记T+2月离网的客户
# 标记为1的是离网客户,0为在网客户。
data2["yonghuzhuangtai"] = data2['yonghuzhuangtai_201801'].map({
'正使用':0,'停机':0,'已转换品牌':0,
'欠费销户':1,'正式销户':1,'预约销户':1,'null':1})
data2.shape
最后行数减少到417039行。
3)流失客户
流失客户11435人,人均33.79元,合计38.6万。
小结:
经过对number的清洗和标签的定义,行数由原来的668945行减少到417039行,数据由56个feature和1个label构成,这就使得问题成为一个监督学习(supervised learning)。预处理是一种比较粗的处理,特征工程则会更加的细致。
四、特征工程
网上流传:“有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。那特征工程到底是什么呢?顾名思义,其本质是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。”
个人理解,落到项目上,就是数据有各种乱七八糟的情况,比如字符(中文或英文)、数值缺失、量纲不一等等,一般不能直接被模型使用,而特征工程就是对数据进行处理,令其可以被模型读取使用,而更好的特征工程,你可以根据原有特征生成新特征或者筛选重要特征等等,从而提升模型的效果或效率。
1、定性特征处理
定性特征,就是用字符来描述的特征,比如套餐名字、入网渠道等等。
对应的就是定量特征,就是用数字来描述的特征,比如消费、通话分钟等等。
对于有大小意义的定性特征转化成有大小意义的数字,对于没有大小意义的定性特征进行哑编码(one-hot)。
- 有大小意义的定性特征转化成有大小意义的数字
# 有大小意义的定性特征,可以变成有大小关系的定量特征
data['zhongduanzhishi']=data['zhongduanzhishi'].map({'4G':4,'3G':3,'2G':2,})
-
没有大小意义的定性特征进行哑编码(one-hot)
哑编码(one-hot),就是把一个特征根据特征的内容“穷举”拆分成多个特征,然后描述上变成1/0,示意图:
# 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。
(除了填0,当然还有利用其他数据生成或者填入平均值之类的)
data.fillna(value = 0, axis = 1, inplace = True)
(这里要分割线一下,以下内容就是对前一版本的更新迭代了。)
3、细看数据
经过一系列的操作后,还没正式看看数据的分布是怎样的,现在来看一下,有意料之外,也是意料之中。
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("whitegrid") #seaborn有五种模版darkgrid, whitegrid, dark, white, and ticks可以选择
def one_feature_Analysis(data, one_feature, threshold_value):
data_leave = copy.copy(data[data["yonghuzhuangtai"]==1])
data_filter_greater = copy.copy(data[data[one_feature]>threshold_value])
data_filter_greater_leave = copy.copy(data_filter_greater[data_filter_greater["yonghuzhuangtai"]==1])
data_filter_smaller = copy.copy(data[data[one_feature]<=threshold_value])
data_filter_smaller_leave = copy.copy(data_filter_smaller[data_filter_smaller["yonghuzhuangtai"]==1])
#------------------------------------主要是找出异常值的划分界限----------------------------
fig = plt.figure(figsize=(30,10))
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('max')
ax2 = fig.add_subplot(1,2,2)
ax2.set_title(threshold_value)
sns.distplot(data[one_feature], ax = ax1, bins=100,kde=True)
sns.distplot(data_filter_smaller[one_feature], ax = ax2, bins=100,kde=True)
#-----------------------------------主要是看看单个特征与标签的关系--------------------------
g = sns.FacetGrid(data_filter_smaller, hue="yonghuzhuangtai",size=8)
g.map(plt.hist, one_feature,alpha=.6,bins= 30 );
g = sns.FacetGrid(data_filter_smaller_leave, hue="yonghuzhuangtai",size=8)
g.map(plt.hist, one_feature,alpha=.6,bins= 30 ,color = 'r');
plt.show()
print('-----------------------------------------------------------------------------------------------------')
3.1、 先看主叫次数:
- 下面第一个图说明数据并不是正态分布,而是幂律分布的,而且明显存在离群值outlier
-
第二个图说明流失客户集中(92%)在通话次数不大于7次的范围内。(有个想法,把问题简化在此部分客户的训练,后面的文章会提到。)
3.2、 看ARPU:
也出现类似的情况,也是幂律分布,存在outlier,离网客户集中在 0 ARPU值。
3.3、 再看看两个feature
横坐标是主叫次数,纵坐标是当月ARPU,label分为两种颜色,这个图表达了两个feature和label的分布关系,橙色流失客户集中在左下,人为把分布分为三个区域。
- 1区:这个区域的客户是流失客户集中区
- 2区:这个区域内的客户基本都是留存的
- 3区:外围的数据很稀疏,毕竟高主叫次数和高ARPU的客户是少数。
再看看arpu和gprs的。
其实如果对客户进行九宫格划分,从这个图来看,好像不大合理,客户会集中在某个格子。
对比以上两个图片:
- 第一个图,主叫次数和arpu某程度上是正相关,通话越多,arpu越大,随意分布大概呈现扇形。
- 第二个图,mou值跟gprs流量,毕竟人的精力时间是有限的,如果mou值很大,很难gprs也很大。
然后再试试把取值缩放到某个范围,没有明显的界限能进行划分。
4、标准化
- 什么是标准化:就是去均值(0均值)+无量纲。
- 为什么要标准化:许多学习算法中目标函数的基础都是假设所有的特征都是零均值并且具有同一阶数上的方差。如果某个特征的方差比其他特征大几个数量级,那么它就会在学习算法中占据主导位置,导致学习器并不能像我们说期望的那样,从其他特征中学习。(出处点这里)
标准化的方法有很多,但是要处理带有离群值的数据,sklearn推荐使用RobustScaler。
from sklearn.preprocessing import RobustScaler
robustScaler = RobustScaler( quantile_range=(25, 75))
X_train_RobustScaler = robustScaler.fit_transform(X_train)
X_train_RobustScaler_df = pd.DataFrame(X_train_RobustScaler,columns=X_train.columns,index=X_train.index)
来对比下RobustScaler前后的变化,用之前专门看过的主叫时长和ARPU,主叫时长的绝大部分样本都压缩在[-0.5,3]之间,ARPU的绝大部分样本都压缩在[-1,2]之间,这两个feature的数据量级基本在同一水平,同时,最大化地减少了outlier的影响。
5、降维-feature selection
为什么要降维,减少要考虑的随机变量的数量,提高效率。重点讲特征选取(feature selection),个人理解,提数的时候把能提的字段(feature)都提了,但是其中很多字段跟label并没多大关系,减少后运行更高效率。另外还有PCA (Principal component analysis 主成分分析),用于对一组连续正交分量中的多变量数据集进行方差最大方向的分解,PCA后的feature数据,其实你都不知道他的意思是什么了。
5.1、方差variance
方差选择法只是考虑特征本身的离散程度,并不考虑与标签结果的相关性。(个人了解,主要从方差大小中,查到一些方差几乎为0的feature,直接删掉。)
data_var = pd.DataFrame(data.var(),columns=['var'])
print(data_var.sort_values(axis = 0,ascending = False, by = 'var'))
5.2、方差分析ANOVA
from sklearn.feature_selection import SelectKBest, f_classif , chi2
# 将X和Y拆分开
X = data.loc[:, data.columns != 'yonghuzhuangtai']
y = data.loc[:, data.columns == 'yonghuzhuangtai']
print(X.shape)
print(y.shape)
# chi2 : Input X must be non-negative.
# For regression: f_regression, mutual_info_regression
# For classification: chi2, f_classif, mutual_info_classif
# Warning:Beware not to use a regression scoring function with a classification problem, you will get useless results.
selector = SelectKBest(f_classif, k="all")
selector.fit(X, y.values.ravel())
scores = selector.scores_
selector_scores = pd.DataFrame(selector.scores_,columns=["scores"],index=X.columns)
print(selector_scores.sort_values(axis = 0,ascending = False, by = 'scores'))
print("-----------------------------------")
# Plot the scores.
fig = plt.figure(figsize=(20,20))
plt.barh(range(len(selector_scores.index)), selector_scores["scores"])
plt.yticks(range(len(selector_scores.index)),selector_scores.index, rotation='horizontal')
plt.show()
print('scores mean:',selector_scores.mean())
5.3、基于树模型的特征选择法
Feature Selection 最实用的方法也就是看 Random Forest 训练完以后得到的Feature Importance 了,选取importance值高的feature。
from sklearn.ensemble import RandomForestClassifier
#不管任何参数,都用默认的,我们拟合下数据看看:
random_forest = RandomForestClassifier(oob_score=True, random_state=10)
random_forest.fit(X_train,y_train.values.ravel())
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'))
print("-----------------------------------")
# Plot the scores.
fig = plt.figure(figsize=(20,20))
plt.barh(feature_importances_df_1.index,feature_importances_df_1["importances"])
plt.show()
importances
dangyueARPU 0.065700
yonghuzhuangtai_201711_using 0.055392
yonghuzhuangtai_201711_stop_using 0.048193
shengri 0.045874
huoyuetianshu_xishu 0.043831
shang1yueyue 0.041631
zhanghuyue 0.040714
ruwangriqi 0.033349
jihuoriqi 0.032044
huoyuetianshu 0.030658
guoqu3yueyuejunyue 0.028249
yuejieri 0.026939
jinsanyueGPRS 0.026760
dangyueMOU 0.023672
jinsanyueARPU 0.022665
jinsanyueMOU 0.021207
zaiwangyueshu 0.020833
shang1yueGPRS 0.020492
importances最高的是当月ARPU和当月状态,还有活跃天数、余额等。其实这个结果可以看成,客户离网前通过这些特征表现了,你再不管,他就走了,如果你察觉到了,就去问问人家为什么走咯?(知道关联关系,但不能说是因果关系。)
小结
还没试试生成新特征。
第一部的preprocessing已经结束,THE END......