数据分析综合项目案例:幸福指数深度挖掘(KNN,随机森林)

数据分析综合项目案例:幸福指数深度挖掘(KNN,随机森林)

这个项目主要难度在清洗阶段。先看一下数据集。这个数据字段稍微有点多。

很多很多字段,可以看一下下方的拉条。

怎么样去使用字段是很关键的。这里左右字段的含义,用一张表格整理了一下。

介绍的字段信息有很多。这当中的字段一共有 140 个。

有一些预备知识需要介绍一下。

预备知识(基础好可跳过)

什么是数据预处理

通常获取数据通常都是不完整的,缺失值、零值、异常值等情况的出现导致数据的质量大打折扣,而数据预处理技术就是为了让数据具有更高的可用性而产生的。

原始数据 = 菜市场刚买回来的生鲜,数据预处理 = 备菜

你要做一顿大餐(跑机器学习 / 数据分析模型),直接拿刚买的菜下锅肯定不行:菜里有烂叶子、泥沙、虫眼、大小不一、有的还冻坏了,还有不能吃的菜根外皮;不处理直接炒,味道差、没法吃,甚至会吃坏肚子。数据预处理,就是给 “原始数据” 做全套备菜操作,把脏、乱、残缺的数据,整理成能直接下锅分析的干净数据。

原始数据有缺失值、零值、异常值:

缺失值 = 蔬菜缺块 / 少半颗

  • 比如买的白菜有一大半烂掉丢掉、鸡蛋碎了一个;放到数据里就是某条记录少填了年龄、销售额、检测指标。
  • 不处理的后果:模型读到一半 “断食材”,直接报错或者计算跑偏。
  • 预处理操作:补全(用平均值填充)、直接删掉残缺严重的样本。

异常值 = 菜里混进石子 / 变质食材

  • 比如一筐土豆里混了块石头、一颗完全发霉的土豆;数据里就是离谱数字:正常人身高 2 米以内,突然出现一条身高 999cm 的错误录入。
  • 不处理的后果:这颗 “石子” 会搅乱整锅菜的味道,拉高 / 拉低整体平均值,分析结果完全失真。
  • 预处理操作:剔除极端异常、修正错误数值。

零值干扰 = 不该为空的食材全是空

  • 比如统计店铺日销量,某天设备故障全部记成 0,但当天明明有营业;不是真实销量为 0,是采集出错。
  • 不处理的后果:误以为店铺那天彻底停业,后续营收预测全部出错。
  • 预处理操作:区分真实零和错误零,修正无效零记录。

重复数据的处理

还有重复的数据,首先第一步也是非常重要的一步,就是判断是否是重复数据。来看一下哪个是真正的重复数据。

表格 A:

姓名体重身高购买商品付款金额
小华67180OPPO Reno164499
小华67180oPPO Reno164499

表格 B:

姓名体重身高购买商品付款金额付款时间
小华67180OPPO Reno1644992026-06-13 00:00
小华67180oPPO Reno1644992026-06-13 00:00

通过表格 A 的这两个数据可以发现,购买商品不太一样,一个是大写的O,一个是小写的o。同时观察一下 B,发现商品也是一个大写一个小写的。这俩表格的两行数据其实都是完全一摸一样的,只不过商品大小写不一样,但是其他的部分意思全部都是一样的。如果你认为是一样的内容,那么就全部把它改为大写或者全部改为小写,然后去重。数据量非常多的时候,保险起见,可以统一变成大写,或者统一都变成小写。一般来说,把常用的处理放在前面,把去重放在后面。

接下来去 jupyter 上面看一下数据。把这段代码直接复制粘贴上去,然后运行一下。

import pandas as pd # 创建一个带有重复数据的DataFrame df = pd.DataFrame(data=[['a', 1], ['a', 2], ['a', 3],['b', 1], ['b', 2],['a', 1], ['a', 2]],columns=['label', 'num']) df

可以发现总共是 5 行 a,2 行 b。a 对应的 num 是 1 2 3,如果想判断这些数据是否有重复的数据,需要用到一个函数。直接 df.duplicated() 就可以。

# 判断 df.duplicated() # axis=0

直接打印就可以发现,它第一次出现的时候不认为是重复数据,当它第二次出现的时候就认为是重复数据了。默认是按行进行删除的。它里面参数 axis 默认是 0,除非手动设置一下 axis 为 1,就会变成按列删除。之间介绍过,除非这个字段对分析没有任何用处,反而执行的时候又影响到执行的效率,这个时候就可以考虑把字段给删除掉,其余时候要慎删。一般来说都是删除行。

