ML学习笔记——决策树Python3代码实现与解析(ID3、C4.5)

ML学习笔记——决策树Python3代码实现与解析(ID3、C4.5)

本文以经典的西瓜问题为例,决策树原理详见文章 ML学习笔记——决策树

ID3

代码详解

代码来源:https://blog.csdn.net/leaf_zizi/article/details/82848682

数据集

青绿 蜷缩 浊响 清晰 凹陷 硬滑 是 
乌黑 蜷缩 沉闷 清晰 凹陷 硬滑 是 
乌黑 蜷缩 浊响 清晰 凹陷 硬滑 是 
青绿 蜷缩 沉闷 清晰 凹陷 硬滑 是 
浅白 蜷缩 浊响 清晰 凹陷 硬滑 是 
青绿 稍蜷 浊响 清晰 稍凹 软粘 是 
乌黑 稍蜷 浊响 稍糊 稍凹 软粘 是 
乌黑 稍蜷 浊响 清晰 稍凹 硬滑 是 
乌黑 稍蜷 沉闷 稍糊 稍凹 硬滑 否 
青绿 硬挺 清脆 清晰 平坦 软粘 否 
浅白 硬挺 清脆 模糊 平坦 硬滑 否 
浅白 蜷缩 浊响 模糊 平坦 软粘 否 
青绿 稍蜷 浊响 稍糊 凹陷 硬滑 否 
浅白 稍蜷 沉闷 稍糊 凹陷 硬滑 否 
乌黑 稍蜷 浊响 清晰 稍凹 软粘 否 
浅白 蜷缩 浊响 模糊 平坦 硬滑 否 
青绿 蜷缩 沉闷 稍糊 稍凹 硬滑 否

ID3主代码
源代码基于python2,在python3环境下会报错,因此以下代码在源代码的基础上稍有修改并添加了详细的注释。

import numpy as np
import pandas as pd
import sklearn.tree as st
import math
import matplotlib
import os
import matplotlib.pyplot as plt

# 以下注释主要针对西瓜数据集


# 计算信息熵
# 参数 - 数据集(二维list)
# 返回值 - 传入的数据集的信息熵(浮点数)
def calcEntropy(dataSet):
    mD = len(dataSet)   # 数据集行数
    dataLabelList = [x[-1] for x in dataSet]    # 类别list(是/否)
    dataLabelSet = set(dataLabelList)   # 类别list -> 类别set(元素不重复,只有‘是’和‘否’)
    ent = 0
    for label in dataLabelSet:  # 遍历两次,一次遍历‘是’类别,一次遍历‘否’类别,遍历结束得到信息熵ent
        mDv = dataLabelList.count(label)
        prop = float(mDv) / mD
        ent = ent - prop * np.math.log(prop, 2)

    return ent


# 删除数据集包含某一特征的所有列数据,并返回经过删除的曾包含该特征的list的数据集(二维list)
# 参数
# index - 要拆分的特征的下标(整型数),代表属性
# feature - 要拆分的特征(字符串)
# 返回值 - dataSet中所有特征是feature的那一行数据中去掉该feature的数据集(二维list)
def splitDataSet(dataSet, index, feature):
    splitedDataSet = []  # 初始化已拆分的数据集splitedDataSet,类型为list
    for data in dataSet:  # 遍历数据集的每一行
        if (data[index] == feature):  # 如果是我们要拆分出来的特征feature
            sliceTmp = data[:index]  # 新建一个列表sliceTmp,先取这一行feature前面的所有数据
            sliceTmp.extend(data[index + 1:])  # 再取这一行feature后面的所有数据
            splitedDataSet.append(sliceTmp)  # 将sliceTmp加到splitedDataSet中去
    return splitedDataSet


