思路:
第一步:定义函数——计算熵:
输入:某个样本集合的因变量数据,输出计算出的熵
第二步:定义函数——根据特征分割样本集合
输入:总样本集合df、某个特征,输出划分后的各个样本子集sub_df
第三步:定义函数——选择最佳特征
输入:样本集合、特征集合,选出能够最大化gain的最佳特征,输出最佳特征、gain值、用第二步得到的分割结果
第四步:定义class——递归生成决策树
核心函数:生成决策树
1.根据第三步函数进行
2.结束条件:输出最佳特征为None
3.递归部分:对每一个子集做递归
其他函数:定义节点——联系二叉树结构
1.读入数据
#### 1.读入数据
import os
import pandas as pd
import numpy as np
from math import log # 计算信息熵要用
## 将代码块运行结果全部输出,而不是只输出最后的,适用于全文
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
## 分类变量——转化为虚拟变量
os.chdir("/Users/mac/Desktop/快乐研一/python/机器学习python实现")
df = pd.read_csv("example_data.csv")
df.head()
humility | outlook | temp | windy | play | |
---|---|---|---|---|---|
0 | high | sunny | hot | False | no |
1 | high | sunny | hot | True | no |
2 | high | overcast | hot | False | yes |
3 | high | rainy | mild | False | yes |
4 | normal | rainy | cool | False | yes |
2.定义计算熵的函数
#### 2.定义计算熵的函数
# 前置问题
tmp_y = df['play'].tolist() # 转化为list,方便后面迭代
set(tmp_y) # 因变量的类别
[tmp_y.count(i) for i in set(tmp_y)] # 各类别的样本比例
# 正式定义函数计算信息熵
def information(data_y):
# 信息熵 = -sum(p*log2p)
data_y = data_y.tolist()
probs = [data_y.count(i)/len(data_y) for i in set(data_y)]
entropy = [prob*log(prob,2) for prob in probs]
sum_entropy = -sum(entropy)
return sum_entropy
# 测试
information(df['play'])
{'no', 'yes'}
[5, 9]
0.9402859586706309
3.根据特征分割样本
#### 3.根据特征分割样本
# 前置问题1——怎么subset?
df[df['outlook']=="sunny"] # 选择出outlook变量等于sunny的样本子集
# 前置问题2——怎么输出多个子集——字典{key:df}
pd.DataFrame # 定义一个空的df
aa = {types: pd.DataFrame for types in set(df['outlook'])} # 定义一个空的字典用于存放所有子集
aa
aa.keys()
# 正式定义函数
def split_df(data,col):
'''
输入:data——df数据集,col——列名,根据col这一列的取值来分割data
输出:字典,每一个key是col取值,对应value是子集
'''
types = set(data[col].tolist())
sub_df = {i:pd.DataFrame for i in types} # 定义一个存放子集集合的空字典
for key in sub_df.keys():
sub_df[key] = data[data[col]==key] # 把对应的子集填入空字典
return sub_df
# 测试
sub_df = split_df(df,'temp')
sub_df['mild']['play']
humility | outlook | temp | windy | play | |
---|---|---|---|---|---|
0 | high | sunny | hot | False | no |
1 | high | sunny | hot | True | no |
7 | high | sunny | mild | False | no |
8 | normal | sunny | cool | False | yes |
10 | normal | sunny | mild | True | yes |
pandas.core.frame.DataFrame
{'overcast': pandas.core.frame.DataFrame,
'rainy': pandas.core.frame.DataFrame,
'sunny': pandas.core.frame.DataFrame}
dict_keys(['overcast', 'rainy', 'sunny'])
3 yes
7 no
9 yes
10 yes
11 yes
13 no
Name: play, dtype: object
4.选择最佳特征
#### 选择最佳特征
# 前置知识——给出所有自变量的colname
[col for col in df.columns if col!='play']
# 正式定义函数
def choose_col(data,col_y):
'''
输入:样本集合、特征集合,从各个特征中选出能够最大化gain的最佳特征
设输出的初始值max_gain = 0(gain 的取值范围为0~logn)
for每一个特征:
调用split_df分割数据,for 分割后的每一个子集,调用information 计算条件熵,再按照样本比例加权求和
差值计算信息增益,如果大于前一个,更新输出值
输出:最佳特征、gain值、用第二步得到的分割结果
'''
entroy_D = information(data[col_y]) # 计算全集上的信息熵
colnames_x = [col for col in data.columns if col!=col_y] # 自变量的列名
max_gain = -1 # 设定gain初始值
max_col = None
max_df = None
for col_x in colnames_x: # 对于所有特征
sub_df = split_df(data,col_x) # 按照某个特征分割
entroy_sub = [] # 用于保存各个子集信息熵*权重的list
for key in sub_df.keys(): # 对于每一个子集
sub_y = sub_df[key][col_y]
entroy_sub.append(information(sub_y)*len(sub_y)/len(data[col_y])) # 子集计算信息熵*权重
entroy_x = sum(entroy_sub)
gain_x = entroy_D-entroy_x
if gain_x> max_gain: # 如果信息增益增大了,更新输出值
max_gain = gain_x
max_col = col_x
max_df = sub_df
return max_gain, max_col, max_df
# 测试
choose_col(df,'play')
['humility', 'outlook', 'temp', 'windy']
(0.246749819774439,
'outlook',
{'overcast': humility outlook temp windy play
2 high overcast hot False yes
6 normal overcast cool True yes
11 high overcast mild True yes
12 normal overcast hot False yes,
'rainy': humility outlook temp windy play
3 high rainy mild False yes
4 normal rainy cool False yes
5 normal rainy cool True no
9 normal rainy mild False yes
13 high rainy mild True no,
'sunny': humility outlook temp windy play
0 high sunny hot False no
1 high sunny hot True no
7 high sunny mild False no
8 normal sunny cool False yes
10 normal sunny mild True yes})
# 前置知识1,如何定义一个有更多叉的数结构的节点,name是他自己,connections存储链接的所有子节点
class Node:
def __init__(self,name):
self.name = name
self.connections = {} # 存储所有子节点的信息
def connect(self,label,node): # 链接到子节点
self.connections[label] = node
# 测试
parent = Node('root')
child1 = Node('child1')
child2 = Node('child2')
parent.connect('A',child1)
parent.connect('B',child2)
parent.connections
# 前置知识2 核心函数:生成决策树**
# 结束条件:输出最佳特征为None
# 递归部分:对每一个子集做递归
def build(data, col_x, col_y):
# 结束条件
if len(data)==0 or len(col_x)==0:
return
# 递归
current_gain, current_col, current_df = choose_col(data[col_x+[col_y]], col_y) # 对剩余的特征空间寻找
print(current_gain, current_col, current_df.keys())
col_x_new = [_ for _ in col_x if _ not in [current_col]] # 更新剩余的特征空间
for i in current_df.keys(): # 对于每一个子集
build(current_df[i], col_x_new, col_y) # 递归调用
# 测试
build(df,['humility', 'outlook', 'temp', 'windy'],'play')
{'A': <__main__.Node at 0x116459a58>, 'B': <__main__.Node at 0x11645ceb8>}
0.246749819774439 outlook dict_keys(['overcast', 'rainy', 'sunny'])
-0.0 humility dict_keys(['high', 'normal'])
-0.0 temp dict_keys(['mild', 'hot'])
-0.0 windy dict_keys([True])
-0.0 windy dict_keys([False])
-0.0 temp dict_keys(['hot', 'cool'])
-0.0 windy dict_keys([False])
-0.0 windy dict_keys([True])
0.9709505944546686 windy dict_keys([False, True])
-0.0 humility dict_keys(['high', 'normal'])
-0.0 temp dict_keys(['mild'])
-0.0 temp dict_keys(['mild', 'cool'])
-0.0 humility dict_keys(['normal', 'high'])
-0.0 temp dict_keys(['cool'])
-0.0 temp dict_keys(['mild'])
0.9709505944546686 humility dict_keys(['high', 'normal'])
-0.0 temp dict_keys(['mild', 'hot'])
-0.0 windy dict_keys([False])
-0.0 windy dict_keys([False, True])
-0.0 temp dict_keys(['mild', 'cool'])
-0.0 windy dict_keys([True])
-0.0 windy dict_keys([False])
5.定义一个class,把build的结果存成一个tree
# 定义一个class,把build的结果存成一个tree
class ID3Tree:
class Node: # 定义节点
def __init__(self, name):
self.name = name
self.connections = {}
def connect(self, label, node): # 节点对应多个子节点
self.connections[label] = node
def __init__(self, data, label): # 需要给出data和因变量label
self.columns = data.columns
self.data = data
self.label = label
self.root = self.Node("Root") # 初始化一个根节点
def construct_tree(self):
self.construct(self.root, "", self.data, self.columns) # 输入父节点、父节点的取值、当前样本集合、剩余特征集合
def construct(self, parent_node, parent_connection_label, input_data, columns):
max_value, best_col, max_splited = choose_col(input_data[columns], self.label)
if (not best_col) or max_value==0: # 结束条件,1.没有剩余特征2.样本集合为空3.已经归于同一类
node = self.Node(input_data[self.label].iloc[0])
parent_node.connect(parent_connection_label, node) # 设为叶节点
return
node = self.Node(best_col) # 生成新节点
parent_node.connect(parent_connection_label, node)
new_columns = [col for col in columns if col != best_col] # 更新剩余特征集合
for splited_value, splited_data in max_splited.items(): # 对于新子集,递归
self.construct(node, splited_value, splited_data, new_columns)
def print_tree(self, node, tabs):
print(tabs + node.name)
for connection, child_node in node.connections.items():
print(tabs + "\t" + "(" + str(connection) + ")")
self.print_tree(child_node, tabs + "\t\t")
df
mytree = ID3Tree(df,'play')
mytree.construct_tree()
mytree.print_tree(mytree.root,"")
humility | outlook | temp | windy | play | |
---|---|---|---|---|---|
0 | high | sunny | hot | False | no |
1 | high | sunny | hot | True | no |
2 | high | overcast | hot | False | yes |
3 | high | rainy | mild | False | yes |
4 | normal | rainy | cool | False | yes |
5 | normal | rainy | cool | True | no |
6 | normal | overcast | cool | True | yes |
7 | high | sunny | mild | False | no |
8 | normal | sunny | cool | False | yes |
9 | normal | rainy | mild | False | yes |
10 | normal | sunny | mild | True | yes |
11 | high | overcast | mild | True | yes |
12 | normal | overcast | hot | False | yes |
13 | high | rainy | mild | True | no |
Root
()
outlook
(overcast)
yes
(rainy)
windy
(False)
yes
(True)
no
(sunny)
humility
(high)
no
(normal)
yes