把重复值删掉。使用 df.drop_duplicates() 就可以了。打印出来看看。

df.drop_duplicates()

可以看到运行之后,它把最后 5 6 这两行删除了,只剩下 0 1 2 3 4。这个就是重复值删除掉的过程。接下来看一下数据的标准化处理。

数据标准化

Z-score 标准化(StandardScaler)

只做两件事:把这一列数据的平均值变成 0;数据离散程度(波动)统一成标准差 = 1。
本质:衡量“这个数值离平均水平差了几个档次”,消除单位影响。

举个栗子,身高:160、165、170、175、180。全班平均身高 170,我们以 170 为基准线 0:170 → 0(刚好平均);175 比平均高 5cm → +1;165 比平均矮 5cm → -1;180 高 10cm → +2;160 矮 10cm → -2;处理后数据:-2,-1,0,1,2。
数字含义:正数 = 高于平均,负数 = 低于平均,数字绝对值越大偏离均值越远。

再来个栗子:考试排名分:不看原始卷面分,只看你在班里处于上游还是下游。
比如语文满分 150、数学满分 100,没法直接对比;用 Z-score 后,两门课都以班级平均分做基准,可以公平比较你哪科更好。

1. 计算公式


:原始样本值
:该特征全部数据的均值
:该特征全部数据的标准差

标准差的公式:

符号说明
:总体标准差
:总体均值(你之前 Z 分数公式里的均值)
:总体全部数据量
:第个数据

2. 处理后数据特性

新数据集均值 = 0,标准差 = 1;
保留原始数据的相对分布、离群点关系;
数值可正可负,无固定区间。

3. 适用场景

对尺度敏感的算法:SVM、逻辑回归、线性回归、PCA 主成分分析、K-Means 聚类、神经网络;
数据大致服从正态分布;需要消除量纲(比如收入、年龄、体重单位完全不同)。

4. 缺点

受极端异常值(离群点)影响很大。

代码实现

回到 jupyter notebook 中,使用代码感受一下这个数据处理方法。将下面代码复制粘贴到 jupyter 当中,然后运行。

from sklearn.preprocessing import StandardScaler import pandas as pd views = pd.DataFrame({ 'height': [1.8, 1.7, 1.9, 1.75, 1.68, 1.67], 'weight': [80, 70, 98, 67, 68, 50] }) ss = StandardScaler() views_scaled = ss.fit_transform(views) # 使用同一个Z-score进行标准化 print(views_scaled)

发现身高体重都标准化处理完成了。还可以用定义函数的方式手动实现 Z-score 标准化。效果是一样的。

from sklearn.preprocessing import StandardScaler import pandas as pd import numpy as np views = pd.DataFrame({ 'height': [1.8, 1.7, 1.9, 1.75, 1.68, 1.67], 'weight': [80, 70, 98, 67, 68, 50] }) f = lambda x: ((x - np.mean(x)) / np.std(x)) views.apply(f)

MinMaxScaler() 最大最小化处理

把一整列数据,全部压缩到 0 ~ 1 区间里,原始最小值变成 0,最大值变成 1,中间数值按比例缩放。

举个栗子,班级 5 个同学身高:160、165、170、175、180。最小身高 160 → 映射为 0,最大身高 180 → 映射为 1。中间 170 刚好在中间,就变成 0.5;165 就是 0.25,175 是 0.75。处理后全部数据:0, 0.25, 0.5, 0.75, 1。

像压缩视频分辨率:原图高低差距很大,统一压到固定范围。或者称重:不管东西最重多沉、最轻多轻,统一换算成 0 到 1 的比例。

1. 计算公式

:该特征最小值
:该特征最大值

2. 处理后数据特性
全部数据被压缩到 [0, 1] 区间;
严格保留数据大小比例关系;
均值不再固定为 0。

3. 适用场景
不依赖距离、方差的模型:决策树、随机森林(树模型对尺度不敏感,归一化非必需);
图像像素归一化(像素 0~255 缩放到 0~1);
神经网络输入层(很多网络要求输入 0~1);
数据无明显极端异常值。

4. 缺点
如果数据存在极大 / 极小离群值,会导致正常数据被挤压在极小区间,区分度变差。

Z-score vs MinMax 核心对比表格展示:

维度Z-score 标准化MinMax 归一化
输出范围无固定区间,均值 0 方差 1固定 [0,1]
抗异常值差,均值标准差易被偏移极差更易被离群值破坏
核心作用消除分布偏移、统一方差压缩数值区间、保留比例
代表工具

StandardScaler()

MinMaxScaler()
代码实现

回到 jupyter 当中,将下面的代码复制粘贴。运行之后可以看到,最大最小化处理把每一列的数据都压缩在了 0 ~ 1 之间。

from sklearn.preprocessing import MinMaxScaler import pandas as pd views = pd.DataFrame({ 'height': [1.8, 1.7, 1.9, 1.75, 1.68, 1.67], 'weight': [80, 70, 98, 67, 68, 50] }) mms = MinMaxScaler() views_scaled = mms.fit_transform(views) print(views_scaled)

补充关键知识点:fit_transform 和 transform 的区别

  • 训练集用 fit_transform:一边统计最大最小值,一边转换
  • 测试集只能用 transform:必须沿用训练集算出的 min/max,不能重新 fit,防止数据泄露

接下来看一下哑变量和独热编码。

哑变量和独热编码

先讲底层问题:机器看不懂文字。假设一列特征:性别【男、女】;颜色【红、黄、蓝】。电脑只认数字,如果你手动标男 = 1,女 = 2,模型会误以为:女的数值比男大,存在大小关系,但性别根本没有高低之分,会造成严重误差。

解决办法:独热编码,衍生出来的 0/1 新列就叫哑变量。前面两种只处理连续数值特征,独热编码专门处理分类离散特征(文字 / 类别)。

1. 概念说明

哑变量(Dummy Variable)

把多分类特征拆成若干 0/1 二元变量,比如特征 “颜色:红 / 蓝 / 黄”,拆为红、蓝、黄三列,样本为红色则红列 = 1,其余 = 0。
普通哑变量会舍弃一列避免多重共线性(线性回归常用)。

独热编码(One-Hot Encoding)

完整生成全部类别对应的 0-1 向量,不丢弃任何一列,树模型、神经网络、KNN 常用,对应工具 OneHotEncoder()。

2. 示例
原始分类列:['男','女','男','未知']
独热编码后生成 3 列:

未知
100
010
100
001

3. 适用场景
类别无序离散特征:城市、性别、商品类型、颜色;
不能直接把文字类别丢进数学模型(模型只能计算数字),必须转为 0/1 数值。

4. 局限
类别数量极多时(如上万城市)会产生维度爆炸,此时改用标签编码、目标编码更合适。

代码实现

现在有性别:{男,女,其他}。性别特征有三个不同的分类值,需要三个 bit 的值来表示这些类别。独热码表示为: 男:{01},女:{10},其他:{00}。多个特征时表示为:性别:{男,女,其他}; 性别编码为:男:{01},女:{10},其他:{00}。年级:{一年级,二年级,三年级}; 年级编码为:一年级:{10},二年级:{01},三年级:{00}。对于二年级的男生就可以编码为:{0110}(前面的 01 表示一年级,后面的 10 表示男生)。

哑变量方式,下面代码复制粘贴,运行一下。没问题。

# 哑变量 import pandas as pd df = pd.DataFrame({ '性别': ['男', '女', '其他'], '年级': ['一年级', '二年级', '三年级'] }) df = pd.get_dummies(df) df

这里 False 是 0,True 对应的是 1。老版本显示的是 0 和 1,版本较新的话,显示的是 True 和 False。它是 pandas 里面的方式,可以发现性别有三列,年级也是有三个。因为它会看里面每一个的唯一值,可以给它单独做成一列。

独热编码也是一样的,用 one_hot 进行转化就可以了。独热码表示: 男:{001},女:{010},其他:{100}。多个特征时表示:性别:{男,女,其他};性别编码为:男:{001},女:{010},其他:{100};年级:{一年级,二年级,三年级};年级编码为:一年级:{100},二年级:{001},三年级:{010};对于二年级的男生编码可以为:{001001}(前面的 001 表示二年级,后面的 001 表示男生)。将下面的代码直接复制粘贴一下,然后运行。

import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({ '性别': ['男', '女', '其他'], '年级': ['一年级', '二年级', '三年级'] }) ont_hot = OneHotEncoder(categories='auto') ont_hot.fit(df) ont_hot.transform(df).toarray()

这地方也是先拟合后转化,可以分开写,也可以下划线合一起。可以发现,这地方也是转成了数组的格式,和上面的方式是一模一样的。