# 选择最优划分属性(信息增益法)
# 参数 - 数据集(二维list)
# 返回值 - 最好的特征的下标(整型数)
def chooseBestFeature(dataSet):
    entD = calcEntropy(dataSet)  # 计算信息熵
    mD = len(dataSet)  # 数据集的行数
    featureNumber = len(dataSet[0]) - 1  # 数据集的特征数,减1是因为数据集最后一列是类型而不是特征
    maxGain = -100  # 初始化最大信息增益
    maxIndex = -1   # 初始化最大信息增益对应的列号(属性)
    for i in range(featureNumber):  # 遍历数据集的所有列(属性),i:0,1,...,featureNumber-1
        entDCopy = entD
        featureI = [x[i] for x in dataSet]  # 特征向量(第i列,即第i个属性)
        featureSet = set(featureI)  # 不含重复元素的特征集合(第i列,即第i个属性)
        for feature in featureSet:  # 遍历该特征集合中的特征feature
            splitedDataSet = splitDataSet(dataSet, i, feature)  # 含feature的数据集,但不包含feature的一列
            mDv = len(splitedDataSet)  # 行数,即含feature的数据个数
            entDCopy = entDCopy - float(mDv) / mD * calcEntropy(splitedDataSet)
            #  为什么这里可以直接计算splitedDataSet的信息熵?
            #  因为计算信息熵时只需要数据集中最后一列的数据(类型),有无含feature的一列对计算结果无影响
        if (maxIndex == -1):
            maxGain = entDCopy
            maxIndex = i
        elif (maxGain < entDCopy):  # 找到了更大的信息增益,即找到了更优的划分属性
            maxGain = entDCopy
            maxIndex = i

    return maxIndex


# 返回数量最多的类别
# 参数 - 类别向量(一维list)
# 返回值 - 类别(字符串)
def mainLabel(labelList):
    labelRec = labelList[0]
    maxLabelCount = -1  # 数量最多的类别的类别数
    labelSet = set(labelList)  # 无重复元素的类别集合
    for label in labelSet:  # 遍历该集合中的类别
        if (labelList.count(label) > maxLabelCount):  # 如果传入的类别向量中当前类别的数量比maxLabelCount大,更新数量最多的类别
            maxLabelCount = labelList.count(label)
            labelRec = label
    return labelRec


# 构建决策树
def createFullDecisionTree(dataSet, featureNames, featureNamesSet, labelListParent):
    labelList = [x[-1] for x in dataSet]  # 取数据集最后一列为类别向量
    if (len(dataSet) == 0):  # 数据集为空集
        return mainLabel(labelListParent)
    if (len(dataSet[0]) == 1):  # 没有可划分的属性了
        return mainLabel(labelList)  # 选出最多的类别作为该数据集的标签
    if (labelList.count(labelList[0]) == len(labelList)):  # 全部都属于同一个类别
        return labelList[0]

    bestFeatureIndex = chooseBestFeature(dataSet)  # 最好的属性的下标
    bestFeatureName = featureNames.pop(bestFeatureIndex)  # 从featureNames中删去bestFeature,并返回bestFeature
    myTree = {
   bestFeatureName: {
   }}  # 用字典构建决策树
    featureList = featureNamesSet.pop(bestFeatureIndex)  # 从featureNamesSet中删去bestFeature对应的子属性集,并返回这个最好的属性的子属性集
    featureSet = set(featureList)  # 转换成set类型
    for feature in featureSet:
        featureNamesNext = featureNames[:]  # 这里的featureNames已经删去了最好的属性
        featureNamesSetNext = featureNamesSet[:][:]  # 这里的featureNamesSet也已经删去了最好的属性
        splitedDataSet = splitDataSet(dataSet, bestFeatureIndex, feature)
        # 递归建树
        myTree[bestFeatureName][feature] = createFullDecisionTree(splitedDataSet, featureNamesNext, featureNamesSetNext, labelList)
    return myTree


# 返回值
# dataSet 数据集
# featureNames 属性集(一维list)
# featureNamesSet 一个二维list,其中每一个元素是对应的属性的所有子属性的集合list
def readWatermelonDataSet():
    fr = open(r'C:\Users\静如止水\Desktop\ID3\data.txt', encoding='utf-8')
    dataSet = [inst.strip().split(' ') for inst in fr.readlines()]
    # print("dataSet =")
    # print(dataSet)
    # print('\n')
    featureNames = ['色泽', '根蒂', '敲击', '纹理', '脐部', '触感']
    # 获取featureNamesSet
    featureNamesSet = []
    for i in range(len(dataSet[0]) - 1):
        col = [x[i] for x in dataSet]
        colSet = set(col)
        featureNamesSet.append(list(colSet))
    return dataSet, featureNames, featureNamesSet

