决策树的本质是一棵树,它的每一个叶子节点表示某种分类。通过对整个树的分支进行选择,最终到达叶子节点,可得到它是何种分类的事物。
一、如何选择最佳的决策
1. 奥卡姆剃刀原理
“如无必要,勿增其值”。就是说在进行决策时候,我们要选择最快能获得结果的方式。更加直白的说法就是“能用三分力,别动五成功”。
2. 信息熵
将奥卡姆剃刀原理应用到决策树中,我们要引入信息熵。这里不对信息熵做过多的阐述。只需要明白信息熵表示一种信息的混乱状态。一个数据集合越有序,信息熵越小,所以要得到最小的信息熵的状态,可以选择信息熵的变化最大的方向进行。
信息熵计算方式:
X={x1, x2 … xn}表示一组变量,P(xi)表示对应的概率。
若对于一组数据有很多种特征,那么对于每一种特征都有条件信息熵。则条件熵的计算公式为:
T={t1, t2, … tm}表示特征T,对于不同特征ti他在样本中出现的概率为|s|/S
信息熵增量计算公式如下:
即为基础信息熵减去条件熵。
3. 决策树构建
通过数据集上所有信息熵的增量,选择最大的信息熵增量的特征。通过这个特征对整个数据集进行划分。得到一个更小的数据集,继续在这个数据集上进行划分,直到找到最终的预测值。(这样说可能有点抽象,看下面实例及代码)
二.实例说明数据选自kaggle上一个银行数据集。目的是为了预测客户是否会进行进行预期存款。数据集可以在上述链接下载。 下面为数据示例。最后的deposit即为要预测的值。
age | job | marital | education | default | balance | housing | loan | contact | day | month | duration | campaign | pdays | previous | poutcome | deposit | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 59 | admin. | married | secondary | no | 2343 | yes | no | unknown | 5 | may | 1042 | 1 | -1 | 0 | unknown | yes |
1 | 56 | admin. | married | secondary | no | 45 | no | no | unknown | 5 | may | 1467 | 1 | -1 | 0 | unknown | yes |
2 | 41 | technician | married | secondary | no | 1270 | yes | no | unknown | 5 | may | 1389 | 1 | -1 | 0 | unknown | yes |
3 | 55 | services | married | secondary | no | 2476 | yes | no | unknown | 5 | may | 579 | 1 | -1 | 0 | unknown | yes |
4 | 54 | admin. | married | tertiary | no | 184 | no | no | unknown | 5 | may | 673 | 2 | -1 | 0 | unknown | yes |
统计此数据集,可以得到特征变量及其取值(分为两种)。
1. 特征取值为字符串
[1] job : admin,technician, services, management, retired, blue-collar, unemployed, entrepreneur, housemaid, unknown, self-employed, student
[2] marital : married, single, divorced
[3] education: secondary, tertiary, primary, unknown
[4] default : yes, no
[5] housing : yes, no
[6] loan : yes, no
[7] deposit : yes, no (Dependent Variable)
[8] contact : unknown, cellular, telephone
[9] month : jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
[10] poutcome: unknown, other, failure, success
2. 特征取值为数字
[1] age
[2] balance
[3] day
[4] duration
[5] campaign
[6] pdays
[7] previous
在此数据集中,我们要选择一个特征对数据集进行划分。如何选择特征进行划分呢?答案就是选择信息熵增量最大的特征。此数据集第一次划分的最大熵增量为balance(已经过计算)。通过balance的取值可以将其分成若干类,计算若干类的划分下的信息熵增量,可以继续划分数据集,直到得到最终结果。
【说明】由于此数据集有些特征取值为数字,在做决策时划分的太细了,从而不能得到一个较好的结果,所以可以将数据进行离散化,在一个范围之内的数据看做是一种数据。在本文未做离散化。
# 决策树预测
import numpy as np
import pandas as pd
import math
class BankPredict():
def __init__(self):
pass
# 读取数据
def import_data(self, filename):
input_csvdata = pd.read_csv(filename)
feature_name = np.array(input_csvdata.columns.values).tolist()
input_data_set = np.array(input_csvdata).tolist()
# print(feature_name)
# print(input_data_set)
return input_data_set, feature_name
# 划分数据集 (通过某个特征的取值进行划分)
def split_data(self, data_set, axis, class_value):
ret_data_set = []
for feat_vector in data_set:
if feat_vector[axis] == class_value: # 获取到相同的取值,然后进行划分,返回相同分类的列表
reduce_feat_vector = feat_vector[0:axis]
reduce_feat_vector.extend(feat_vector[axis+1:])
ret_data_set.append(reduce_feat_vector)
# print (ret_data_set)
return ret_data_set
# 计算某个特定特征的信息熵
def cal_shannon_evt(self, sub_data_set):
# 计算指定列(特征)的某一种类别信息熵 对于本问题来说,结果只会有YES/NO sub_data_set里面只存储最后一列的信息 大小为n*1
class_count = {}
for item in sub_data_set:
class_count[item[-1]] = class_count.get(item[-1], 0) + 1
# print(class_count)
# 计算此特征本种分类下的信息熵
shannon_evt = 0.0
data_size = len(sub_data_set)
for class_item in class_count:
pxi = (float(class_count[class_item])/float(data_size))
shannon_evt = shannon_evt - pxi*math.log2(pxi)
return shannon_evt
# 计算条件熵
def cal_condition_evt(self, data_set, axis, class_value):
# 计算在某个特征的划分下,划分之后的条件熵(用信息熵*特定特征分类的出现的概率) axis可表示特征 class_value可表示特征内的分类情况
condition_evt = 0.0
data_size = len(data_set)
for value in class_value:
sub_data_set = self.split_data(data_set, axis, value)
sub_shannon_evt = self.cal_shannon_evt(sub_data_set)
# 计算条件熵
condition_evt = condition_evt + (float(len(sub_data_set))/data_size)*sub_shannon_evt
return condition_evt
# 计算熵增量
def inc_evt(self, data_set, base_evt, axis):
# 获取某一列
feature_list = [item[axis] for item in data_set]
# print(feature_list)
class_value = set(feature_list)
new_evt = self.cal_condition_evt(data_set, axis, class_value)
# 计算熵增 信息熵-条件熵
ie = base_evt - new_evt
return ie
# 选择熵增最大的特征进行划分
def choose_best_feature(self, data_set):
feature_num = len(data_set[0]) - 1 # 排除最后一列
base_evt = self.cal_shannon_evt(data_set)
best_evt = 0.0
best_feature = -1
for axis in range(feature_num):
axis_feature_evt = self.inc_evt(data_set, base_evt, axis)
if axis_feature_evt > best_evt:
best_evt = axis_feature_evt
best_feature = axis
# 返回熵增最大的特征行
return best_feature
# 当只有决策到只有一个类别时,输出出现次数最多的类别
def majority_class(self, class_list):
class_count = {}
for item in class_list:
class_count[item] = class_list[item]
temp_num = 0
result_class = ""
for item in class_count:
if temp_num < class_count[item]:
temp_num = class_count[item]
result_class = item
return result_class
# 构建决策树
def create_decision_tree(self, data_set, label):
# 总分类列表,本题中只有YES/NO
class_list = [example[-1] for example in data_set]
'''
决策成功的两种情况
1. 本次划分之后分类列表中只有一种分类,直接结束。
2. 本次划分使用完了所有特征还是不能划分成一个分类,选择出现次数最多的分类结束。
'''
# 情况1
if class_list.count(class_list[0]) == len(class_list):
return class_list[0]
# 情况2
if len(data_set[0]) == 1:
return self.majority_class(class_list)
best_feature = self.choose_best_feature(data_set)
best_feature_label = label[best_feature]
my_tree = {best_feature_label:{}} # 对于某个特征的树
# 已经使用过的特征进行删除标记
del(label[best_feature]) #这里删除只会删掉引用
feature_value = [example[best_feature] for example in data_set]
values = set(feature_value)
# print(best_feature_label, values)
# 对于每一种划分的不同类别都进行建树
for value in values:
sub_label = label[:]
# print(best_feature, value)
my_tree[best_feature_label][value] = self.create_decision_tree(self.split_data(data_set, best_feature, value), sub_label)
return my_tree
# 测试单个节点
def single_test(self, my_tree, testcase, labels):
# 获取根节点
root_key = list(my_tree.keys())[0]
# 根节点下的所有子树
all_child_tree = my_tree[root_key]
# 和测试节点进行比较
feature_index = labels.index(root_key)
testcase_key = testcase[feature_index]
# print('-------------------')
# print(labels)
# print('root_key: ', root_key, '/all_child_tree: ', all_child_tree, '/feature_index: ', feature_index, '/testcase_key: ', testcase_key)
# 获取测试节点对应子树
child_tree = all_child_tree[testcase_key]
# print('root_key: ', root_key, '/all_child_tree: ', all_child_tree, '/testcase_key: ', testcase_key, '/child_tree: ', child_tree)
if isinstance(child_tree, dict):
result = self.single_test(child_tree, testcase, labels)
else:
result = child_tree
return result
_DEBUG = True
if __name__ == "__main__":
FILE_NAME = r'2020\ML\ML_action\\2.DecisionTree\data\bank.csv'
bp = BankPredict()
print("data loading...")
train_size = 11000
import_data_set, feature_name = bp.import_data(FILE_NAME)
label = feature_name.copy()
data_set = import_data_set[0:train_size]
print("data load over.")
print('building tree...')
my_tree = bp.create_decision_tree(data_set, label)
print('build tree end.')
if _DEBUG == True:
# 测试
print("test result = ", bp.single_test(my_tree, data_set[2], feature_name))
print("real result = ", data_set[2][-1])