以上这些内容复习好了之后,正式开始幸福指数分析项目。

综合项目案例:幸福指数深度挖掘

先来看字段说明这个表。这个数据的是做问卷调查得来的。问卷调查是为了了解更多的信息,总共是 140 个问答题。这个数据集大概看一下,发现这里对应的编号、类型、问卷时间、民族、宗教信仰、还有年收入、政治面貌、房子所有者,还有第三方平台的资金等等,这个是数据是很全面的。这个问的信息是非常全面的,主要针对的是个人信息,还有一些关于配偶子女的信息。

最难的是对这些数据进行处理。可以看到这里有关键字段已经给标黄了,happiness,这个字段也就是目标列。就是生活是否幸福。

看一下它这里的选项。1 = 非常不幸福; 2 = 比较不幸福; 3 = 说不上幸福不幸福; 4 = 比较幸福; 5 = 非常幸福; -8 = 无法回答。观察一下这个数据,可以发现 3 和 -8 是差不多的。接下来看一下怎么去做处理,看一下唯一值都有哪些。

分析

回到 jupyter,开始做分析。首先数据集先读取出来,并且指定一下编码格式,打印出来看看。

Happiness=pd.read_csv('happiness_train_complete.csv',encoding='gbk') Happiness

打印出来之后发现,这里的数据有很多。总共采集了 8000 条数据。想看人是不是幸福,最终通过 140 个问题来看到底幸福不幸福,幸福的程度是多少,如果总共是 10 分能打多少分。所以通过这些数据,happiness 是目标列,后面建模,预测幸福指数有多少,这个就是目标。

接下来去看一下目标列重复值。介绍一种新的方式,set()。直接把变量名字拿过来,目标列拿过来,这样就可以直接去做处理。这是一种比较简单的方式。set() 是 Python 内置集合函数,集合会自动剔除所有重复元素,只保留唯一不重复的值。

# 重复值 set(Happiness['happiness'])

打印出来可以看到一共 6 个值,之前分析过,-8 和 3 是差不多的意思(1 = 非常不幸福; 2 = 比较不幸福; 3 = 说不上幸福不幸福; 4 = 比较幸福; 5 = 非常幸福; -8 = 无法回答)。所以在做数据清洗的时候要把这两个字段清洗一下。

数据清洗

数据清洗比较多,慢慢来看。

归类处理

现在可以先把那个 -8 和 3 给处理了。-8 是问卷里无法回答,不能直接删掉样本,并且和 3 表达的意思(说不上幸福不幸福)一样,于是统一归 3(说不上幸福不幸福),方便后续建模分析。直接使用 replaced 函数进行替换,把 -8 用 3 来替换就可以了。替换完成之后可以通过画图的方式来进行展示。可以看一下这些数据的分布情况。

# 把所有“无法回答(-8)”的样本,统一归类为“说不上幸福不幸福(3)” Happiness['happiness']=Happiness['happiness'].replace(-8,3) Happiness['happiness'].plot.hist()

可以发现 4 是最高的,回到数据当中看一下 4 的意思,4 = 比较幸福。这当中还是存在不幸福的情况,只不过是比较少的。这是第一个字段。

缺失值处理

缺失值查看

接下来看一下数据有没有缺失。使用 info() 进行查看。

Happiness.info()

可以发现,info() 在这个地方使用似乎行不通。因为这个数据有点儿多,没有办法把全部的数据显示出来。可以使用 isnull 来看一看。在数据比较多的情况下,可以筛选进行查看。先查看其中 30 个数据,看有没有空值的情况,用这种方式试一试。

先把字段拿过来,先看前 30 个数据。

Happiness.isnull().sum()[:30]

通过这 30 行的查看可以发现,是有空值存在的,所以要对空值进行一下处理。统计一下缺失的比例。定义一个函数,把 Happiness 拿过来,然后首先统计一下总的缺失值个数。接下来要去算它的百分比,用上面的名字加上百分比命名新变量,然后将缺失的总个数除以数据量总数的值赋值到变量当中。之后要把算出来的个数和百分比拼接,用 pandas 里面的 concat 方法进行拼接,个数和百分比都拿过来,拼接方向是 1 轴(axis=1)。之后添加列名,让数据更容易去查看,0 这列表示缺失数,1 这列表示缺失的百分比。接下来通过切片的方式去取它下标为 1 这列不为 0 的,将这列进行降序(ascending=False)排序,依据是缺失百分比。最后可以再把这个结果保留一位小数。最终把这个表进行返回。调用这个函数,然后打印一下这个表格,看一下对应的缺失百分比的结果。

