sklearn 决策树无法处理类别特征

温故知新, 没事儿又去看看以前的code.


1. 前言

之前写了篇 利用sklearn对红酒数据集分类 . 其中, 红酒数据集是长这样的:

而西瓜数据集是长这样的:

很明显, 前者属于浮点特征型数据, 后者则是类别特征型数据.

同样, 我们利用sklearn.tree模块来进行树的构造, 结果报错

ValueError: could not convert string to float: '青绿'

它就不能处理非浮点型的数据, 我本人觉得比较扯. 那要咋办呢? 下面就介绍两种处理方法.

2. 独特编码 (One-Hot Encoding)

独热编码, 又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码, 每个状态都有它独立的寄存器位, 并且在任意时候, 其中只有一位有效.

例如, 西瓜数据中的色泽这一属性, 有 “青绿”, “浅白”, “乌黑” 三个类别,即[“青绿”, “浅白”, “乌黑”]. 若使用独热编码, “青绿” 则可表示为 [0, 0, 1], “浅白” 可表示为 [0, 1, 0], “乌黑” 可表示为 [1, 0, 1]. 如下图所示:

但它不好使的点就在于把增加了数据维度, 色泽这一属性的数据原本为一维 (17,), 经过独热编码后就成了二维的了(17,3). 当然, 我觉得还是能用吧, 它就相当于把原数据的特征细化成更多. 原本 {“色泽: 青绿, 乌黑, 浅白}, 变成了 {“青绿”: 是, 否}, {“乌黑”: 是, 否}, {“浅白”: 是, 否}.

3. 标签编码 (Label Encoding)

标签编码, 根据字符串形式的特征值在特征序列中的位置, 为其指定一个数字标签, 用于提供给基于数值算法的学习模型. 即是对不连续的数字或者文本进行编号.

还是刚才的例子, 经过标签编码后, “青绿” --> “2” , “乌黑” --> “0”, “浅白” --> “1”. 整体如下图所示: 

import pandas as pd
from sklearn import tree
import graphviz
from sklearn.preprocessing import OneHotEncoder, LabelEncoder

D = [
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '硬挺', '敲声': '清脆', '纹理': '清晰', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '硬挺', '敲声': '清脆', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
]

clf = tree.DecisionTreeClassifier(criterion="entropy", splitter="random")

data = pd.DataFrame(D)
# print(data)

LabelEncoder = LabelEncoder()
for i in range(data.shape[1]):
    data.iloc[:, i] =LabelEncoder.fit_transform(data.iloc[:, i])

# OneHotEncoder = OneHotEncoder()
# data = OneHotEncoder.fit_transform(data.iloc[:, :]).toarray()
# data = pd.DataFrame(data)
# print(data)

#
clf = clf.fit(data.iloc[:-4, :-1], data.iloc[:-4, -1])
score = clf.score(data.iloc[-4:, :-1], data.iloc[-4:, -1])
print("acc :", score)
feature_name = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
target_name = ['好瓜', '坏瓜']
dot_data = tree.export_graphviz(clf
                                , out_file=None
                                , feature_names=feature_name
                                , class_names=target_name
                                , filled=True
                                , rounded=True
                                )
graph = graphviz.Source(dot_data.replace('helvetica', '"Microsoft YaHei"'), encoding='utf-8')
graph.view("tree")

得到的树: 

可以看到, sklearn将其当作浮点型特征进行了处理. 虽然4个测试样本的准确率大约维持在75%, 但我觉得还是 “变了味”.

4. 小结

1. 我们在调用各种库的时候, 同时也需要了解其底层的构造.

2. 独热编码和标签编码都是挺不错的数据处理方式, 但要注意到, 它们都不能对像 “大小”、“长短” 等带有程度意义的特征属性进行处理.

5. 补充

我在网上找到了一个能够对字符串型特征进行处理的决策树代码, 供大家借鉴学习. “鱼和熊掌不能兼得”, 它也不能处理浮点型特征数据.

from random import choice
from collections import Counter
import math

