算法原理:
决策树是一种自上而下,对样本数据进行树形分类的过程。由节点和有向边组成,节点分为内部节点和叶子点,其中每个内部节点表示一个特征或者属性,叶节点表示类别。从顶部节点开始,所有样本聚在一起,经过根节点划分,样本被分到不同的字节点中,再根据字节点的特征进一步划分,直到所有样本都被归为某个类别。
决策树构造方法其实就是每次选择一个好的特征以及分裂点作为当前节点的分类条件。
决策树的训练和测试:
- 训练阶段:从给定的训练集构造一棵树(从根节点开始选择特征,如何进行特征切分)
- 测试阶段:根据构造出来的树模型,从上到下走一遍
- 关键:如何构造这棵树—— 选择特征;确定分枝(切分)标准。
- 衡量标准:a.熵——表示随机变量不确定性的度量,就是物体内部的混乱程度。值越低,越稳定;越高,越杂乱,即不稳定。
H(x)=- pi*log pi,i=1,2,3…n
当pi接近1(概率大),结果接近0;
当pi接近0(概率小),结果接近无穷.
也就是说,每取到一个数的概率越小,说明越混乱,熵值越大。
分类任务中,我们希望通过节点分之后数据类别的熵值越小越好。
引用一下算法案例链接(感谢原创):https://www.cnblogs.com/Erdos001/p/5777465.html
(一)ID3:最大信息墒
熵:表示随机变量的不确定性。
条件熵:在一个条件下,随机变量的不确定性。
信息增益:熵 - 条件熵
在信息增益中,衡量标准是看特征能够为分类系统带来多少信息,带来的信息越多,该特征越重要。对一个特征而言,系统有它和没它时信息量将发生变化,而前后信息量的差值就是这个特征给系统带来的信息量。所谓信息量,就是熵。它定义为一个特征能够为分类系统带来多少信息,带来的信息越多,说明该特征越重要,相应的信息增益也就越大
所以一个特征下的信息增益越大,说明这个特征的分类效果越好。
(二)C4.5:最小信息增益率
C4.5克服了ID3用信息增益选择属性时偏向选择取值多的属性的不足
(三)CART: 最小基尼系数
利用GINI系数判断:越纯基尼系数越小,分类越好。
采用二叉树,利用二元切割法,每一步按照特征A的取值切割成两份
剪枝策略:避免过拟合
预剪枝:
作用:边建立决策树边进行剪枝操作(更实用)
方式:限制深度;限制叶子节点个数;叶子结点样本数;信息增益量等。
优点:降低过拟合,减少决策树训练时间
缺点:有欠拟合风险
也就是说,对每个节点在划分前进行估计,若当前节点的划分不能带来决策树泛化性的提升,则停止划分并将当前节标记叶节点;后剪枝:
建立完一个完整的决策树以后,自底向上对非叶子节点进行考察,如果将结点对应的子树替换为叶节点能够带来泛华性提升,则将该子树替换为叶子节点。
优点:欠拟合风险小
缺点:训练时间花销大
用户流失项目:
接下来,通过一份电信公开数据集,进行决策树模型的构建,预测那些有可能流失的电信用户,以针对性实施挽留策略。
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
df=pd.read_csv(r'C:/Users/Admin/....../churn.csv')
df.head()
首先导入我们需要分析的数据,并查看前5行.
这里的字段分别为:国家,账户长度,地区代码,电话,国际计划,邮件计划,邮件信息,日间时长,日间次数,日间费用,傍晚时长,傍晚次数,傍晚费用,夜间时长,夜间次数,夜间费用,国际时长,国际次数,国际费用,是否流失
1、数据探索
拿到数据集后第一件事,首先进行数据的基本观察,通过描述性统计呈现,为后续的模型建立与方法选择提供灵感
df.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
这里可以查看到所有字段的有效数值和对应的数值类型。可以看到,没有缺失数据,数据集比较规整(毕竟是公开数据集)
df.describe() #查看一下字符类型为整型的字段,对应的统计数值。
user_state=user_state['Day Mins','Eve Mins','Night Mins','Intl Mins']
user_state.sum().plot()
对于一天内各时间段下面的拨打时长,我们可以做出一个针对不同州下的对比分布图。
首先,不同地方通话时长各有不同,但同一地方三个时间段的时长都相近,也就是说,通话总时长会与当地人民的习惯相关,而每个地方几乎都是白天通话最少,晚上通话最长。由此可推测,夜晚这个消退了白天繁忙的日常后,是人们愿意亲苏沟通的好时机。
其次,国际通话时长平稳的搁置在水平轴上方不远,说明用户多在国内进行工作生活,国际长途电话需求较少。
user_mail=df.groupby('State').sum().sort_values('VMail Message')
user_mail['VMail Message'].plot()
这是关于不同州下发送邮件的数量的曲线图。可以看到,邮件的发送和当地的‘风俗习惯’息息相关。
sns.distplot(df[['Day Charge']],bins = 20,kde = True)
sns.distplot(df[['Eve Charge']],bins = 20,kde = True)
sns.distplot(df[['Night Charge']],bins = 20,kde = True)
根据白天/傍晚/晚上 三个时间段下用户的话费直方图分布情况看到(红:day,蓝:Eve,紫:Night),白天通话平均为30元,方差较大,分布较广;傍晚通话话费次之,平均在18元左右,大体分布在10~30元;夜间通话话费基本集中在10元上下,且人数较多。
综合可以看出,不同的州可能有不同发送邮件习惯,但是都习惯在晚上,买个优惠包,‘便宜’的说个够。
整体看下所有字段相互间的关系:
data=df[['']]
sns.pairplot(data=df)
plt.show()
现在创建新的字段进行汇总统计:
df['mins']=df['Day Mins']+df['Eve Mins']+df['Night Mins']
df['call']=df['Day Calls']+df['Eve Calls']+df['Night Calls']
df['charge']=df['Day Charge']+df['Eve Charge']+df['Night Charge']
df.head()
fig,(ax1,ax2,ax3,ax4) = plt.subplots(1,4,figsize=(18,5))
sns.barplot(x='Churn?',y='mins',data=df,ax=ax1)
sns.barplot(x='Churn?',y='call',data=df,ax=ax2)
sns.barplot(x='Churn?',y='charge',data=df,ax=ax3)
sns.countplot(x='Churn?',hue='VMail Plan',data=df,ax=ax4)
plt.show()
从前三张图可以看到,是否流失与通话时长/通话次数/通话费用并没有什么关系(柱形高度很相近),但是与邮件的收发计划与否可能相关。因此,在运用决策树进行预测时,我们可以考虑除去明显无法起到决策作用的字段:电话,
致此,我们对数据已经有了初步的了解。接下来,利用决策树模型进行分类预测。
2、构建决策树模型
- 目标:对于公司运营而言,我们希望找到那些可能流失的人员,并针对流失概率较大的用户进行针对性挽留。
- 步骤:
from __future__ import division
churn_df = pd.read_csv(r'C:/Users/Admin……churn.csv')
col_names = churn_df.columns.tolist()
print ("Column names:")
print (col_names)
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?']
to_show = col_names[:6] + col_names[-6:] #选择部分需要的字段
print ("\nSample data:")
churn_df[to_show].head(6)
#提取目标变量
churn_result = churn_df['Churn?']
# 将目标分类变量编码为0,1,其中True为1
y = np.where(churn_result == 'True.',1,0)
# 删除一些我们不需要的字段&目标字段作为特征,并存入新建的变量中
to_drop = ['State','Area Code','Phone','Churn?'] #前三个字段无实际意义,最后字段是目标变量。
churn_feat_space = churn_df.drop(to_drop,axis=1)#删掉无关字段后,获得特征
# 将涉及'yes'/'no'的字段选出来,修改为布尔值:yes=true,no=false
# 后续会统一对布尔值进行0/1编码
yes_no_cols = ["Int'l Plan","VMail Plan"]
churn_feat_space[yes_no_cols] = churn_feat_space[yes_no_cols] == 'yes'
features = churn_feat_space.columns
#修改的布尔值其实是数据框形式,为了进行拟合,需要改成numpy数组的形式:as_matrix(),数值设置为浮点型
X = churn_feat_space.as_matrix().astype(np.float)
# 将其余数值数据做标准化处理
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
# 标准化处理后的数据已经可以用于后续建模。这里打印一下数据的维度(行*列)
print ("\nFeature space holds %d observations and %d features"% X.shape)
#去除数组中的重复数字,输出目标变量所有值
print ("\nUnique target labels:",np.unique(y))
print (X[0])
#输出目标变量为0的值:也就是输出流失用户量
print (len(y[y == 0]))
Feature space holds 3333 observations and 17 features
Unique target labels:[0,1]
[ 0.67648946 -0.32758048 1.6170861 1.23488274 1.56676695 0.47664315
1.56703625 -0.07060962 -0.05594035 -0.07042665 0.86674322 -0.46549436
0.86602851 -0.08500823 -0.60119509 -0.0856905 -0.42793202]
2850
随机森林中树模型参数:
1.criterion gini or entropy
2.splitter best or random 前者是在所有特征中找最好的切分点 后者是在部分特征中(数据量大的时候)
3.max_features None(所有),log2,sqrt,N 特征小于50的时候一般使用所有的
4.max_depth 数据少或者特征少的时候可以不管这个值,如果模型样本量多,特征也多的情况下,可以尝试限制下
5.min_samples_split 如果某节点的样本数少于min_samples_split,则不会继续再尝试选择最优特征来进行划分如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。
6.min_samples_leaf 这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝,如果样本量不大,不需要管这个值,大些如10W可是尝试下5
7.min_weight_fraction_leaf 这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝默认是0,就是不考虑权重问题。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。
8.max_leaf_nodes 通过限制最大叶子节点数,可以防止过拟合,默认是"None”,即不限制最大的叶子节点数。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制具体的值可以通过交叉验证得到。
9.class_weight 指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多导致训练的决策树过于偏向这些类别。这里可以自己指定各个样本的权重如果使用“balanced”,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。
10.min_impurity_split 这个值限制了决策树的增长,如果某节点的不纯度(基尼系数,信息增益,均方差,绝对差)小于这个阈值则该节点不再生成子节点。即为叶子节点 。
n_estimators:要建立树的个数
为了确定分类方法,我们可以写一个函数,让测试数据在不同分类方法下面进行分类预测,通过最后的分类效果进行最后的方法选择。
from sklearn.cross_validation import KFold
def run_cv(X,y,clf_class,**kwargs): # **是kwargs 分类器参数代码
kf = KFold(len(y),n_folds=5,shuffle=True) # 将数据分成五份,并打乱数据(shuffle=True)
y_pred = y.copy()
# Iterate through folds
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
# Initialize a classifier with key word arguments
clf = clf_class(**kwargs)#加载分类器
clf.fit(X_train,y_train) #拟合训练测试集
y_pred[test_index] = clf.predict(X_test)
return y_pred
# 引入三个分类模型,进行对比用于后续判断
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn.neighbors import KNeighborsClassifier as KNN
# 自定义准确率函数
def accuracy(y_true,y_pred):
# NumPy interprets True and False as 1. and 0.
return np.mean(y_true == y_pred)
#求出三种方法下的准确率分值
print ("Support vector machines:")
print ("%.3f"% accuracy(y, run_cv(X,y,SVC)))
print ("\nRandom forest:")
print ("%.3f" % accuracy(y, run_cv(X,y,RF)))
print ("\nK-nearest-neighbors:")
print ("%.3f" % accuracy(y, run_cv(X,y,KNN)))
从这里就可以看到,决策树的准确率会更高一些。
def run_prob_cv(X, y, clf_class, **kwargs):
kf = KFold(len(y), n_folds=5, shuffle=True)
y_prob = np.zeros((len(y),2))# 生成与y相应大小的0矩阵
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test)
return y_prob
我们关心的是,分类器错的情况,即预测没有流失的客户,实际流失了。这个按照准确率是看不出来的。因此,需要看的是,预测不流失的客户,实际也不流失的概率有多高,如此也就是关注预测不流失但是实际流失的客户群体。这个,其实就是召回率(TP/(TP+FN))。
import warnings
warnings.filterwarnings('ignore')
# Use 10 estimators so predictions are all multiples of 0.1
pred_prob = run_prob_cv(X, y, RF, n_estimators=10)
#print pred_prob[0]
pred_churn = pred_prob[:,1]
is_churn = y == 1
# Number of times a predicted probability is assigned to an observation
counts = pd.value_counts(pred_churn)
#print counts
# calculate true probabilities
true_prob = {}
for prob in counts.index:
true_prob[prob] = np.mean(is_churn[pred_churn == prob])
true_prob = pd.Series(true_prob)
# pandas-fu
counts = pd.concat([counts,true_prob], axis=1).reset_index()
counts.columns = ['pred_prob', 'count', 'true_prob']
counts
数据显示,会有70%可能性(预测)流失的用户,有66个,这66个中间之只有86.3636%用户真实流失了
所以我们需要首先关注预测值为70%以上的客户。小于它的可以先不考虑。