[数据挖掘笔记02] 决策树ID3算法

1.原理

问题】故事发生在100年前,一个还没有手机的时代,小明的女朋友小红想去找小明玩,但不知道小明在不在家,因为小明可能出去打球了。现在小红想知道小明的去向,她手里有过去14次去找小明玩时的数据,请帮她判断一下小明到底是在家还是去打球了。[现在的情况是[‘rainy’,‘hot’,‘high’,‘false’]]
在这里插入图片描述
这里引入两个概念:

  • 熵:表示随机变量不确定性的度量,物体内部的混乱程度。比如说学校里的选修课,可能有来自学校各个不同专业的人选修,混乱程度很高,不易分类;而专业课可能都是同一个专业的同学在上,很容易分类,纯度高,随表一块板砖拍下去砸到的就是你们专业的。计算公式为:
    在这里插入图片描述
    其中p为取集合中某个属性取到的概率。

原始数据中一共14天,9天打球,5天不打球,所以原始数据的熵为:
在这里插入图片描述

  • 信息增益:特征X使得类Y的不确定性减少程度。信息增益越大,越能减少不确定性。

ID3算法就是根据最大信息增益来选取决策树的节点。

该数据集一共有4种划分方式,如下:
在这里插入图片描述
在这里插入图片描述
接下来分别计算每种划分方式的熵,首先,对于outlook属性,有
在这里插入图片描述
outlook取sunny,overcast,rainy的概率分别是5/14,4/14,5/14,则分类后的熵值为:
在这里插入图片描述
outlook的信息增益:Gain(D, outlook) = Ent(D) - 0.693 = 0.247
同理可以计算出:
Gain(D, temperature) = 0.029
Gain(D, humility) = 0.152
Gain(D, windy) = 0.048

outlook的信息增益最大,因此选取outlook作为根节点,此时的决策树为:
在这里插入图片描述
可以看到overcast已经确定,只要outlook为overcast,小明就去打球。这样就可以排除含overcast的数据了,现在的数据集如下:
在这里插入图片描述
现在来计算sunny,sunny的数据集如下:
在这里插入图片描述
我们基于这个数据集来进行计算各种划分方式的熵,划分方式有如下几种:
在这里插入图片描述
对于temperature属性,计算熵:
在这里插入图片描述
temperature取hot,mild,cool的概率分别是2/5, 2/5,1/5,则分类后的熵值为:
在这里插入图片描述
根据前面的计算,我们知道Ent(outlook=sunny) = 0.971,则temperature的信息增益为:

Gain(outlook=sunny, temperature) = Ent(outlook=sunny) - 0.4= 0.971 - 0.4 = 0.571

同理可得humility和windy的信息增益:

Gain(outlook=sunny, humility) = 0.971
Gain(outlook=sunny, windy) = 0.971 - 0.951=0.02

humility的信息增益最大,因此选择humility作为叶子节点,这时候可以看到humility为high的决策全为no,humility为normal的决策全为yes,因此可以确定决策,此时的决策树为:
在这里插入图片描述

继续计算rainy,rainy的数据集如下:
在这里插入图片描述
我们基于这个数据集来进行计算各种划分方式的熵,划分方式有如下几种:
在这里插入图片描述
这里可以直接看出windy为false的决策全为yes,windy为true的决策全为no,因此可以确定决策,选择windy作为叶子节点,此时的决策树为。
在这里插入图片描述
至此,就完成了决策树的构建,其实这就是一个不断递归的过程。我们把现在的数据带进决策树中计算,[‘rainy’,‘hot’,‘high’,‘false’],从outlook开始,outlook=rainy,走右边的节点,到达windy,windy=false,走左边的节点,结果为yes。因此,如果今天去小明的家,有很大可能见不到小明,因为他打球去了。

ID3算法的问题:

如果选用id特征来分类,每个id都是唯一的,因此每个类别里只有一类,纯洁度是最高的,信息增益最大,根据ID3算法的思想,我们要选用信息增益最大的节点来划分,那我们是不是要选择id呢?但我们都知道id属性并没有什么意义,因此在ID3算法的基础上,产生了C4.5算法,它使用信息增益率来进行计算,考虑了自身的熵。

C4.5算法
在这里插入图片描述
拿outlook特征和id特征来说,基于outlook的信息增益是0.247,基于id的信息增益是0.940
outlook自身的熵值为:
在这里插入图片描述
id自身的熵值为:
在这里插入图片描述
outlook的信息增益率:0.247 / 1.557 = 0.157
id的信息增益率:0.940 / 3.81 = 0.247