# 缺失值比例 def miss_deal(Happiness): # 统计它总体的个数 miss_count=Happiness.isnull().sum() # 统计它的百分比 miss_count_precent=miss_count*100/len(Happiness) # 个数和百分比进行拼接 miss_table=pd.concat([miss_count,miss_count_precent],axis=1) # 添加列名 miss_table=miss_table.rename(columns={0:'缺失数',1:'缺失百分比'}) miss_table=miss_table[miss_table.iloc[:,1]!=0].sort_values('缺失百分比',ascending=False).round(1) return miss_table miss_table=miss_deal(Happiness) miss_table

这是运行之后缺失比例的结果。这时候就可以根据确实的数据给它做一下处理。观察一下这个数据,前面都还是正常的,最后的时候打印出来了两行 0.0。其实数据是没有问题的,因为它是百分比,到最后是 0.0,并不是没有把数据处理完,而是因为最后小数点值保留了一位,有可能是 0.02%,给它保留的是一位,所以就只剩 0.0 了。

缺失比例有了,现在就可以依照缺失比例给它去做处理。首先是删除操作。

缺失值比例大于 60 的字段处理

先筛选出来缺失值比例大于 60 的,返回列名存入到变量当中,打印出来看一看。

# 删除缺失值 del_feature=list(miss_table[miss_table['缺失百分比']>60].index) del_feature

这些就是缺失比例大于 60% 的字段。前面缺失百分比那个地方没有加百分号,是因为加了之后就变成文本类型了,为了让字段保持数值类型可以参与计算,就没有加百分号,但是要清楚这里是百分比。

这里是有索引的,在原数据里面对应的是列名,这个地方缺失了 60% 的数据直接给删除掉了。这里还有 100%,99.6% 这种,这样的字段就没有任何意义了。这里使用 drop 来删除字段。

Happiness.drop(del_feature, axis=1, inplace=True) print(len(Happiness.loc[0]))

打印出来的结果是 130,也就是去除缺失值在 60% 以上的数据之后,还剩 130 个字段。删除了 10 个。现在缺失值大于 60% 的字段已经做完处理了,还有缺失值在 60% 以下的数据没有进行处理。

配偶相关字段处理

缺失值在 60% 以下的这部分数据不能使用删除处理,因为缺失的比较少,说明对后面分析肯定是有用处的,这些字段就用填充的方法进行处理。具体用什么办法填充,要结合介绍里面的资料来看。首先这些字段里,以 s_ 开头的比较多。可以到介绍标里筛选一下 s_ 开头的找一下字段。发现这几个都在一起的。

可以看到,都是关于配偶的数据,放在了一起。这里有详细的解释。

这个 s_work_exper 字段,“1 = 目前从事非农工作; 2 = 目前务农,曾经有过非农工作; 3 = 目前务农,没有过非农工作; 4 = 目前没有工作,而且只务过农; 5 = 目前没有工作,曾经有过非农工作; 6 = 从未工作过; ”。

感觉一下,这个字段的缺失值应该怎样去填充。这个时候就考验对业务的理解能力了。可以看到这一小截全是配有的数据,还有一种情况,就是这个人没有配偶。所以其实不用管它写的多复杂,缺失的地方直接用 0 来填充就好了。把字段拿过来,直接复制粘贴进来,使用 fillna 函数进行填充。

# 填充 Happiness['s_work_exper'] = Happiness['s_work_exper'].fillna(0)

剩下几个字段,除了 's_work_exper' 还有别的。别的其实和刚刚也是一样的,写的再复杂,ta 有可能没配偶,也都直接用 0 来填充就可以了。剩下的那些,只要是 s_ 开头的,都可以用 0 来填充。Ctrl + C,Ctrl + V,把那些 s_ 开头的字段全都拿过来,全部用 0 来填充。都写在同一个代码框里就可以。直接运行。

# 填充 # 配偶 Happiness['s_work_exper']=Happiness['s_work_exper'].fillna(0) Happiness['s_income']=Happiness['s_income'].fillna(0) Happiness['s_hukou']=Happiness['s_hukou'].fillna(0) Happiness['s_political']=Happiness['s_political'].fillna(0) Happiness['s_birth']=Happiness['s_birth'].fillna(0) Happiness['s_edu']=Happiness['s_edu'].fillna(0)

