2022年 11月 3日

python编程实现决策树算法

最近布置了个课堂作业,用python实现决策树算法 。整了几天勉勉强强画出了棵歪脖子树,记录一下。

大体思路:

1.创建决策树My_Decision_Tree类,类函数__init__()初始化参数、fit()进行决策树模型训练、predict()进行预测、evaluate()进行模型评估、save_model()保存模型(csv格式)、load_model()加载模型、show_tree()使用Pillow库绘制决策树以及其他一些封装的功能函数;

2.最佳划分点的度量通常有Gini值、Entropy、Classification error等,本程序采用Gini值,计算方法如下: 

构建决策数模型所用到的功能函数(gini值计算相关)说明: 

①get_col_gini(self,threshold_point, value_series, label_series):计算一列数据在某一阈值划分点下的gini_split值;

②get_best_point(self,value_series, label_series):对连续型数据进行大小排序,分别计算切分点及其对应的gini_split值,找到使得gini_split值最小的最佳切分点;

③get_best_column(self,data,label_series):遍历数据表中的属性列,计算每列的最佳划分点和gini_split值,并对比找到最适合划分的一列及其对应的划分点;

3.决策树构建(训练)的逻辑: 

①程序开始以root为父节点,先找到6列属性(剩余属性)中最适合划分的列和划分点,并将其划分为两个子节点,记录判断条件和此节点位置。删除子节点中的该列属性以避免最终决策树模型倾向于某个属性,利用剩余的属性继续构造决策树;

②判断各个子节点中的标签是否相同,如果为同一类则移到叶节点中,否则将此节点更新到下个流程中的父节点中;