id 的信息增益率还是比outlook大,这是因为样本的数据量不够大,如果我们把数据数量乘以100,也就是说有1400个样本,各属性比例保持不变,基于id的信息增益也是不变的,这时id自身的熵值为:
在这里插入图片描述
id的信息增益率:0.940 / 10.45 = 0.090,信息增益率马上降了下来。

CART算法

CART算法使用GINI系数作为衡量指标,GINI系数的计算公式为:
在这里插入图片描述

2.代码实现

首先在项目文件夹建立一个名为treePlotter的包,我们要用它来绘制决策树。
在这里插入图片描述
在__init__.py文件里放入如下代码:

import matplotlib.pyplot as plt


"""绘制决策树的函数"""
decisionNode = dict(boxstyle="sawtooth", fc="0.8")  # 定义分支点的样式
leafNode = dict(boxstyle="round4", fc="0.8")  # 定义叶节点的样式
arrow_args = dict(arrowstyle="<-")  # 定义箭头标识样式


# 计算树的叶子节点数量
def getNumLeafs(myTree):
   numLeafs = 0
   firstStr = list(myTree.keys())[0]
   secondDict = myTree[firstStr]
   for key in secondDict.keys():
      if type(secondDict[key]).__name__ == 'dict':
         numLeafs += getNumLeafs(secondDict[key])
      else:
         numLeafs += 1
   return numLeafs


# 计算树的最大深度
def getTreeDepth(myTree):
   maxDepth = 0
   firstStr = list(myTree.keys())[0]
   secondDict = myTree[firstStr]
   for key in secondDict.keys():
      if type(secondDict[key]).__name__ == 'dict':
         thisDepth = 1 + getTreeDepth(secondDict[key])
      else:
         thisDepth = 1
      if thisDepth > maxDepth:
         maxDepth = thisDepth
   return maxDepth


# 画出节点
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
   createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', \
                           xytext=centerPt, textcoords='axes fraction', va="center", ha="center", \
                           bbox=nodeType, arrowprops=arrow_args)


# 标箭头上的文字
def plotMidText(cntrPt, parentPt, txtString):
   lens = len(txtString)
   xMid = (parentPt[0] + cntrPt[0]) / 2.0 - lens * 0.002
   yMid = (parentPt[1] + cntrPt[1]) / 2.0
   createPlot.ax1.text(xMid, yMid, txtString)