这地方就全部填充完成了。

教育相关字段处理

s_ 开头的全部处理完成之后,还有 edu_ 开头的两个字段,一个是 'edu_yr',一个是 'edu_status'。这俩字段是关于教育,教育看一下用什么来进行填充。同样方法,先到字段解释的那张表搜索一下。搜到之后定位在这里。

搜索之后可以看到, 'edu_status' 表示的是最高教育程度的状态。“1 = 正在读; 2 = 辍学和中途退学; 3 = 肄业; 4 = 毕业; ”。这里面肄业其实和 2 是差不多的。这里面肄业和辍学的,起码是读过书了,还有种情况,就是压根连书都没读的。这里的缺失值对应的就是这种情况。所以这个地方也用 0 来填充。但是要知道一下,上面代码框里面的 0 和下面的不一个意思,虽然都是 0。上面的 0 表示没有配偶,下面的 0 表示没念过书。

# 教育 edu_status Happiness['edu_status']=Happiness['edu_status'].fillna(0)

'edu_yr',对于这个字段的解释是:“您已经完成的最高学历是哪一年获得的“已完成”指已获得毕业证”。这个字段没有太大的意义,后面也没有对应的描述,所以它和幸福指数没有太大的关系。这里可以手动将这个字段进行删除。

del Happiness['edu_yr']

这样,edu_ 开头的字段也给进行处理了。

社交相关字段处理

接下来处理 social_ 开头的,'social_neighbor' 和 'social_friend' 这两个字段,social_ 开头一般是社交。看一下社交部分,还是刚才的办法,从介绍的表格当中查找。在这里。

一个是和邻居的社交程度,一个是和朋友社交的频繁程度。可以看到对应的值是一模一样的,都是“1 = 几乎每天; 2 = 一周1到2次; 3 = 一个月几次; 4 = 大约一个月1次; 5 = 一年几次; 6 = 一年1次或更少; 7 = 从来不; ”。尽管它再复杂,一周几次一个月几次这样的,前面都是有社交的。还有一部分人,是从出生到现在完全没社交过的。对应的就是 7, 7 就是从来不。这个地方就可以用 7 来进行填充。这两个字段的空值都是用 7 来填充。

# 社交 Happiness['social_neighbor']=Happiness['social_neighbor'].fillna(7) Happiness['social_friend']=Happiness['social_friend'].fillna(7)

孩子相关字段处理

字段 minor_child,孩子相关。

可以从介绍表里看到,它表示有多少个未成年子女,后面对应的没有给出描述。这里可以用 0 进行填充。压根没有孩子的。

# 孩子 Happiness['minor_child']=Happiness['minor_child'].fillna(0)

户口相关字段处理

hukou_loc 字段,户口相关。还是刚才的方法,去找字段是什么意思。可以看到,这里是户口登记地。

上户口就是出生的时候录入的身份信息。所以这里的空值直接填 4,户口待定就可以了。

# 户口 Happiness['hukou_loc']=Happiness['hukou_loc'].fillna(4)

婚姻相关字段处理

marital_1st 这个字段,表示第一次结婚的时间。

marital_now 表示与目前的配偶是哪一年结婚的。

发现 marital_now 这个字段没有一个具体的描述。这个时候要回到原数据,也就是数据集当中来。找到这个字段,在这里。

然后点一下这个字段的筛选,可以看到它最新的年份是 2023 年。我们可以认为这份调查问卷是 2023 年做的。所以如果这里是空值,我们就可以默认填写 2023。

所以这个字段的缺失值用 2023 来填充就可以了。

通过这个字段还可以计算出一个字段,婚姻时长,就是结婚多少年了。这个字段命名为“marital_yr”,直接用 2023 减去结婚年份就可以了。最后再将数据类型转换为 int64。这个算出来之后,“marital_now”这个字段就不需要了,可以给它删除掉。

marital_1st 这个字段也要删除掉。这个字段意思是第一次结婚的时间。结婚年份都有了这个字段没啥用,不留着了。这样,婚姻情况就处理好了。

# 婚姻 marital_now Happiness['marital_now']=Happiness['marital_now'].fillna(2023) Happiness['marital_yr']=2023-Happiness['marital_now'].astype('int64') del Happiness['marital_now'] del Happiness['marital_1st']

家庭收入字段处理

family_income 这个字段。收入这个字段是比较特殊的,一般都是使用中位数来进行填充。这里也是,直接用中位数去填充就可以了。运行看一下。

