Python手搓C4.5决策树+Azure Adult数据集分析

前言

课上的实验

Adult数据集可以在Azure官网上找到

Azure 开放数据集中的数据集 - Azure Open Datasets | Microsoft Learn

数据集预处理

  1. 删除难以处理的权重属性fnlwgt与意义重复属性educationNum
  2. 去除重复行与空行
  3. 删除包含异常值的数据

处理连续值属性

  1. 年龄数据分箱(使得各个年龄段中高收入人群占比的差异尽量大):
  2. 资本收益数据分箱
  3. 资本支出数据分箱
  4. 某周工作时长数据分箱

处理离散值属性

  1. workclass工作部门,可以把相同的工作部门归为一类避免决策树分叉过多
  2. education学历,可以把学历相近的分为一块,以减少决策树分叉
  3. maritalStatus婚姻状况,将离异、丧偶、分居等归为一类,未婚归为一类,已婚与配偶暂时不在归为一类,再婚归为一类,分四类。
  4. occupation职业,由于不同职业的薪水状况不同,所以只能每个职业都单独作为一类
  5. relationship家庭关系,每种单独分为一类
  6. race种族,每种单独归类
  7. sex性别,分两类
  8. nativeCountry国籍,由于美国人居多,所以分为美国与其他国家两类
  9. income收入,这是我们需要预测的结果,分为 >50K 和 <=50K,由于测试集中的标签多了一个‘.’所以需要单独处理一下

C4.5决策树

其实决策树并没什么太难的地方,主要是使用的python,pandas库在划分数据集时如果使用单行遍历会很慢,此时需要找到符合功能需求的批处理函数

决策树主要分为以下几个模块

1、计算信息熵(D表示数据集,|D|表示数据集大小,Di表示分类结果为i的数据集)

信息熵:

条件信息熵:(按照属性A划分之后的信息熵加权平均数,D(j)表示属性A为j的数据集)

2、获取数据集中的众数。作为叶节点的信息

3、将数据集按照某个关键字划分。这里很坑,如果单行遍历划分回巨慢无比,但是pandas有专门的批处理函数groupby用于划分(划分时间直接从30+s优化到0.0s),但是如果当前值不存在会发生报错,所以要单独加入一个占位的DataFrame

4、决策树划分策略

按照C4.5决策树的划分规则,需要计算信息增益比

信息增益:

信息增益比

所以,在寻找最优划分策略的时候需要枚举每一个未划分的属性,计算划分后的数据集的信息增益比,选择信息增益比最高的属性进行划分即可

5、决策树构建

由于决策树很容易过拟合,所以这里使用了两种剪枝方法,首先设置节点纯度阈值,当递归时节点纯度高于阈值时可以直接选用当前数据集的众数作为节点值,停止递归。然后设置深度阈值,当超过该深度时就取当前数据集的众数作为节点值,停止递归。

构建过程:

由于是进行的递归构建,相当于在对最优决策树做一个先根遍历,首先对于当前节点,在决策树存储矩阵上添加一行,存储当前节点的决策信息;然后将每个儿子返回的矩阵依次append到这个矩阵下方,利用当前的矩阵行数计算儿子行标相对于当前节点行标的增量。完成构建之后,为了后续方便查询,对每个节点的用当前的行数加上儿子节点的增量,就可以算出儿子节点对应的行数。

6、决策树分类过程

对于每个数据组,从决策树根节点开始,选择决策树节点划分的属性,将当前节点id跳转到数据该属性对应值的儿子节点即可,直到跳转到叶子节点停止

运行结果

训练集分类结果(准确率0.833)

测试集分类结果(准确率0.836)

效果还是不错,堪比神经网络?

反思总结

这次实验花费了很长时间在数据集的分析和处理上

包括年龄和资本收支的分箱、离散值归并,并且发现了测试集数据中income标签与训练集不同的问题。

决策树构建过程中花费了许多时间去查询pandas的批处理函数,如果之前有pandas库调用的基础会好很多。

决策树存储结构选用numpy是不太合适的,因为每一个节点的结构儿子个数是不定的,如果按照最多分支数来设置矩阵的列数会有很多空间是浪费的。使用list+dict保存每个节点的数据,用json文件存储读取应该会方便一些。

完整代码

数据预处理

columns = ['age', 'workclass', 'fnlwgt', 'education', 'educationNum', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex',
          'capitalGain', 'capitalLoss', 'hoursPerWeek', 'nativeCountry', 'income']