def plotTree(myTree, parentPt, nodeTxt):
   numLeafs = getNumLeafs(myTree)
   firstStr = list(myTree.keys())[0]
   cntrPt = (plotTree.x0ff + \
             (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.y0ff)
   plotMidText(cntrPt, parentPt, nodeTxt)
   plotNode(firstStr, cntrPt, parentPt, decisionNode)
   secondDict = myTree[firstStr]
   plotTree.y0ff = plotTree.y0ff - 1.0 / plotTree.totalD
   for key in secondDict.keys():
      if type(secondDict[key]).__name__ == 'dict':
         plotTree(secondDict[key], cntrPt, str(key))
      else:
         plotTree.x0ff = plotTree.x0ff + 1.0 / plotTree.totalW
         plotNode(secondDict[key], \
                  (plotTree.x0ff, plotTree.y0ff), cntrPt, leafNode)
         plotMidText((plotTree.x0ff, plotTree.y0ff) \
                     , cntrPt, str(key))
   plotTree.y0ff = plotTree.y0ff + 1.0 / plotTree.totalD


def createPlot(inTree):
   fig = plt.figure(1, facecolor='white')
   fig.clf()
   axprops = dict(xticks=[], yticks=[])
   createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
   plotTree.totalW = float(getNumLeafs(inTree))
   plotTree.totalD = float(getTreeDepth(inTree))
   plotTree.x0ff = -0.5 / plotTree.totalW
   plotTree.y0ff = 1.0
   plotTree(inTree, (0.5, 1.0), '')
   plt.show()

if __name__=='__main__':
    createPlot()

建立一个test.csv文件,放入测试数据。
在这里插入图片描述
接下来就可以写主程序了,代码如下。

from math import log
import operator
import treePlotter  #画出决策树的包

'''计算信息熵'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)   #计算数据集中的项目数
    labelCounts = {}
    
    #为所有可能的分类创建字典
    for featVec in dataSet:
        currentLabel = featVec[-1]  #取出数据集的最后一列
        if currentLabel not in labelCounts.keys():  #如果属性不在字典中
            labelCounts[currentLabel] = 0   #加入字典
        labelCounts[currentLabel] += 1  #当前属性出现次数加1
    
    #计算当前数据集的香农熵并返回
    shannonEnt = 0.0
    for key in labelCounts: #遍历字典labelCounts的键值对
        prob = float(labelCounts[key]) / numEntries  #计算每个属性出现的概率
        shannonEnt = -prob * log(prob, 2)   #熵计算公式
    return shannonEnt

'''划分数据集'''
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:  #如果数据集的目标特征值等于value
            reducedFeatVec = featVec[:axis] #抽取数据集中的目标特征值列之前的所有列
            reducedFeatVec.extend(featVec[axis+1:]) #抽取数据集中的目标特征值列之后的所有列
            retDataSet.append(reducedFeatVec)
    return retDataSet  #返回不包含第axis列且第axis列值与value相等的数据集
    
'''以信息增益最大为原则,选择最好的数据集划分方式'''
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1   #获取总特征数量
    baseEntropy = calcShannonEnt(dataSet)   #计算原始数据集熵
    bestInfoGain = 0.0  #记录信息增益
    bestFeature = -1    #和原始数据集熵差最大的划分对应的属性的索引
    
    #创建唯一的分类列表
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)  #获取该特征所有不同的值
        #计算每种划分方式的信息熵
        newEntropy = 0.0
        for value in uniqueVals:    #计算该属性划分下所有划分子集的信息熵,并叠加
            subDataSet = splitDataSet(dataSet, i, value)    #划分数据子集
            prob = len(subDataSet) / float(len(dataSet))    #计算第i列中,特征值为value的概率
            newEntropy += prob * calcShannonEnt(subDataSet) #计算以第i个属性为划分方式得到的信息熵
        infoGain = baseEntropy - newEntropy  #计算信息增益
        #选择具有最大信息增益的属性作为测试属性
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature  #返回特征的索引

'''若已经遍历了所有属性,但类别标签不唯一,采用多数表决法决定叶子节点分类'''
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
            classCount[vote] += 1
            #根据第1列的键值进行倒序排列
            sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
        return sortedClassCount[0][0]
    
'''构建决策树'''
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]  #保存数据集列表的最后一列
    #递归终止条件一:如果数据集内所有分类一致
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    #递归终止条件二:如果所有属性都划分完毕
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)   #将它们都归为一类
    bestFeat = chooseBestFeatureToSplit(dataSet)    #选择具有最大信息增益的属性作为测试属性
    bestFeatLabel = labels[bestFeat]    #测试属性对应的类别
    myTree = {bestFeatLabel:{}} #创建字典,用于记录最佳分类标签对应的类别和出现次数
    del labels[bestFeat]    #在属性列表中剔除最佳属性(删掉名字)
    featValues = [example[bestFeat] for example in dataSet] #获取最佳划分标签下的所有变量取值
    uniqueVals = set(featValues)    #唯一化
    for value in uniqueVals:    #逐行遍历变量取值集合
        subLabels = labels[:]   #复制类标记,并存储在新的列表变量中
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

'''使用决策树进行分类'''
def classify(inputTree, featLabels, testVec):
    firstStr = list(inputTree.keys())[0]    #当前树的根节点的属性名称
    secondDict = inputTree[firstStr]    #根节点的所有子节点
    featIndex = featLabels.index(firstStr)  #找到根节点特征对应的下标
    for key in secondDict.keys():
        if testVec[featIndex] == key: #获取待分类对象中当前分类的特征值
            if type(secondDict[key]).__name__ == 'dict':    #如果节点不是叶子节点,则递归
                classLabel = classify(secondDict[key], featLabels, testVec) #调用函数本身,直到得到叶子节点
            else:
                classLabel = secondDict[key]    #如果节点是叶子节点,返回当前节点分类标签
    return classLabel


def main():
    fr = open('test.csv')   #打开测试数据集文件
    test = [inst.strip().split(',')[0:] for inst in fr.readlines()] #读取数据
    testLabels = ['outlook','temperature','humidity','windy','play']    #输入所有属性变量
    testTree = createTree(test, testLabels) #生成此数据集的决策树
    #createTree函数对属性做了改变,因此需要再次输入
    testLabels = ['outlook','temperature','humidity','windy','play']
    #调用treePlotter文件的CreatePlot函数绘制决策树
    treePlotter.createPlot(testTree)
    decision = classify(testTree, testLabels, ['rainy','hot','high','false'])
    print("\n" * 5,"决策为:", decision)
    
main()

运行结果
在这里插入图片描述
可以看到,运行结果与我们手工计算的结果一致。

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值