③一直循环以上过程,当父节点为空时结束训练,如果所有子节点都为叶节点时则决策树完美将数据分类;当然也会出现所有属性都用完但子节点中标签依旧不唯一的情况,这时以该节点中个数较多的标签类作为分类结果,终止模型构建。

  1. # -*- coding: utf-8 -*-
  2. # @Version: Python 3.8.2
  3. # @Author: 707
  4. # @Use: 决策树算法编写
  5. import numpy as np
  6. import pandas as pd
  7. from sklearn.model_selection import train_test_split #数据集划分
  8. #采用Pillow可视化决策树
  9. from PIL import Image
  10. from PIL import ImageDraw
  11. from PIL import ImageFont
  12. ###决策树框架###
  13. class My_Decision_Tree(object):
  14. '''决策树框架'''
  15. def __init__(self, arg=None):
  16. ##初始化类参数
  17. self.arg = arg
  18. #存放决策树中的各层判断条件
  19. self.decision_df=pd.DataFrame(columns=['parent_depth','parent_pos','this_depth','this_pos','column','point','label'])
  20. self.Parent_node_list=[] #存放父节点和子节点的DataFrame
  21. self.Child_node_list=[]
  22. self.leaf_list=[] #存放划分好的叶节点
  23. self.branch_list=[] #存放未能划分出来的节点
  24. def fit(self,x_train,y_train):
  25. '''传入训练集和标签构造决策树'''
  26. '''
  27. 程序逻辑:
  28. (1)程序开始以root为父节点,先找到6列属性(剩余属性)中最适合划分的列和划分点,并将其划分为两个子节点,
  29. 记录判断条件和此节点位置。删除子节点中的该列属性以避免最终决策树模型倾向于某个属性,利用剩余的属性继续构造决策树
  30. (2)判断各个子节点中的标签是否相同,如果为同一类则移到叶节点中,否则将此节点更新到下个流程中的父节点中
  31. (3)一直循环以上过程,当父节点为空时结束训练,如果所有子节点都为叶节点时则决策树完美将数据分类;当然也会出现所有属性都用完但子节点
  32. 中标签依旧不唯一的情况,这时以该节点中个数较多的标签类作为分类结果,终止模型构建。
  33. '''
  34. x_data=x_train.copy()
  35. if(y_train.name not in x_data.columns):
  36. x_data[y_train.name]=y_train
  37. #把第一层(原数据)放入父节点列表Parent_node_list
  38. self.Parent_node_list.append(x_data)
  39. #写入第一层的决策(跟节点)
  40. decision={'this_depth':1,'this_pos':0,'column':'root','label':'#'}
  41. self.decision_df=self.decision_df.append(decision,ignore_index=True)
  42. #开始循环计算分类节点
  43. parent_count=0 #循环的父节点数
  44. child_pos=0 #子节点的位置
  45. depth=2 #第几层节点
  46. while True:
  47. parent_node=self.Parent_node_list[parent_count]
  48. #找到第一个适合划分的列和划分点
  49. col_1,point_1=self.get_best_column(parent_node,parent_node[y_train.name])
  50. print('decision condition:',col_1,point_1)
  51. #根据条件把父节点划分为两部分
  52. Child_node1=parent_node.loc[parent_node[col_1]<=point_1]
  53. Child_node2=parent_node.loc[parent_node[col_1]>point_1]
  54. #每一部分的分类结果
  55. result=[]
  56. for child_node in [Child_node1,Child_node2]:
  57. #删除已使用过的属性
  58. del(child_node[col_1])
  59. #判断子节点标签是否为一类,是则将其放入叶节点列表中
  60. if(len(child_node[y_train.name].unique())==1):
  61. self.leaf_list.append(child_node)
  62. print('添加一个叶节点,标签类型:',child_node[y_train.name].unique()[0],'数据大小:',child_node.shape)
  63. result.append(child_node[y_train.name].unique()[0])
  64. # 判断子节点标签是否还有剩余属性可以作为分类依据,如果有则添加到子节点列表中用于后续分类
  65. elif(child_node.shape[1]!=1):
  66. self.Child_node_list.append(child_node)
  67. print('添加一个子节点,数据大小:',child_node.shape)
  68. result.append('#')
  69. #都不满足说明该节点没有分完但是分不下去了,提示错误
  70. else:
  71. self.branch_list.append(child_node)
  72. print('child_node节点已用完所有属性,仍然没分出来,剩余数据大小:')
  73. print(child_node[y_train.name].value_counts())
  74. values=child_node[y_train.name].value_counts()
  75. if(len(values)==0):
  76. replace=list(parent_node[y_train.name].value_counts().index)[0]
  77. else:
  78. replace=list(values.index)[0]
  79. print('用%s作为该条件下的预测结果' %replace)
  80. result.append(replace)
  81. #找到该父节点在该层中所对应的位置
  82. p_pos_list=self.decision_df.loc[(self.decision_df['this_depth']==depth-1)&(self.decision_df['label']=='#'),'this_pos']
  83. p_pos_list=list(p_pos_list)
  84. # print('p_pos_list:',p_pos_list)
  85. #判断完一个父节点之后,把判断条件加入decision_df中
  86. decision1={'parent_depth':depth-1,'parent_pos':p_pos_list[parent_count],'this_depth':depth,'this_pos':child_pos,
  87. 'column':col_1,'point':point_1,'label':result[0]}
  88. decision2={'parent_depth':depth-1,'parent_pos':p_pos_list[parent_count],'this_depth':depth,'this_pos':child_pos+1,
  89. 'column':col_1,'point':point_1,'label':result[1]}
  90. self.decision_df=self.decision_df.append([decision1,decision2],ignore_index=True)
  91. #当遍历完父节点列表所有值后,将子节点更新为父节点
  92. child_pos+=2
  93. parent_count+=1
  94. if(parent_count==len(self.Parent_node_list)):
  95. parent_count=0
  96. child_pos=0
  97. depth+=1
  98. print('该层决策结束,进行下一层决策\n')
  99. self.Parent_node_list=self.Child_node_list.copy()
  100. self.Child_node_list.clear()
  101. print('更新parent_node_list,大小:%d' %len(self.Parent_node_list))
  102. #判断父节点列表中是否还有未分类的节点,如果没有则表示已经全部分好,结束训练
  103. if(len(self.Parent_node_list)==0):
  104. print('决策树构建完成')
  105. #显示构建好的决策树:判断条件及结果(叶节点)
  106. print(self.decision_df)
  107. break
  108. def predict(self,x_test):
  109. '''输入测试数据进行决策判断'''
  110. y_predict=list()
  111. if(type(x_test)==pd.core.series.Series):
  112. pred=self.get_ylabel(x_test)
  113. y_predict.append(pred)
  114. else:
  115. for index,row in x_test.iterrows():
  116. pred=self.get_ylabel(row)
  117. y_predict.append(pred)
  118. y_predict=np.array(y_predict,dtype=str)
  119. return y_predict
  120. def evaluate(self,x_test,y_test):
  121. '''输入测试集和标签评估决策树准确性,返回acc'''
  122. y_true=np.array(y_test,dtype=str)
  123. y_pred=self.predict(x_test)
  124. # print(y_pred)
  125. # print(y_true)
  126. label_list=list(self.decision_df['label'].unique())
  127. label_list.remove('#')
  128. label_list=np.array(label_list,dtype=str) #类型转换
  129. #创建混淆矩阵(index为true,columns为pred)
  130. confusion_matrix=pd.DataFrame(data=0,columns=label_list,index=label_list)
  131. for i in range(len(y_true)):
  132. confusion_matrix.loc[y_true[i],y_pred[i]]+=1
  133. print('混淆矩阵:')
  134. print(confusion_matrix)
  135. #计算准确率
  136. acc=0
  137. for i in range(len(label_list)):
  138. acc+=confusion_matrix.iloc[i,i]
  139. acc/=len(y_true)
  140. print('acc:%.5f' %acc)
  141. return acc
  142. def save_model(self,path):
  143. '''以csv格式保存模型'''
  144. self.decision_df.to_csv(path,index=False)
  145. def load_model(self,path):
  146. '''以csv格式读取模型'''
  147. self.decision_df=pd.read_csv(path)
  148. def get_col_gini(self,threshold_point, value_series, label_series):
  149. '''Gini值计算函数'''
  150. # 将输入进行重组
  151. df_input = pd.DataFrame()
  152. df_input['value'] = value_series
  153. df_input['label'] = label_series
  154. # print(df_input)
  155. # 设计Gini值的计算表格
  156. label_cols = label_series.value_counts()
  157. df_gini = pd.DataFrame(columns=['node1', 'node2'], index=label_cols.index)
  158. for c in label_cols.index:
  159. df_c = df_input.loc[df_input['label'] == c]
  160. df_gini.loc[c, 'node1'] = df_c.loc[df_c['value']<= threshold_point].shape[0]
  161. df_gini.loc[c, 'node2'] = df_c.loc[df_c['value']> threshold_point].shape[0]
  162. #计算node1、node2节点gini值中和的部分
  163. sum_n1=df_gini['node1'].sum()
  164. sum_n2=df_gini['node2'].sum()
  165. # print(df_gini)
  166. # 计算node节点gini值
  167. gini_n1=gini_n2=0
  168. if(sum_n1==0):
  169. for c in label_cols.index:
  170. gini_n2+=(df_gini.loc[c,'node2']/sum_n2)**2
  171. elif(sum_n2==0):
  172. for c in label_cols.index:
  173. gini_n1+=(df_gini.loc[c,'node1']/sum_n1)**2
  174. else:
  175. for c in label_cols.index:
  176. gini_n1+=(df_gini.loc[c,'node1']/sum_n1)**2
  177. gini_n2+=(df_gini.loc[c,'node2']/sum_n2)**2
  178. gini_n1 = 1-gini_n1
  179. gini_n2 = 1-gini_n2
  180. #计算gini_split
  181. gini_split=sum_n1/(sum_n1+sum_n2)*gini_n1 +sum_n2/(sum_n1+sum_n2)*gini_n2
  182. # print("point:%f,gini_split:%f" %(threshold_point,gini_split))
  183. return gini_split
  184. def get_best_point(self,value_series, label_series):
  185. '''找到一列属性中最适合划分(gini值最小)的点'''
  186. value_array=np.array(value_series)
  187. value_array=np.sort(value_array)
  188. df_point = pd.DataFrame(columns=['point', 'gini_value'])
  189. # 循环属性值列,计算划分点及其gini值并添加至df_point数据表中
  190. for i in range(len(value_array) + 1):
  191. if(i == 0):
  192. point = value_array[i] - 1
  193. elif(i == len(value_array)):
  194. point = value_array[i - 1]
  195. else:
  196. point = 0.5 * (value_array[i] + value_array[i - 1])
  197. gini = self.get_col_gini(point, value_series, label_series)
  198. s = pd.Series(data={'point': point, 'gini_value': gini})
  199. df_point.loc[i] = s
  200. df_point.sort_values(by='gini_value', inplace=True)
  201. best_point = df_point.iloc[0, 0]
  202. best_gini = df_point.iloc[0,1]
  203. # print("best point for column '%s':%f" %(value_series.name,best_point))
  204. # print(df_point)
  205. return best_point,best_gini
  206. def get_best_column(self,data,label_series):
  207. '''遍历data中的属性列,计算其最佳划分点及gini值,找出最适合划分的一列和划分点'''
  208. x_data=data.copy()
  209. if(label_series.name in x_data.columns):
  210. del(x_data[label_series.name])
  211. gini_columns=pd.DataFrame(columns=['point','gini'],index=x_data.columns)
  212. for col_name in x_data.columns:
  213. point,gini=self.get_best_point(x_data[col_name],label_series)
  214. s=pd.Series({'point':point,'gini':gini})
  215. gini_columns.loc[col_name]=[point,gini]
  216. # gini_columns=gini_columns.append(s,ignore_index=True) #append会更改索引
  217. gini_columns.sort_values(by='gini',inplace=True)
  218. # print(gini_columns)
  219. best_col=gini_columns.index[0]
  220. best_point=gini_columns.iloc[0,0]
  221. return best_col,best_point
  222. def get_ylabel(self,x_series):
  223. '''计算一行x数据(Series)对应的标签'''
  224. model=self.decision_df
  225. y_pred='#'
  226. x_index=1
  227. parent_index=[]
  228. child_df=pd.DataFrame()
  229. # for i in range(1):
  230. while (y_pred=='#'):
  231. #判断条件
  232. condition=[model.loc[x_index,'column'],model.loc[x_index,'point']]
  233. if(x_series[condition[0]]>condition[1]):
  234. x_index+=1
  235. # print('%s>%f' %(condition[0],condition[1]))
  236. # else:
  237. # print('%s<=%f' %(condition[0],condition[1]))
  238. y_pred=model.loc[x_index,'label']
  239. #更新父节点索引并找到其子节点
  240. parent_index=[model.loc[x_index,'this_depth'],model.loc[x_index,'this_pos']]
  241. child_df=model.loc[(model['parent_depth']==parent_index[0])&(model['parent_pos']==parent_index[1])]
  242. #找到标签时结束
  243. if(child_df.shape[0]!=0):
  244. x_index=list(child_df.index)[0]
  245. # print('跳到第%d行继续判断' %x_index)
  246. # else:
  247. # print('预测结束')
  248. # print('pred:',y_pred)
  249. return y_pred
  250. def show_tree(self):
  251. '''将决策树进行可视化'''
  252. def add_text(im_draw,text_str,xy,multiline=1):
  253. '''在绘图对象的某个位置添加文字'''
  254. #设置大小
  255. font_h,font_w=25,14
  256. font_h*=multiline
  257. text_len=round(len(text_str)/multiline)
  258. font=ImageFont.truetype(font='simsun.ttc',size=20)
  259. im_draw.text(xy=(xy[0]-font_w*3,xy[1]),text=text_str,font=font,fill='black',align='center')
  260. #绘制矩形
  261. # im_draw.rectangle(xy=(xy[0],xy[1],xy[0]+font_w*text_len,xy[1]+font_h),outline='black',width=2)
  262. interval_x,interval_y=60,80
  263. model=self.decision_df.copy()
  264. model['x_pos']=model['this_pos']
  265. model['y_pos']=(model['this_depth']-1)*interval_y
  266. model['text']='text'
  267. max_depth=model.iloc[-1,2]
  268. #创建图像
  269. img_w,img_h=1500,600
  270. tree_img=Image.new(mode='RGB',size=(img_w,img_h),color='white')
  271. draw=ImageDraw.Draw(tree_img) #创建绘图对象
  272. parent_pos=[]
  273. parent_x_pos=0
  274. x_pos=0
  275. for x_index in model.index:
  276. text=model.loc[x_index,'column']
  277. if (str(model.loc[x_index,'point']) == 'nan'):
  278. x_pos=img_w/4
  279. else:
  280. #跟新text内容和x位置
  281. model.loc[x_index,'x_pos']=x_pos
  282. parent_pos=[model.loc[x_index,'parent_depth'],model.loc[x_index,'parent_pos']]
  283. parent_x_pos=model.loc[(model['this_depth']==parent_pos[0])&(model['this_pos']==parent_pos[1]),'x_pos']
  284. depth=model.loc[x_index,'this_depth']-1
  285. if(model.loc[x_index,'this_pos']%2==0):
  286. text+='\n<='+('%.3f' %model.loc[x_index,'point'])
  287. x_pos=parent_x_pos-interval_x*np.sqrt(max_depth-depth)
  288. else:
  289. text+='\n>'+('%.3f' %model.loc[x_index,'point'])
  290. x_pos=parent_x_pos+interval_x*np.sqrt(max_depth-depth)
  291. x_pos=x_pos.iloc[0]
  292. if(model.loc[x_index,'label'] !='#'):
  293. text+='\nClass:'+str(model.loc[x_index,'label'])
  294. #将文字和位置添加到
  295. model.loc[x_index,'text']=text
  296. model.loc[x_index,'x_pos']=x_pos
  297. # 调整节点横坐标位置
  298. gap=140
  299. for depth in model['this_depth'].unique():
  300. if(depth!=1):
  301. same_depth=model.loc[model['this_depth']==depth]
  302. for x_index in same_depth.index[:-1]:
  303. if(x_index==same_depth.index[0]):
  304. if((model.loc[same_depth.index[0],'x_pos']-model.loc[same_depth.index[-1],'x_pos'])<gap*len(same_depth.index)):
  305. #如果整体太挤,整层先往左移一段
  306. for i in same_depth.index:
  307. model.loc[i,'x_pos']-=gap*len(same_depth.index)/8
  308. #如果相邻两个靠太近,右边的往右移一点
  309. if((model.loc[x_index+1,'x_pos']-model.loc[x_index,'x_pos'])<gap):
  310. model.loc[x_index+1,'x_pos']=model.loc[x_index,'x_pos']+gap
  311. # model.loc[x_index,'x_pos']-=gap/2
  312. #绘制文字和线
  313. this_img_pos=[]
  314. parent_img_pos=[]
  315. for x_index in model.index:
  316. #绘制直线
  317. if(x_index !=0):
  318. this_img_pos=[model.loc[x_index,'x_pos'],model.loc[x_index,'y_pos']]
  319. parent_pos=[model.loc[x_index,'parent_depth'],model.loc[x_index,'parent_pos']]
  320. parent_img_pos=model.loc[(model['this_depth']==parent_pos[0])&(model['this_pos']==parent_pos[1]),['x_pos','y_pos']]
  321. parent_img_pos=[parent_img_pos.iloc[0,0],parent_img_pos.iloc[0,1]]
  322. draw.line(xy=(parent_img_pos[0],parent_img_pos[1],this_img_pos[0],this_img_pos[1]),fill='gray',width=1)
  323. #添加文字
  324. this_pos=(model.loc[x_index,'x_pos'],model.loc[x_index,'y_pos'])
  325. text=model.loc[x_index,'text']
  326. add_text(im_draw=draw,text_str=text,xy=this_pos)
  327. #显示图片
  328. tree_img.show()
  329. if __name__ == '__main__':
  330. # 读取文件
  331. data = pd.read_csv('./paras_labels.csv')
  332. #数据按8:2进行训练集、测试集切分
  333. x_train,x_test,y_train,y_test=train_test_split(data,data['label'],test_size=0.2,random_state=7)
  334. ds_tree=My_Decision_Tree()
  335. ds_tree.fit(x_train,y_train)
  336. # ds_tree.save_model('my_decision_tree%d.csv' %e)
  337. # ds_tree.load_model('my_decision_tree%d.csv' %e)
  338. # print(ds_tree.decision_df)
  339. print('训练集评估模型:')
  340. ds_tree.evaluate(x_train,y_train)
  341. print('测试集评估模型:')
  342. ds_tree.evaluate(x_test,y_test)
  343. ds_tree.show_tree()

结果:

用我的数据跑了一下,成功长出一棵歪脖子树,nice! 

 代码能力有限,有错误的地方欢迎大佬们交流,批评指正[狗头抱拳]