# 收入 用中位数填充 Happiness['family_income'].fillna(Happiness['family_income'].median())

运行之后是这样的结果。

把这个填充的值赋给家庭收入字段,就处理完成了。最后再打印一下空值占比结果,看看是不是都处理完成了,是不是都为 0。

# 收入 用中位数填充 Happiness['family_income']=Happiness['family_income'].fillna(Happiness['family_income'].median()) miss_table=miss_deal(Happiness) miss_table

可以看到打印出来的确实数和缺失百分比都没有了,也就是所有的缺失值都处理完成了。接下来对类型进行处理。

类型处理

年龄计算

首先对年龄进行处理。观察数据发现没有任何一个字段表示年龄。年龄应该如何获取。可以观察一下字段。这个年龄是可以通过两个字段计算出来的。使用问卷当前时间的年份,减去出生年份,就可以计算出来年龄。也就是 survey_time 和 birth 这两个字段。

新增的这个字段命名为“age”。用 survey_time 字段的年减去 birth 就可以了。年龄计算出来之后,就可以把这两个字段删掉了。然后把配偶、父亲、母亲的出生年份也都删除掉,这些都用不上。

# 年龄 Happiness['age']=pd.to_datetime(Happiness['survey_time']).dt.year-Happiness['birth'] Happiness.drop(['survey_time','birth'],axis=1,inplace=True) Happiness.drop(['s_birth', 'f_birth', 'm_birth'], axis=1, inplace=True)

接下来对字段进行标准化处理。

对数值型特征进行标准化

把所有连续数值特征存入列表,使用 Z-Score 标准化(StandardScaler)完成数据标准化处理。转换后的标准化结果,直接覆盖回原数据集对应的列。这么做可以消除不同特征量纲、数值范围差异,方便后续建模。

# 对数值型特征进行标准化 from sklearn.preprocessing import StandardScaler, MinMaxScaler numeric_cols = [ 'income', 'height_cm', 'weight_jin', 's_income','family_income', 'family_m', 'house', 'car', 'son', 'daughter', 'minor_child', 'inc_exp', 'public_service_1', 'public_service_2', 'public_service_3', 'public_service_4', 'public_service_5', 'public_service_6', 'public_service_7', 'public_service_8', 'public_service_9', 'floor_area' ] Happiness[numeric_cols] = (StandardScaler().fit_transform(Happiness.loc[:, numeric_cols]))

如果对这一步不熟悉,可以查看本篇开头预备知识里的数据标准化的那部分内容。

标准化处理完成之后,整个的数据清洗阶段也就完成了。开始下一个阶段,建模。

建模(KNN,随机森林)

KNN 方法

KNN 算法介绍

建模处理需要看一下用到的算法。使用的是 KNN(K近邻)算法。它是监督学习,而且既不属于分类也不属于回归。思想很简单,就是为了找到它的邻居,这里有一个欧氏距离的概念,这是距离的公式。可以看一下。

KNN 算法,它的理论就是通过这个公式去算距离。给定一个数据集,对于一个新来到的样本,模型在数据集中找到距离该样本最近的 K 个样本,在这 K 个样本中某一类出现的次数最多就把这个新的样本分到这个类别中,少数服从多数原则。

比如下图这个黄色圆形属于哪个类别?如果是在最里面实心圆圈的范围内,这个黄色圆形就会分在橘色三角这个类别里面。因为这个实心圆圈的范围里面,橘色三角形有两个,绿色方形只有一个,遵循少数服从多数原则,这个黄色圆形样本就是属于橘色三角形类别的。如果设置了 K=5,就要找它最近的 5 个邻居,就不在实心圆圈的范围了,而是在虚线圆圈的范围里。这个时候绿色方形有 3 个,而橘色三角形只有 2 个,这时候预测出来的黄色圆形,它就不再属于橘色三角形的类别,而是绿色方形类别。也就是这个黄色圆形,在 K=3 的时候预测出来是橘色三角形,在 K=5 的时候预测出来是绿色方形。K 值的选择会影响预测出来的类别。

KNN 中 K 值的选取对分类的结果影响至关重要,K 值选取的太小,模型太复杂,K 值选取的太大,导致分类模糊。

提取特征列目标列

回到 jupyter 上来,先把特征列和目标列提取出来。首先是特征列,把目标列删除,id 也删除,剩下的全是特征列。接下来目标列,直接去获取就可以了。然后打印一下 y 对应各个值出现的次数,看一看这个数据。