基于matplotlib的可视化代码

以下代码与源代码一致,无删改。

# 能够显示中文
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.serif'] = ['SimHei']

# 分叉节点,也就是决策节点
decisionNode = dict(boxstyle="sawtooth", fc="0.8")

# 叶子节点
leafNode = dict(boxstyle="round4", fc="0.8")

# 箭头样式
arrow_args = dict(arrowstyle="<-")


def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    """
    绘制一个节点
    :param nodeTxt: 描述该节点的文本信息
    :param centerPt: 文本的坐标
    :param parentPt: 点的坐标,这里也是指父节点的坐标
    :param nodeType: 节点类型,分为叶子节点和决策节点
    :return:
    """
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)


def getNumLeafs(myTree):
    """
    获取叶节点的数目
    :param myTree:
    :return:
    """
    # 统计叶子节点的总数
    numLeafs = 0

    # 得到当前第一个key,也就是根节点
    firstStr = list(myTree.keys())[0]

    # 得到第一个key对应的内容
    secondDict = myTree[firstStr]

    # 递归遍历叶子节点
    for key in secondDict.keys():
        # 如果key对应的是一个字典,就递归调用
        if type(secondDict[key]).__name__ == 'dict':
            numLeafs += getNumLeafs(secondDict[key])
        # 不是的话,说明此时是一个叶子节点
        else:
            numLeafs += 1
    return numLeafs


def getTreeDepth(myTree):
    """
    得到数的深度层数
    :param myTree:
    :return:
    """
    # 用来保存最大层数
    maxDepth = 0

    # 得到根节点
    firstStr = list(myTree.keys())[0]

    # 得到key对应的内容
    secondDic = myTree[firstStr]

    # 遍历所有子节点
    for key in secondDic.keys():
        # 如果该节点是字典,就递归调用
        if type(secondDic[key]).__name__ == 'dict':
            # 子节点的深度加1
            thisDepth = 1 + getTreeDepth(secondDic[key])

        # 说明此时是叶子节点
        else:
            thisDepth = 1

        # 替换最大层数
        if thisDepth > maxDepth:
            maxDepth = thisDepth

    return maxDepth


def plotMidText(cntrPt, parentPt, txtString):
    """
    计算出父节点和子节点的中间位置,填充信息
    :param cntrPt: 子节点坐标
    :param parentPt: 父节点坐标
    :param txtString: 填充的文本信息
    :return:
    """
    # 计算x轴的中间位置
    xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0]
    # 计算y轴的中间位置
    yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1]
    # 进行绘制
    createPlot.ax1.text(xMid, yMid, txtString)


def plotTree(myTree, parentPt, nodeTxt):
    """
    绘制出树的所有节点,递归绘制
    :param myTree: 树
    :param parentPt: 父节点的坐标
    :param nodeTxt: 节点的文本信息
    :return:
    """
    # 计算叶子节点数
    numLeafs = getNumLeafs(myTree=myTree)

    # 计算树的深度
    depth = getTreeDepth(myTree=myTree)

    # 得到根节点的信息内容
    firstStr = list(myTree.keys())[0]

    # 计算出当前根节点在所有子节点的中间坐标,也就是当前x轴的偏移量加上计算出来的根节点的中心位置作为x轴(比如说第一次:初始的x偏移量为:-1/2W,计算出来的根节点中心位置为:(1+W)/2W,相加得到:1/2),当前y轴偏移量作为y轴
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff)

    # 绘制该节点与父节点的联系
    plotMidText(cntrPt, parentPt, nodeTxt)

    # 绘制该节点
    plotNode(firstStr, cntrPt, parentPt, decisionNode)

    # 得到当前根节点对应的子树
    secondDict = myTree[firstStr]

    # 计算出新的y轴偏移量,向下移动1/D,也就是下一层的绘制y轴
    plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD

    # 循环遍历所有的key
    for key in secondDict.keys():
        # 如果当前的key是字典的话,代表还有子树,则递归遍历
        
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值