df_train_set = pd.read_csv('./adult.data', names=columns)
df_test_set = pd.read_csv('./adult.test', names=columns, skiprows=1) #第一行是非法数据

print(df_train_set.head())
print(df_test_set.head())
df_train_set.to_csv('./train_adult.csv', index=False)
df_test_set.to_csv('./test_adult.csv', index=False)
def preprocess(in_path, out_path):
   df = pd.read_csv(in_path)
   df.drop(['fnlwgt', 'educationNum'], axis=1, inplace=True) # fnlwgt列用处不大,educationNum与education类似
   print(df.columns)
   df.drop_duplicates(inplace=True) # 去除重复行
   df.dropna(inplace=True) # 去除空行 
   print(df.shape[0])
   # 查找异常值, 避免与正则表达式的?冲突需要转义
   new_columns = ['workclass', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex', 'nativeCountry', 'income']
   for col in new_columns:
      df = df[~df[col].str.contains(r'\?', regex=True)]
   print(df.shape[0])
   continuous_column = ['age', 'capitalGain',  'capitalLoss', 'hoursPerWeek']
   bins = [0, 23, 33, 43, 55, 100]
   df['age'] = pd.cut(df['age'], bins, labels=False)
   bins = [-1, 10000, 50000, 150000]
   df['capitalGain'] = pd.cut(df['capitalGain'], bins, labels=False)
   bins = [-1, 1000, 2000, 10000]
   df['capitalLoss'] = pd.cut(df['capitalLoss'], bins, labels=False)
   bins = [0, 20, 40, 60, 80, 100]
   df['hoursPerWeek'] = pd.cut(df['hoursPerWeek'], bins, labels=False)

   discrete_column = ['workclass', 'education', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex', 'nativeCountry', 'income']
   workclass_mapping = {' Private': 0, ' Self-emp-not-inc': 1, ' Self-emp-inc': 1, ' Local-gov': 2, 
                  ' State-gov': 2, ' Federal-gov': 2, ' Without-pay': 3, ' Never-worked': 3}
   df['workclass'] = df['workclass'].map(workclass_mapping)
   education_mapping = {' Preschool':0 , ' 1st-4th': 0, ' 5th-6th': 0, ' 7th-8th': 1, ' 9th': 1, ' 10th': 2, ' 11th': 2, ' 12th': 2, ' HS-grad': 3, ' Some-college': 3, ' Assoc-voc': 3,' Assoc-acdm': 3, ' Bachelors': 4, ' Masters': 4, ' Prof-school': 4,' Doctorate': 4}
   df['education'] = df['education'].map(education_mapping)
   maritalStatus_mapping = {' Never-married': 0, ' Married-civ-spouse': 1, ' Married-spouse-absent': 1, ' Married-AF-spouse': 2, ' Divorced': 3, ' Separated': 3, ' Widowed': 3}
   df['maritalStatus'] = df['maritalStatus'].map(maritalStatus_mapping)
   occupation_mapping = {' Prof-specialty': 0, ' Exec-managerial': 1, ' Adm-clerical': 2, ' Craft-repair': 3,
      ' Sales': 4, ' Other-service': 5, ' Machine-op-inspct': 6, ' Transport-moving': 7,
      ' Handlers-cleaners': 8, ' Farming-fishing': 9, ' Tech-support': 10,
      ' Protective-serv': 11, ' Priv-house-serv': 12, ' Armed-Forces': 13}
   df['occupation'] = df['occupation'].map(occupation_mapping)
   relationship_mapping = {' Unmarried': 0, ' Wife': 1,' Husband': 2,' Own-child': 3, ' Not-in-family': 4, ' Other-relative': 5}
   df['relationship'] = df['relationship'].map(relationship_mapping)
   race_mapping = {' White': 0, ' Black': 1, ' Asian-Pac-Islander': 2, ' Amer-Indian-Eskimo': 3,' Other': 4}
   df['race'] = df['race'].map(race_mapping)
   sex_mapping = {' Male': 0, ' Female': 1}
   df['sex'] = df['sex'].map(sex_mapping)
   nativeCountry_mapping = {' United-States': 0, ' Mexico': 1, ' Philippines': 1, ' Germany': 1, ' Puerto-Rico': 1,
      ' Canada': 1, ' India': 1, ' El-Salvador': 1, ' Cuba': 1, ' England': 1, ' Jamaica': 1,
      ' South': 1, ' China': 1, ' Italy': 1, ' Dominican-Republic': 1, ' Vietnam': 1,
      ' Guatemala': 1, ' Japan': 1, ' Poland': 1, ' Columbia': 1, ' Iran': 1, ' Taiwan': 1,
      ' Haiti': 1, ' Portugal': 1, ' Nicaragua': 1, ' Peru': 1, ' Greece': 1, ' France': 1,
      ' Ecuador': 1, ' Ireland': 1, ' Hong': 1, ' Cambodia': 1, ' Trinadad&Tobago': 1,
      ' Thailand': 1, ' Laos': 1, ' Yugoslavia': 1, ' Outlying-US(Guam-USVI-etc)': 1,
      ' Hungary': 1, ' Honduras': 1, ' Scotland': 1, ' Holand-Netherlands': 1}
   df['nativeCountry'] = df['nativeCountry'].map(nativeCountry_mapping)
   income_mapping = {' <=50K': 0, ' <=50K.': 0,' >50K': 1,' >50K.': 0,}
   df['income'] = df['income'].map(income_mapping)
   df.to_csv(out_path, index=False)
preprocess('./train_adult.csv','./train_adult_processed.csv')
preprocess('./test_adult.csv','./test_adult_processed.csv')

决策树

# 读数据,初始化决策树参数,预设每行的范围
df_train = pd.read_csv('./train_adult_processed.csv') # 读取预处理后的数据
columns = df_train.columns.to_list()
flags = [0 for i in range(len(columns))]
column_range_map = {columns[i]:df_train.iloc[:,i].value_counts().index.max()+1 for i in range(len(columns))}
print(column_range_map)

pure_rate_thresh = 0.87 #纯度阈值 用于决策树剪枝
deep_thresh = 7 #深度阈值 用于决策树剪枝
def calc_entropy(df):
    """
    计算数据集income的信息熵
    :param df: 数据集
    :return: 信息熵
    """
    all = len(df)
    if all == 0:
        return 0
    cnts = [list(df['income']).count(0), list(df['income']).count(1)]
    sum = 0
    for cnt in cnts:
        if cnt > 0:
            sum -= cnt/all*math.log2(cnt/all)
    return sum
def calc_mode(df):
    """
    计算数据集income的众数
    :param df: 数据集
    :return: 众数
    """
    #如果没有数据可以支撑决策,就随机输出
    if len(df) == 0:
        return random.randint(0, 1)
    cnt = [list(df['income']).count(0), list(df['income']).count(1)]
    return 0 if cnt[0]>cnt[1] else 1
def split_dataset(df, index):
    """
    按照给定的列划分数据集
    :param df: 原始数据集
    :param index: 指定特征的列索引
    :return: 切分后的数据集
    """
    df_list = []
    for val in range(column_range_map[index]):
        if val in list(df[index]):
            df_list.append(df.groupby(index).get_group(val))
        else:
            df_list.append(pd.DataFrame([],columns=columns))
    return df_list

def choose_best_feature_to_split(df, flags):
    """
    选择最好的特征进行分裂
    :param df: 数据集
    :param flags: 区分特征是否被完全区分开,初始为全0, 若某个特征被区分开那么flags对应的下标为1
    :return: best_value:(分裂特征的index, 特征的值), best_df:(分裂后的子树数据集), best_gain:(选择该属性分裂的最大信息增益率)
    """
    h_df = calc_entropy(df)
    best_value = -1
    best_df = []
    best_gain = 0
    cols = df.columns.to_list()
    for i in range(0,len(cols)-1): # 注意不能直接划分income
        if(flags[i]):
            continue
        index = cols[i]
        df_list = split_dataset(df,index)
        h_df_a = 0
        split_info = 1e-10
        # 采用C4.5方法,计算信息增益比
        for sdf in df_list:
            if len(sdf) == 0 :
                continue
            h_df_a += len(sdf)/len(df) * calc_entropy(sdf)
            split_info -=  len(sdf)/len(df) * math.log2(len(sdf)/len(df))
        now_gain = (h_df - h_df_a)/ split_info
        if best_gain < now_gain:
            best_df = df_list
            best_value = i
            best_gain = now_gain
    return (best_value,best_df,best_gain)

def build_decision_tree(df, flags, fadep):
    """
    构建决策树
    :param df: 数据集
    :param flags: 区分特征是否被完全区分开,初始为全0, 若某个特征被区分开那么flags对应的下标为1
    :return: 决策树
    """
    dep = fadep + 1
    if len(df) == 0 or flags.count(1) == len(flags) or dep > deep_thresh: #该数据集为空 或者 所有关键字都被划分 或者 超出决策树深度阈值 返回
        leaf = np.zeros((1,16),dtype='int')
        leaf[0][0]= calc_mode(df)
        leaf[0][1]= 0
        return leaf
    cnt = [list(df['income']).count(0), list(df['income']).count(1)]
    if max(cnt[0],cnt[1])/(len(df)) > pure_rate_thresh:# 高于纯度阈值 返回
        leaf = np.zeros((1,16),dtype='int')
        leaf[0][0]= 0 if cnt[0]>cnt[1] else 1
        leaf[0][1]= 0
        return leaf
    best_value,best_df,best_gain = choose_best_feature_to_split(df,flags)
    # 创建该子树的根节点
    tree = np.zeros((1,16),dtype='int')
    tree[0][0] = best_value
    tree[0][1] = len(best_df)
    son_cnt = 0
    # 递归加入儿子的节点列表
    for sdf in best_df:
        son_flags = copy.deepcopy(flags)
        son_flags[best_value] = 1
        son_tree = build_decision_tree(sdf, son_flags, dep)
        # 计算当前儿子节点的行标 相对于 当前行标的增量
        tree[0][son_cnt+2] = tree.shape[0]
        # 把儿子的所有节点拼接进来
        tree = np.append(tree,son_tree,axis=0)
        son_cnt += 1
    return tree
    
def save_decision_tree(decision_tree):
    """
    决策树的存储
    :param decision_tree: 训练好的决策树
    :return: void
    """
    np.save('decision_tree.npy', decision_tree)
    
    
def load_decision_tree():
    """
    决策树的加载
    :return: 保存的决策树
    """    
    decision_tree = np.load('decision_tree.npy', allow_pickle=True)
    return decision_tree
decision_tree = build_decision_tree(df_train, flags, 0)
# 由于当前算出来的是偏移量,需要进行重定位
for i in range(decision_tree.shape[0]):
    for j in range(2,2+decision_tree[i][1]):
        decision_tree[i][j] += i
print(decision_tree)
print(decision_tree.shape[0])
save_decision_tree(decision_tree)

分类测试

def classify(tree, id, df_row):
    """
    用训练好的决策树进行分类
    :param cart:决策树模型
    :param id:当前在决策树上的节点编号
    :param df_row: 一条测试样本
    :return: 预测结果
    """
    while id >= 0 and id < len(tree):
        if tree[id][1] == 0:
            return tree[id][0]
        id = tree[id][2+df_row.iloc[ tree[id][0] ]]
    return -1


def predict(decision_tree, df):
    """
    用训练好的决策树进行分类
    :param cart:决策树模型
    :param df: 所有测试集
    :param columns: 特征列表
    :return: 预测结果
    """
    print("predict start!")
    pred_list = []
    for i in range(len(df)):
        pred_label = classify(decision_tree, 0, df.iloc[i])
        if pred_label == -1:
            pred_label = random.randint(0, 1) # 防止classify执行到返回-1,但一般不会执行到返回-1
        pred_list.append(pred_label)
    return pred_list

def calc_acc(pred_list, test_list):
    """
    返回预测准确率
    :param pred_list: 预测列表
    :param test_list: 测试列表
    :return: 准确率
    """
    pred = np.array(pred_list)
    test = np.array(test_list)
    acc = np.sum(pred_list == test_list) / len(test_list)
    return acc
#检验训练集
np.set_printoptions(threshold=np.inf)
df_train = pd.read_csv('./train_adult_processed.csv')
decision_tree = load_decision_tree() # 加载模型
test_list = df_train['income'].to_numpy()
pred_list = predict(decision_tree, df_train)
acc = calc_acc(pred_list, test_list)
print(acc)
#检验测试集
df_test = pd.read_csv('./test_adult_processed.csv')
decision_tree = load_decision_tree() # 加载模型
test_list = df_test['income'].to_numpy()
pred_list = predict(decision_tree, df_test)
acc = calc_acc(pred_list, test_list)
print(acc)

#测试集结果保存到文件
file = open("result.txt", "w")
file.write(str(pred_list)+'\n')
file.write(str(acc))
file.close()

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值