# 建模 from sklearn.model_selection import train_test_split X=Happiness.drop(['happiness','id'],axis=1) y=Happiness['happiness'] print(y.value_counts())

打印出来 y 的次数可以发现,它这个分布是不均匀的。分布不均匀的话,之前介绍过过拟合的概念,过拟合就是在测试集效果不好,在训练集效果好。在这里,模型会偏向预测大类、完全忽略小类,这时用过采样解决。

过采样

过采样:复制、生成少数类样本,人为扩充少数类数量,让两类样本数量接近均衡;核心思路:增加少数类数据,消除类别数量差距,使数据达到平衡。看一下这个图。

可以看到,(a) 就是单纯取了附近的几个点直接进行计算,在 (b) 中,它还是那几个点,但是在中间加了个正方形。它在两个数据点之间插入了这个点,这个点就是数据,这个操作叫插值法,通过这个方法把类别数据增加,实现过采样。用这个方式不会影响模型性能,它会让数据的每个类别更加均衡,更加平均一点儿。

实现过采样,导入过采样的包,使用 SMOTE 来进行处理。使用的时候,随机种子设置为 0 就可以了,把特征拿过来进行拟合然后重采样。再来打印一下 y 各个值出现的次数。这里有个警告,不用管。能运行就行。

from imblearn.over_sampling import SMOTE over_samples=SMOTE(random_state=0) X,y=over_samples.fit_resample(X,y) # 重采样 print(y.value_counts())

再次打印 y 的次数,发现这几类样本数量变得非常均匀了。

让数据均衡有两种方式,一种是把多的类别数据量变少,一种是把少的变多。本次是把少的变多。因为重采样的方法是通过 SMOTE 这样的方式,SMOTE 是经典过采样算法,算法逻辑固定:通过插值生成新的少数类样本,把少数类样本数量提升至和多数类持平,实现均衡。让每一类数据都是一样多的。其实实际上在处理这类问题的时候,不一定必须一样多的,相差一两个或者三四个都是没问题的。但是像之前那样,样本量一个是几百个一个是几千,这就差别太大了。

这次是数据量都一样了,换成别的数据集有可能有少一点儿或者多一点儿的情况都是正常的。

划分数据集、训练、拟合、预测、评估

数据均衡了之后,下一步就是要划分数据集了,将数据集划分出训练集和测试集。并且使用算法,将算法导入进来。评估方式顺便也导入进来,分类算法评估常用的指标是准确率,把准确率的包导入进来。之后开始建模,这里需要设置它的 K 值,这里的 K 值是用 n_neighbors,设置为 5。这个如果不合适后面可以调整的(肘部法则)。严谨一点儿是要用肘部法则来调整,这里直接设置为 5 了。下一步就是拟合数据,用训练集数据训练模型。接下来可以去预测了,放的是测试集数据存入到 y_hat 里,之后就可以把预测值打印一下。最后准确率也打印一下。

from sklearn.neighbors import KNeighborsClassifier # 分类算法 from sklearn.metrics import accuracy_score X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.2,random_state=0) Knn=KNeighborsClassifier(n_neighbors=5) # 可以通过肘部法则去调整 Knn.fit(X_train,y_train) y_hat=Knn.predict(X_test) print(y_hat) acc=accuracy_score(y_test,y_hat) print(acc)

可以看到,准确率在 80% 以上,模型准确率能达到 80 分,性能还是非常好的。KNN 算法实现就是这么实现的,比较简单。

这样就可以通过输入一个人的特征就能预测出来这个人的幸福情况。还可以尝试一下其他算法,这个案例用随机森林也可以做。

随机森林方法

导入随机森林要用到的包,接下来实例化。这里设置一下随机森林当中的随机种子数,一般来说树模型结构都设置为 42。实例化之后训练模型,训练数据放进去。之后预测,预测出来的结果存在 y_hat2 变量当中。最后算一下准确率,预测的值和准确率打印出来。

# 随机森林算法 from sklearn.ensemble import RandomForestClassifier tree_clf=RandomForestClassifier(random_state=42) tree_clf.fit(X_train,y_train) y_hat2=tree_clf.predict(X_test) print(y_hat2) acc2=accuracy_score(y_test,y_hat2) print(acc2)

可以发现,准确率结果接近 90%,模型性能非常好。它比 KNN 准确率是要高一些的。基本上分类算法都是用随机森林来做,随机森林比绝大多数的分类算法效果都要好一点。