决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。
简单说就是依据熵值计算,不断地做出选择,直到获得最终结果(熵值为0或者在设定的某个范围内)。根据计算方法不同,有ID3算法、C4.5算法、CART算法等。
每个对象就是一个节点,开始的地方叫根节点,最后不能分的地方叫叶子节点。
剪枝是决策树停止分支的方法之一,剪枝有分预先剪枝和后剪枝两种。
预先剪枝是在树的生长过程中设定一个指标,当达到该指标时就停止生长,这样做容易产生“视界局限”,就是一旦停止分支,使得节点N成为叶节点,就断绝了其后继节点进行“好”的分支操作的任何可能性。
后剪枝中树首先要充分生长,直到叶节点都有最小的不纯度值为止,因而可以克服“视界局限”,而且无需保留部分样本用于交叉验证,所以可以充分利用全部训练集的信息。但后剪枝的计算量代价比预剪枝方法大得多,特别是在大样本集中,不过对于小样本的情况,后剪枝方法还是优于预剪枝方法的。
以信息增益为准则来选择划分属性,选择划分后信息增益最大的属性进行划分,这种算法对属性可取数目较多时有偏好。
这里举个例子,比如说有个女生要选相亲对象,相亲对象有收入(income)、身高(hight)、长相(look)、体型(shape)等条件可以考察,那么我们用ID3算法来分析以下她的决策。
我们收集了她之前研究过的几十个相亲对象和决策情况,如下表,我们把它保存为文件data01.csv
收入 | 身高 | 长相 | 体型 | 是否见面 |
一般 | 高 | 帅 | 胖 | 是 |
高 | 低 | 帅 | 胖 | 是 |
高 | 低 | 普通 | 瘦 | 是 |
一般 | 低 | 丑 | 瘦 | 否 |
低 | 中 | 丑 | 匀称 | 否 |
低 | 中 | 丑 | 匀称 | 否 |
低 | 中 | 普通 | 瘦 | 否 |
一般 | 高 | 普通 | 匀称 | 是 |
高 | 高 | 帅 | 胖 | 是 |
高 | 中 | 帅 | 胖 | 是 |
低 | 中 | 普通 | 匀称 | 否 |
一般 | 低 | 丑 | 胖 | 否 |
高 | 低 | 普通 | 胖 | 是 |
一般 | 高 | 丑 | 瘦 | 否 |
一般 | 高 | 普通 | 匀称 | 是 |
一般 | 高 | 普通 | 瘦 | 是 |
一般 | 中 | 丑 | 胖 | 是 |
一般 | 中 | 帅 | 匀称 | 是 |
高 | 高 | 帅 | 胖 | 是 |
高 | 中 | 帅 | 胖 | 是 |
低 | 高 | 普通 | 瘦 | 是 |
高 | 低 | 普通 | 胖 | 是 |
低 | 低 | 丑 | 胖 | 否 |
低 | 中 | 丑 | 胖 | 否 |
高 | 低 | 普通 | 瘦 | 是 |
低 | 高 | 丑 | 瘦 | 否 |
一般 | 高 | 帅 | 匀称 | 是 |
高 | 低 | 普通 | 胖 | 是 |
低 | 中 | 丑 | 胖 | 否 |
一般 | 高 | 丑 | 瘦 | 是 |
然后我们用ID3算法来分析:
from math import log
import operator
import numpy as np
import pandas as pd
from pandas import DataFrame, Series
#构建决策和数字的映射
productDict = {'高':1,'一般':2,'低':3,'中':2, '帅':1, '普通':2, '丑':3, '胖':3, '匀称':2,'瘦':1, '是':1, '否':0}
#导入数据
def Importdata(datafile):
dataa = pd.read_excel(datafile) # datafile是excel文件,所以用read_excel,如果是csv文件则用read_csv
#将文本中不可直接使用的文本变量替换成数字
dataa['income'] = dataa['收入'].map(productDict) # 将每一列中的数据按照字典规定的转化成数字
dataa['hight'] = dataa['身高'].map(productDict)
dataa['look'] = dataa['长相'].map(productDict)
dataa['shape'] = dataa['体型'].map(productDict)
dataa['is_meet'] = dataa['是否见面'].map(productDict)
data = dataa.iloc[:,5:].values.tolist() # 取量化后的几列,去掉文本列
b = dataa.iloc[0:0,5:-1]
labels = b.columns.values.tolist() # 将标题中的值存入列表中
return data,labels
#计算数据的熵(entropy)--原始熵
def dataentropy(data, feat):
lendata = len(data) # 数据条数
labelCounts = {} # 数据中不同类别的条数
for featVec in data:
category = featVec[-1] # 每行数据的最后一个字(叶子节点)
if category not in labelCounts.keys():
labelCounts[category] = 0
labelCounts[category] += 1 # 统计有多少个类以及每个类的数量
entropy = 0
for key in labelCounts:
prob = float(labelCounts[key]) / lendata # 计算单个类的熵值
entropy -= prob * log(prob,2) # 累加每个类的熵值
return entropy
#对数据按某个特征value进行分类
def splitData(data,i,value):
splitData = []
for featVec in data:
if featVec[i] == value:
rfv = featVec[:i]
rfv.extend(featVec[i+1:])
splitData.append(rfv)
return splitData
#选择最优的分类特征
def BestSplit(data):
numFea = len(data[0]) - 1 # 计算一共有多少个特征,因为最后一列一般是分类结果,所以需要-1
baseEnt = dataentropy(data,-1) # 定义初始的熵,用于对比分类后信息增益的变化
bestInfo = 0
bestFeat = -1
for i in range(numFea):
featList = [rowdata[i] for rowdata in data]
uniqueVals = set(featList)
newEnt = 0
for value in uniqueVals:
subData = splitData(data,i,value) # 获取按照特征value分类后的数据
prob = len(subData) / float(len(data))
newEnt += prob * dataentropy(subData,i) # 按特征分类后计算得到的熵
info = baseEnt - newEnt # 原始熵与按特征分类后的熵的差值,即信息增益
if (info > bestInfo): # 若按某特征划分后,若infoGain大于bestInf,则infoGain对应的特征分类区分样本的能力更强,更具有代表性。
bestInfo = info # 将infoGain赋值给bestInf,如果出现比infoGain更大的信息增益,说明还有更好地特征分类
bestFeat = i # 将最大的信息增益对应的特征下标赋给bestFea,返回最佳分类特征
return bestFeat
#按分类后类别数量排序,取数量较大的
def majorityCnt(classList):
c_count = {}
for i in classList:
if i not in c_count.keys():
c_count[i] = 0
c_count[i] += 1
ClassCount = sorted(c_count.items(),key=operator.itemgetter(1),reverse=True) # 按照统计量降序排序
return ClassCount[0][0] # reverse=True表示降序,因此取[0][0],即最大值
#构建树
def createTree(data,labels):
classList = [rowdata[-1] for rowdata in data] # 取每一行的最后一列,分类结果(1/0)
#print(classList)
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(data[0]) == 1:
return majorityCnt(classList)
bestFeat = BestSplit(data) # 根据信息增益选择最优特征
bestLab = labels[bestFeat]
myTree = {bestLab:{}} # 分类结果以字典形式保存
del(labels[bestFeat])
featValues = [rowdata[bestFeat] for rowdata in data]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestLab][value] = createTree(splitData(data,bestFeat,value),subLabels)
return myTree
#主程序
datafile = 'data/dateperson/date01.xlsx' # 文件所在位置
data, labels = Importdata(datafile) # 导入数据
jc=createTree(data, labels) # 输出决策树模型结果
print(jc)
执行之后得到如下结果:
PS C:\coding\machinelearning>ID3相亲决策实验.py
{'income': {1: 1, 2: {'hight': {1: {'look': {1: 1, 2: 1, 3: {'shape': {0: 0, 1: 1}}}}, 2: 1, 3: 0}}, 3: {'hight': {1: {'look': {2: 1, 3: 0}}, 2: 0, 3: 0}}}}
PS C:\coding\machinelearning>
从结果来看,小姐姐找相亲对象,首先看收入,收入高的一定见,收入中等的再看身高、长相等。收入低的除非又高又帅,否则免谈。
选择具有最大增益率的属性作为划分属性,具体做法是先从候选划分属性中找到信息增益高于平均水平的属性,再从中选择增益率最高的作为划分属性。
下面再用C4.5算法把上面小姐姐相亲的例子再算一遍
from math import log
import operator
import numpy as np
import pandas as pd
from pandas import DataFrame, Series
productDict = {'高':1,'一般':2,'低':3,'中':2, '帅':1, '普通':2, '丑':3, '胖':3, '匀称':2,'瘦':1, '是':1, '否':0}
#导入数据
def Importdata(datafile):
dataa = pd.read_excel(datafile) # datafile是excel文件,所以用read_excel,如果是csv文件则用read_csv
#将文本中不可直接使用的文本变量替换成数字
dataa['income'] = dataa['收入'].map(productDict) # 将每一列中的数据按照字典规定的转化成数字
dataa['hight'] = dataa['身高'].map(productDict)
dataa['look'] = dataa['长相'].map(productDict)
dataa['shape'] = dataa['体型'].map(productDict)
dataa['is_meet'] = dataa['是否见面'].map(productDict)
data = dataa.iloc[:,5:].values.tolist() # 取量化后的几列,去掉文本列
b = dataa.iloc[0:0,5:-1]
labels = b.columns.values.tolist() # 将标题中的值存入列表中
return data,labels
#计算数据的熵(entropy)--原始熵
def dataentropy(data, feat):
lendata = len(data) # 数据条数
labelCounts = {} # 数据中不同类别的条数
for featVec in data:
category = featVec[-1] # 每行数据的最后一个字(叶子节点)
if category not in labelCounts.keys():
labelCounts[category] = 0
labelCounts[category] += 1 # 统计有多少个类以及每个类的数量
entropy = 0
for key in labelCounts:
prob = float(labelCounts[key]) / lendata # 计算单个类的熵值
entropy -= prob * log(prob,2) # 累加每个类的熵值
return entropy
#对数据按某个特征value进行分类
def splitData(data,i,value):
splitData = []
for featVec in data:
if featVec[i] == value:
rfv = featVec[:i]
rfv.extend(featVec[i+1:])
splitData.append(rfv)
return splitData
#选择最优的分类特征
def BestSplit(data):
numFea = len(data[0]) - 1 # 计算一共有多少个特征,因为最后一列一般是分类结果,所以需要-1
baseEnt = dataentropy(data, -1) # 定义初始的熵,用于对比分类后信息增益的变化
bestGainRate = 0
bestFeat = -1
for i in range(numFea):
featList = [rowdata[i] for rowdata in data]
uniqueVals = set(featList)
newEnt = 0
for value in uniqueVals:
subData = splitData(data,i,value) # 获取按照特征value分类后的数据
prob = len(subData) / float(len(data))
newEnt += prob * dataentropy(subData, i) # 按特征分类后计算得到的熵
info = baseEnt - newEnt # 原始熵与按特征分类后的熵的差值,即信息增益
splitonfo = dataentropy(subData,i) # 分裂信息
if splitonfo == 0: # 若特征值相同(eg:长相这一特征的值都是帅),即splitonfo和info均为0,则跳过该特征
continue
GainRate = info / splitonfo # 计算信息增益率
if (GainRate > bestGainRate): # 若按某特征划分后,若infoGain大于bestInf,则infoGain对应的特征分类区分样本的能力更强,更具有代表性。
bestGainRate = GainRate # 将infoGain赋值给bestInf,如果出现比infoGain更大的信息增益,说明还有更好地特征分类
bestFeat = i # 将最大的信息增益对应的特征下标赋给bestFea,返回最佳分类特征
return bestFeat
def majorityCnt(classList):
c_count = {}
for i in classList:
if i not in c_count.keys():
c_count[i] = 0
c_count[i] += 1
ClassCount = sorted(c_count.items(),key=operator.itemgetter(1),reverse=True)#按照统计量降序排序
return ClassCount[0][0]#reverse=True表示降序,因此取[0][0],即最大值
#构建树
def createTree(data,labels):
classList = [rowdata[-1] for rowdata in data] # 取每一行的最后一列,分类结果(1/0)
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(data[0]) == 1:
return majorityCnt(classList)
bestFeat = BestSplit(data) # 根据信息增益选择最优特征
bestLab = labels[bestFeat]
myTree = {bestLab:{}} # 分类结果以字典形式保存
del(labels[bestFeat])
featValues = [rowdata[bestFeat] for rowdata in data]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestLab][value] = createTree(splitData(data,bestFeat,value),subLabels)
return myTree
#主程序
datafile = 'data/dateperson/date01.xlsx' # 文件所在位置
data, labels = Importdata(datafile) # 导入数据
jc=createTree(data, labels) # 输出决策树模型结果
print(jc)
得到结果和上面的算法大致差不多
PS C:\coding\machinelearning>C4.5相亲决策实验.py
{'income': {1: 1, 2: {'look': {1: 1, 2: 1, 3: {'shape': {1: {'hight': {0: 0, 1: 1}}, 3: {'hight': {0: 0, 1: 1}}}}}}, 3: {'shape': {0: 0, 1: 1}}}}
PS C:\coding\machinelearning>
选择划分后基尼指数最小的属性最为最优划分属性。
这次我们找了个数据集,Titanic号生还者情况的数据集,来分析下乘客的生还情况
#数据处理的库
from numpy.lib.type_check import real
import pandas as pd
#数据分类
from sklearn.model_selection import train_test_split
#算法库
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import GridSearchCV
#评估用到的库
from sklearn.metrics import make_scorer
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
#预测
import numpy as np
#读取数据
data = pd.read_csv('data/titanic/train.csv')
print(data.head())
data.info()
# 计算各特征缺失总数
total = data.isnull().sum().sort_values(ascending=False)
# 计算各特征缺失比例
percent = (data.isnull().sum()/data.isnull().count()).sort_values(ascending = False)
miss_data = pd.concat([total, percent], axis = 1, keys = ['Miss_Total', 'Miss_Percent'])
miss_data.head()
# 缺失值处理。
# 删除‘Cabin’
del data['deck']
# 采用中位数填充缺失值
data['age'] = data['age'].fillna(data['age'].median())
# 众数填充缺失值
data['embark_town'] = data['embark_town'].fillna(data['embark_town'].mode()[0])
# 查看数据情况
data.info()
# 观察Name特征提取其中的Title称呼
#data['Title'] = data['Name'].str.split(",", expand=True)[1].str.split(".", expand=True)[0]
# 将字符型变量做数值化处理
label = LabelEncoder()
data['sex'] = label.fit_transform(data['sex'])
data['class'] = label.fit_transform(data['class'])
data['alone'] = label.fit_transform(data['alone'])
#data['Embarked'] = data['Embarked'].astype(str)
data['embark_town'] = label.fit_transform(data['embark_town'])
# 考虑到PassengerId和Ticker为随机生成的变量,不作为影响目标变量的信息,因此特征选择时,将其去除
features = ['class', 'age', 'n_siblings_spouses', 'parch', 'fare', 'sex', 'alone', 'embark_town','survived']
data = data[features]
data.head()
#划分训练集和测试集
X = data[['class', 'age', 'n_siblings_spouses', 'parch', 'fare', 'sex', 'alone', 'embark_town']]
y = data[['survived']]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=2)#random_state为随机种子,确保每次划分的结果是相同的
#训练模型
dtc = DecisionTreeClassifier()
dtc.fit(X_train, y_train)
y_predict = dtc.predict(X_test)
# 模型评分:准确率,查全率,查准率,F1得分
accuracyScore = accuracy_score(y_test, y_predict)
recallScore = recall_score(y_test, y_predict)
precisionScore = precision_score(y_test, y_predict)
f1Score = f1_score(y_test, y_predict)
print("DecisionTreeClassifier Results")
print("Accuracy :", accuracyScore)
print("Recall :", recallScore)
print("Precision :", precisionScore)
print("F1 Score :", f1Score)
param = {'max_depth': [1, 3, 5, 7]}
# 采用网格搜索进行参数调优
gsearch = GridSearchCV(estimator=dtc, param_grid=param, cv=5, scoring='f1')
gsearch.fit(X=X_train, y=y_train)
print("最优参数:{}".format(gsearch.best_params_))
print("最优模型:{}".format((gsearch.best_estimator_)))
print("模型最高分:{:.3f}".format(gsearch.score(X_test, y_test)))
# 选择最优模型进行预测
dtc = DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=3,
max_features=None, max_leaf_nodes=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, random_state=None,
splitter='best')
dtc.fit(X_train, y_train)
y_predict = dtc.predict(X_test[:10])
# 打印预测结果
print('===================预测值=======================')
print(y_predict)
# 打印真实值
print('===================真实值=======================')
#print(np.array(y_test[:10]).tolist())
realz = np.array(y_test[:10]).ravel()
print(realz)
Accuracy = accuracy_score(realz, y_predict)
print('准确率为:{:.2f}%'.format(Accuracy*100))
打印预测结果如下:
PS C:\coding\machinelearning>CART实验(titanic).py
survived sex age n_siblings_spouses parch fare class deck embark_town alone
0 0 male 22.0 1 0 7.2500 Third unknown Southampton n
1 1 female 38.0 1 0 71.2833 First C Cherbourg n
2 1 female 26.0 0 0 7.9250 Third unknown Southampton y
3 1 female 35.0 1 0 53.1000 First C Southampton n
4 0 male 28.0 0 0 8.4583 Third unknown Queenstown y
RangeIndex: 627 entries, 0 to 626
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 survived 627 non-null int64
1 sex 627 non-null object
2 age 627 non-null float64
3 n_siblings_spouses 627 non-null int64
4 parch 627 non-null int64
5 fare 627 non-null float64
6 class 627 non-null object
7 deck 627 non-null object
8 embark_town 627 non-null object
9 alone 627 non-null object
dtypes: float64(2), int64(3), object(5)
memory usage: 49.1+ KB
RangeIndex: 627 entries, 0 to 626
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 survived 627 non-null int64
1 sex 627 non-null object
2 age 627 non-null float64
3 n_siblings_spouses 627 non-null int64
4 parch 627 non-null int64
5 fare 627 non-null float64
6 class 627 non-null object
7 embark_town 627 non-null object
8 alone 627 non-null object
dtypes: float64(2), int64(3), object(4)
memory usage: 44.2+ KB
DecisionTreeClassifier Results
Accuracy : 0.7698412698412699
Recall : 0.7142857142857143
Precision : 0.7
F1 Score : 0.7070707070707072
最优参数:{'max_depth': 5}
最优模型:DecisionTreeClassifier(max_depth=5)
模型最高分:0.731
===================预测值=======================
[1 1 0 1 0 0 0 1 0 0]
===================真实值=======================
[1 0 0 1 0 0 0 1 0 0]
准确率为:90.00%
PS C:\coding\machinelearning>