数据挖掘实验2python编写贝叶斯分类器
1. Introduction
本文基于前文说的朴素贝叶斯原理,参考圣地亚哥州立大学的实验编写了一个简单的朴素贝叶斯分类器,并利用测试数据进行了测试。
项目地址:
2. 分类器编写
2.1数据说明
采用“adult”数据集,输入文件是adult.data,测试文件是adult.test。数据中一行为一个条目,表示一个人
数据集中的变量
变量名 | 意义 |
---|---|
age | 年龄 |
type_employer | 职业类型,个体,政府等等 |
fnlwgt | 该变量将被我们忽略 |
education | 学历 |
education_num | 学历的数字等级(和上一个一样,将被忽略) |
marital | 婚姻状况 |
occupation | 职业 |
relationship | 不清楚如何表述 |
race | 人种 |
sex | 生理性别 |
capital_gain | 资本收益 |
capital_loss | 资本支出 |
hr_per_week | 每周工作时长 |
country | 国籍 |
income | 年收入是否>50k |
由于参考文章中使用的是R语言进行处理,R语言在数据挖掘和统计上优势极大,几乎就是为其而生。python中也有numpy库,但是这里仅用了numpy库的median取中位数功能,其他还是以python原生类型来处理。
2.2 框架规划
参考使用Python编写朴素贝叶斯分类器,我们也主要使用字典来实现统计,但是可以分成两个字典,一个是>50k的dataset_high,一个是<=50k的dataset_low。
class DataSet:
def __init__(self):
# 存储读入的原始数据
self.data = []
# 支出的中位数
self.loss_mid = 0
# 收入的中位数
self.gain_mid = 0
# 工作时长的中位数
self.hours_mid = 0
# 年龄的中位数
self.age_mid = 0
# 统计的数据,主要部分
self.classfied_dataset = None
# 总数据条目
self.len_data = 0
最后的计算是前文说的:
$$
P(输入数据的特征|>50k) = {P(年龄|>50k)P(职业类型|>50k)…P(国籍|>50k)P(>50K) \over P(输入数据的特征)} \\P(输入数据的特征|<=50k) = {P(年龄|<=50k)P(职业类型|<=50k)…P(国籍|<=50k)P<=(50K) \over P(输入数据的特征)}
$$
最后两者取大者,则就是所建模型的判定,是否大于50k。
公式化简:
由于P(输入数据的特征)对于一条数据,两个公式来说,是相同的,所以略去计算。
2.3 输入数据预处理
a = ["age", "type_employer", "fnlwgt", "education", "education_num", "marital", "occupation", "relationship", "race",
"sex", "capital_gain", "capital_loss", "hr_per_week", "country", "income"]
classfiled_data = {}
loss_median = loss
gain_median = gain
for node in a:
classfiled_data[node] = {}
for line in data:
if len(line) < 10:
continue
for node in a:
if line[a.index(node)] in classfiled_data[node]:
classfiled_data[node][line[a.index(node)]] += 1
else:
classfiled_data[node][line[a.index(node)]] = 1
列表a就是所有的字段,将所有的数据都按照对应字段,统计到classfiled_data上去,最后形成的形式如下:
# 打印classfiled_data的输出,这是已经简化过的输出
{education:
{'Prof-school': 153, 'dropout': 4009, 'Doctorate': 107, 'HS-grad': 8826, 'Bachelors': 3134, 'Assoc': 1823, 'Some-college': 5904, 'Masters': 764},
marital:
{'Widowed': 908, 'Never-married': 10192, 'not-married': 5323, 'Married': 8297},
country:
{'other': 133, 'United-States': 21999, 'British-Commonwealth': 230, 'SE-Asia': 242, 'Euro_1': 159, 'Euro_2': 122, '?': 437, 'South': 64, 'China': 100, 'Latin-America': 1027, 'South-America': 207},
income:
{'<=50K': 24720},
capital_gain:
{'low': 0, 'none': 23685, 'high': 1035},
relationship:
{'Not-in-family': 7449, 'Own-child': 5001, 'Other-relative': 944, 'Husband': 7275, 'Wife': 823, 'Unmarried': 3228},}
即classfiled_data的第一层字段是a里面的字段,每个字段又对应不同类型的子字段,数字是统计所有数据的出现次数。
2.4 字段简化
舍弃没用的fnlwgt
和重复的education_num
字段
对于职业类型字段,Never-worked和without-Pay可以合并为Not-working字段,类似的,也可以把其他一些字段进行合并,合并的步骤是先在classfiled_data['type_employer']里新建一个'not-working'的key,然后其value就是原来['Never-worked', 'Without-pay']的数值之和。在写了很长的代码以后,我将其提取出来做成了一个函数:
def tiny(a_list, category, new_name):
if new_name not in classfiled_data[category]:
classfiled_data[category][new_name] = 0
for key in list(classfiled_data[category]):
if key in a_list and key != new_name:
classfiled_data[category][new_name] += classfiled_data[category][key]
del classfiled_data[category][key]
tiny(['Never-worked', 'Without-pay'], 'type_employer', 'not-working')
tiny(['Local-gov', 'State-gov'], 'type_employer', 'other-govt')
tiny(['Self-emp-inc', 'Self-emp-not-inc'], 'type_employer', 'self-employed')
同样对其他字段也进行了类似的化简。
这里有这样几个字段需要单独处理:
- capital_gain 利用中位数划分成三部分:(-INF, 0] (0, mid] (mid, INF]
- capital_loss 同上
- hr_per_week 工作时间按照10小时间隔划分了。最大值99,映射到100s上
- age 按照5为间隔划分了20组,
3. 测试数据
由于前面对数据进行了化简,所以测试数据的输入也需要按照上面的划分进行映射,我代码里直接使用生成好的字典进行映射。
针对每条数据,计算P(输入数据的特征|>50k) 和P(输入数据的特征|<=50k),取大的返回结果。
最后测试结果如下:
模型的判断正确的次数: 13206
错误的次数 3075
正确率: 0.811130