# 定义数据集
D = [
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '硬滑', '好瓜': '是'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '硬挺', '敲声': '清脆', '纹理': '清晰', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '硬挺', '敲声': '清脆', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '否'},
    {'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
    {'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
]


# ==========
# 决策树生成类
# ==========
class DecisionTree:
    def __init__(self, D, label, chooseA):
        self.D = D  # 数据集
        self.label = label  # 哪个属性作为标签
        self.chooseA = chooseA  # 划分方法
        self.A = list(filter(lambda key: key != label, D[0].keys()))  # 属性集合A
        # 获得A的每个属性的可选项
        self.A_item = {}
        for a in self.A:
            self.A_item.update({a: set(self.getClassValues(D, a))})
        self.root = self.generate(self.D, self.A)  # 生成树并保存根节点

    # 获得D中所有className属性的值
    def getClassValues(self, D, className):
        return list(map(lambda sample: sample[className], D))

    # D中样本是否在A的每个属性上相同
    def isSameInA(self, D, A):
        for a in A:
            types = set(self.getClassValues(D, a))
            if len(types) > 1:
                return False
        return True

    # 构建决策树,递归生成节点
    def generate(self, D, A):
        node = {}  # 生成节点
        remainLabelValues = self.getClassValues(D, self.label)  # D中的所有标签
        remainLabelTypes = set(remainLabelValues)  # D中含有哪几种标签

        if len(remainLabelTypes) == 1:
            # 当前节点包含的样本全属于同个类别,无需划分
            return remainLabelTypes.pop()  # 标记Node为叶子结点,值为仅存的标签

        most = max(remainLabelTypes, key=remainLabelValues.count)  # D占比最多的标签

        if len(A) == 0 or self.isSameInA(D, A):
            # 当前属性集为空,或是所有样本在所有属性上取值相同,无法划分
            return most  # 标记Node为叶子结点,值为占比最多的标签

        a = self.chooseA(D, A, self)  # 划分选择

        for type in self.A_item[a]:
            condition = (lambda sample: sample[a] == type)  # 决策条件
            remainD = list(filter(condition, D))  # 剩下的样本
            if len(remainD) == 0:
                # 当前节点包含的样本集为空,不能划分
                node.update({type: most})  # 标记Node为叶子结点,值为占比最多的标签
            else:
                # 继续对剩下的样本按其余属性划分
                remainA = list(filter(lambda x: x != a, A))  # 未使用的属性
                _node = self.generate(remainD, remainA)  # 递归生成子代节点
                node.update({type: _node})  # 把生成的子代节点更新到当前节点
        return {a: node}


#  定义划分方法

# 随机选择
def random_choice(D, A, tree: DecisionTree):
    return choice(A)


# 信息熵
def Ent(D, label, a, a_v):
    D_v = filter(lambda sample: sample[a] == a_v, D)
    D_v = map(lambda sample: sample[label], D_v)
    D_v = list(D_v)
    D_v_length = len(D_v)
    counter = Counter(D_v)
    info_entropy = 0
    for k, v in counter.items():
        p_k = v / D_v_length
        info_entropy += p_k * math.log(p_k, 2)
    return -info_entropy


# 信息增益
def information_gain(D, A, tree: DecisionTree):
    gain = {}
    for a in A:
        gain[a] = 0
        values = tree.getClassValues(D, a)
        counter = Counter(values)
        for a_v, nums in counter.items():
            gain[a] -= (nums / len(D)) * Ent(D, tree.label, a, a_v)
    return max(gain.keys(), key=lambda key: gain[key])


#  创建决策树
desicionTreeRoot = DecisionTree(D, label='好瓜', chooseA=information_gain).root
print('决策树:', desicionTreeRoot)


# 决策树可视化类
class TreeViewer:
    def __init__(self):
        from graphviz import Digraph
        self.id_iter = map(str, range(0xffff))
        self.g = Digraph('G', filename='decisionTree.gv')

    def create_node(self, label, shape=None):
        id = next(self.id_iter)
        self.g.node(name=id, label=label, shape=shape, fontname="Microsoft YaHei")
        return id

    def build(self, key, node, from_id):
        for k in node.keys():
            v = node[k]
            if type(v) is dict:
                first_attr = list(v.keys())[0]
                id = self.create_node(first_attr + "?", shape='box')
                self.g.edge(from_id, id, k, fontsize='12', fontname="Microsoft YaHei")
                self.build(first_attr, v[first_attr], id)
            else:
                id = self.create_node(v)
                self.g.edge(from_id, id, k, fontsize='12', fontname="Microsoft YaHei")

    def show(self, root):
        first_attr = list(root.keys())[0]
        id = self.create_node(first_attr + "?", shape='box')
        self.build(first_attr, root[first_attr], id)
        self.g.view()


# 显示创建的决策树
viewer = TreeViewer()
viewer.show(desicionTreeRoot)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值