优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征的数据
缺点:可能会产生过度匹配的问题
使用数据类型:数值型和标称型。
#计算数据集的熵
from math import log #导入log函数
def calcShannonEnt(dataSet):
numEntries = len (dataSet) #得到数据集的行数
# print(numEntries)
labelCounts = {}
for featVec in dataSet: #提取数据集每一行的特征向量
currentLabel = featVec[-1] #获取特征向量的最后一列标签
print(currentLabel)
if currentLabel not in labelCounts.keys(): #检测该标签是否存在与字典的关键字
labelCounts[currentLabel] = 0 #将当前标签和0键值存入字典中
print(currentLabel)
labelCounts[currentLabel] +=1 #若存在,则对当前标签对应的键值加1
print(labelCounts) #print函数仅作为测试用,可删去
Ent = 0 #初始化熵
for key in labelCounts: #统计数据集中所有的分类
prob = float(labelCounts[key])/numEntries #计算各个类别的概率
Ent-=prob*log(prob,2) #各个类别的信息期望值
return Ent
#创建数据集,以供测试
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no'],]
labels = ['no surfacing','flippers']
return dataSet,labels
注:numPy中数组是下标是从0行0列开始的。如【0】【-1】则表示第0行的最后一列的元素
2,划分数据集,度量划分数据集的熵,以便判断当前是否正确划分数据集。
#按照给定特征划分数据集
def splitDataSet(dataSet,axis,value):
retDataSet = [] #python语言传递参数列表时,传递的是列表的引用
for featVec in dataSet: #提取数据集的每一行的特征向量
if featVec[axis] == value: #针对axis特征的取值将数据集划分为不同的分支
#如果该特征的值为value,将特征向量的0-axis-1列存入列表reducedFeatVec
reducedFeatVec = featVec[:axis] #
reducedFeatVec.extend(featVec[axis+1:]) #将特征向量的axis+1~最后一列存入元素
retDataSet.append(reducedFeatVec) #将每一行得到的列表整合到一起,成为一个大列表
return retDataSet
注:extend()和append()函数的区别,
比如:a=[1,2,3],b=[4,5,6]
那么a.append(b)的结果为:[1,2,3,[4,5,6]], 即使用append()函数会在列表末尾添加人新的列表对象b而a.extend(b)的结果为:[1,2,3,4,5,6],即使用extend()函数
3.遍历整个数据集,找到最好的特征划分方式。
#选择最好的数据集划分方式
def choseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1 #得到数据集的列再减一就为特征数目
baseEntropy = calcShannonEnt(dataSet) #计算未进行划分的数据集的信息熵
bestInfoGain = 0;bestFeature = -1 #初始化最优信息增益,最优特征
for i in range(numFeatures):
featList = [example[i] for example in dataSet] #得到特征i的所有特征值(即取每一列的所有值)
uniqueVals = set(featList) #利用set集合的性质,元素的唯一性,得到特征i的所有可能值
newEntropy = 0
for value in uniqueVals: #for循环的目的是以特征i的取值进行划分数据集
subDataSet = splitDataSet(dataSet,i,value) #获取特征值value分支包含的数据集
prob = len(subDataSet)/float(len(dataSet)) #计算特征i取值为value时分子集的比例
newEntropy += prob * calcShannonEnt(subDataSet) #计算占比*子集信息熵,并累加得到总熵
infoGain = baseEntropy - newEntropy #获得信息增益的公式(见公式有助于理解)
if(infoGain > bestInfoGain): #为了得到最大信息增益
bestInfoGain = infoGain
bestFeature = i #相应的保存得到最大增益的特征i
return bestFeature
注:第7,8行代码的具体意思为:创建唯一的分类列表标签。
featList = [example[i] for example in dataSet] #得到特征i的所有特征值(即取每一列的所有值)
得到的结果为:
即提取到了每一列的值到一个列表中(example对myDat数据集每一行循环,example[i]得到每一行指定的元素)
uniqueVals = set(featList) #利用set集合的性质,元素的唯一性,得到特征i的所有可能值
4.多数表决原则,确定类标签
#当遍历完所有的特征属性后,类标签仍然不唯一(分支仍有不同标签的实例)
def majorityCnt(classList): #采用多数表决法完成分类
classCount = {} #创建类标签字典
for vote in classList: #遍历类标签列表中的每一个元素
if vote not in classCount.keys(): #如果元素不在字典中,
classCount[vote] = 0 #在字典中添加新的键值对
classCount[vote] +=1 #否则,当前键对应的值加一
sortedClassCount = sorted(classCount.items(), #对字典中键对应的值按从大到小排序
key = operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0] #返回出现次数最多的类标签
5. 创建决策树代码
#创建树的代码
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet] #获取数据集的最后一列的类标签,存入classlist列表
if classList.count(classList[0]) == len(classList):#通过count函数获取类标签列表中第一类标签的数目
return classList[0] #判断数目是否等于列表长度,相同表示所有类标签相同,属于同一类。
if len(dataSet[0]) == 1: #遍历完所有的特征属性,此时数据集列为1,即只有类标签列
return majorityCnt(classList) #多数表决原则,确定类标签
bestFeat = chooseBestFeatureToSplit(dataSet) #确定出当前最优的分类特征
bestFeatLabel = labels[bestFeat] #在特征标签列表中获取该特征对应的值
myTree = {bestFeatLabel:{}} #采用字典嵌套字典的方式,存储分类树的信息
subLabels = labels[:] #复制当前特征标签列表,防止改变原始列表的内容
del(subLabels[bestFeat]) #删除属性列表中当前分类数据集特征
featValues = [example[bestFeat] for example in dataSet] #获取数据集最优特征所在的列
uniqueVals = set(featValues) #采用set集合性质。获取列特征中所有的唯一取值
for value in uniqueVals: #遍历每一个唯一的特征值
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),
subLabels)
return myTree
注:在每个数据集上递归调用函数createTree(),得到的返回值插入到字典变量中,因此当函数结束时,字典中会嵌套很多代表叶子节点信息的字典数据。
我们看一行循环的第一行代码:subLabels = labels[:], 这行代码复制了类标签,并将其存储到新列表subLabels中。
这是因为python函数参数是列表类型时,是按照引用方式传递的,为了保证每次调用函数时不改变原始列表的值,使用新变量代替。
6. 测试算法
#使用决策树测试分类算法
def classify(inputTree,featLabels,testVec):#完成决策树的构造后,应用在分类任务中
firstStr = list(inputTree.keys())[0] #找到树的第一个分类特征,或者谁是跟节点
secondDict = inputTree[firstStr] #从树中得到该分类特征的分支(即跟节点下的分支)
featIndex = featLabels.index(firstStr) #根据分类特征的索引找到对应的标称型数据值(根节点对应的索引为0)
for key in secondDict.keys(): #遍历分类特征所有的取值
if testVec[featIndex] == key: #判断测试实例的第0个特征取值是否等于第第key个子节点
if type(secondDict[key]).__name__=='dict': #判断该子节点是否为字典类型
classLabel = classify(secondDict[key],featLabels,testVec)#若为字典型,则从该分支继续遍历分类
else: #如果是叶子节点,则返回节点取值
classLabel =secondDict[key]
return classLabel
注:结合实战书图3-6理解(p46)
7. 存储决策树
#使用pickle模块存储决策树
def storeTree(inputTree,filename): #存决策树
import pickle #导入pickle模块
fw = open(filename,'wb') #创建一个写的文本文件,若写'w'会报错,改为'wb'
pickle.dump(inputTree,fw) #pickle的dump函数将决策树写入文件中
fw.close() #关闭写完的文件
def grabTree(filename): #取决策树操作
import pickle
fr = open(filename,'rb') #对应于二进制写入数据,‘rb’采用二进制读出数据
return pickle.load(fr)
这里,文件的写入操作为'wb'或'wb+',表示以byte的形式写入数据,相应'rb'以byte形式读入数据
接下来,我们将通过隐形眼镜数据集构建决策树,从而预测患者需要佩戴的隐形眼镜的类型,步骤如下:
(1)收集数据:文本数据集'lenses.txt'
(2)准备数据:解析tab键分隔开的数据行
(3)分析数据:快速检查数据,确保正确地解析数据内容
(4)训练算法:构建决策树
(5)测试算法:通过构建的决策树比较准确预测出分类结果
(6)算法的分类准确类满足要求,将决策树存储下来,下次需要时读取使用
#使用决策树预测影星眼睛类型
def predictType(filename):
fr = open(filename) #打开文本数据
#将文本数据的每一行按照tab键分割,并存入lenses
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate'] #创建特征标签列表
lensesTree = createTree(lenses,lensesLabels) #创建决策树
return lensesTree