Kaggle实例-家庭贫困水平预测

  • **1. 数据背景**
  • **2. 数据目标**
  • **3. 问题和数据说明**
    • **3.1. 目标说明**
    • **3.2. 评估度量**
  • **4. 数据分析**
    • **4.1. Import**
    • **4.2. 读取数据并查看基本信息**
    • **4.3. 探索标签的分布**
    • **4.4. 解决错误的标签**
      • **4.4.1. 识别错误**
      • **4.4.2. 没有户主的家庭**
      • **4.4.3. 纠正错误**
    • **4.5. 缺失值**
    • **4.6. 两个分类变量**
    • **4.7. 特征工程**
      • **4.7.1. 简单认识**
      • **4.7.2. 定义变量的类别**
      • **4.7.3. 人均特征**
      • **4.7.4. 探索家庭变量**
      • **4.7.5. 个人变量**
      • **4.7.6. 聚合**
      • **4.7.7. 特征选择**
      • **4.7.8. 最后的探索**
    • **4.8. 机器学习模型**
      • **4.8.1. 特征重要性**
      • **4.8.2. 模型选择**
      • **4.8.3. 模型的性能比较**
      • **4.8.4. 预测前的准备**
      • **4.8.5. 优化模型:Light Gradient Boosting Machine**
  • **5. 不足及未来的改进**
    • **5.1. 特征选择方面**
    • **5.2. 降维**

有待完善(图片未上传)~

问题链接:costa-rican-household-poverty-prediction
参考文献:
参考了其他参赛者的笔记:

  1. A Complete Introduction and Walkthrough
  2. Costa Rica Poverty Exploration Kernel

1. 数据背景

许多社交项目很难确保为合适的人提供足够的援助。当一个项目关注最贫困的人群时,这一点尤其棘手。世界上最贫穷的人通常无法提供必要的收入和支出记录来证明他们符合条件。
在拉丁美洲,一种流行的方法使用算法来验证收入资格。它被称为代理手段测试(或PMT)。通过PMT,代理商使用一种模型,该模型考虑家庭的可观察家庭属性,如墙壁和天花板的材料,或家中发现的资产,以对其进行分类并预测其需求水平。
虽然这是一种改进,但随着该地区人口的增长和贫困的减少,准确性仍然是一个问题。


2. 数据目标

开发一种机器学习模型,利用个人和家庭特征预测家庭贫困水平


3. 问题和数据说明

本次比赛数据分两个文件:train.csv 和 test.csv 。训练集有9557行和143列,测试集有23856行和142列。每一行代表一个个体,每一列是一个特征,要么是对个体唯一,要么是对个体的家庭唯一。训练集有一个额外列:target,表示1-4级贫困水平,为比赛标签。1 是最极端的贫困。
这是一个有监督的多分类机器学习问题:

  • 监督:提供培训数据的标签
  • 多分类:标签是4类离散值

3.1. 目标说明

目标是预测家庭层面的贫困。数据提供了个人层面的数据,每个人都有自己的特点,也有关于他们家庭的信息,因此必须为每个家庭执行一些单个数据的集合。此外,我们必须对测试集中的每一个个体进行预测,根据项目提交数据的要求,只有户主才会被用于评分,这意味着希望在家庭的基础上预测贫困

这里原数据会出现一个可能的重要错误:虽然一个家庭的所有成员在训练数据中都应该有相同的标签,但是在同一个家庭中,个人标签会出现错误。在这些情况下,项目提示说对每个家庭可以通过 parentesco1 = 1 这一列来标识。

**目标值**表示贫困水平如下:
1 = 极端贫困
2 = 中等贫困
3 = 弱势家庭
4 = 非弱势家庭

另外需要注意的几点:

  • id:每个个体的唯一标识符(实际分析应删除)
  • idhogar:每个家庭的唯一标识符。此变量并不是一个特性,用于按家庭对个人进行分组,因为家庭中所有个人都具有相同的标识符
  • parentsc01:表示这个人是否是一家之主
  • target:标签,家庭成员一致
    模型部分应在家庭的基础上训练每个家庭的标签户主贫困水平。原始数据包括家庭和个人特征的混合,对于个人数据,诚如上所说,需找到一种方法为每个家庭聚合这些数据。没有户主的家庭意味着这些数据不能用于训练

3.2. 评估度量

构建模型是为了找出符合该数据集的最优模型,因此需要对最优模型有一个评判标准。这里,我选择了 Macro F1 Score 来进行预测评分。
F1分数(F1-Score),又称为平衡F分数(BalancedScore),它被定义为精确率和召回率的调和平均数,其计算如下所示:

TP、True Positive 真阳性:预测为正,实际也为正
FP、False Positive 假阳性:预测为正,实际为负
FN、False Negative 假阴性:预测与负、实际为正
TN、True Negative 真阴性:预测为负、实际也为负

【一致判真假,预测判阴阳】

精确率P=TP真阳性TP真阳性+FP假阳性精确率P = \frac{TP真阳性}{TP真阳性+FP假阳性} P=TP+FPTP
召回率R=TP真阳性TP真阳性+FN假阴性召回率R = \frac{TP真阳性}{TP真阳性+FN假阴性} R=TP+FNTP
F1=2∗precision∗recallprecision+recallF1 = 2*\frac{precision*recall}{precision+recall} F1=2precision+recallprecisionrecall
对于多类问题,需要对每个类的F1成绩进行平均。在***不考虑标签不平衡*** 的情况下,MF1分数等于每个类的F1分数的平均值。
MacroF1=F1Class1+F2Class2+F3Class3+F4Class44MacroF1 = \frac{F1Class1+F2Class2+F3Class3+F4Class4}{4} MacroF1=4F1Class1+F2Class2+F3Class3+F4Class4
每个标签出现的次数不计入计算(当使用“加权”分数时才使用)。因此需要使用下列代码来评估模型:

from sklearn.metrics import f1_score
f1_score(y_true, y_predicted, average = 'macro`)

由于最终目标时建立一个预测家庭贫困水平的机器学习模型。所以我们首先要理解问题和数据,此外,应该在选择最优模型之前对众多模型进行评估,并对构建模型之后对预测结果进行研究讨论。因此,本项目的分析步骤如下:

1. 理解问题
2. 探索性数据分析
3. 特征工程
4. 比较几种基本的机器学习模型
5. 尝试更复杂的机器学习模型
6. 优化选择的模型
7. 研究问题下的模型预测
8. 得出结论并展望

上述步骤并不是直线型的,而是迭代的,进行下一步骤可能会回退到上层继续考虑,需要不断评估过去的决策并作出改进。特别是特征工程、建模和优化这些经常重复的步骤,不到达最后并不能下正确的结论。


4. 数据分析

在上述理解后,接下来进入探索性数据分许(EDA)和特征工程。
对于EDA,将研究可用于特征工程和建模的任何与现实联系的异常、趋势、相关性或模式。我们将确保在定量(用统计)和视觉(用数字)两方面调查数据。
一旦很好地掌握了数据和任何潜在特征的有用关系,就可以进行一些特征工程(机器学习流水线的最重要的部分)并建立基线模型。


4.1. Import

首先先导入一些常用的python库:Pandasnumpymatplotlibseaborn,和 sklearn

#数据操作
import pandas as pd
import numpy as np#可视化
import matplotlib.pyplot as plt
import seaborn as sns#设置绘图默认值
%matplotlib inline
plt.style.use('fivethirtyeight')
plt.rcParams['font.size'] = 18
plt.rcParams['patch.edgecolor'] = 'k'
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus'] = False

4.2. 读取数据并查看基本信息

pd.options.display.max_columns = 150#读取数据
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train.head()

train.info()

test.info()


1. 整数

train.select_dtypes(include=['int64']).nunique().value_counts().sort_index().plot.bar(color = 'blue', figsize = (8, 6),edgecolor = 'k', linewidth = 2)
plt.xlabel('单个值的分布')
plt.ylabel('数量')
plt.title('整数列中单个值的个数')


只有两列表示布尔值(0或1)。例如,refrig列表示这个家庭是否有冰箱。不需要聚合从家庭层次的布尔列创建特性时,但是需要聚合单个级别上的布尔列。
2. 浮点数列
表示连续变量。使用OrderedDict来将贫困级别映射到颜色,这会使键和值保持与我们指定的顺序相同(与常规Python字典不同)。下图显示了目标值着色的浮动列的分布。通过这些图,我们可以看到,不同家庭贫困程度的变量分布是否存在显著差异。

from collections import OrderedDictplt.figure(figsize = (20, 16))
plt.style.use('fivethirtyeight')# 颜色字典
colors = OrderedDict({1: 'red', 2: 'orange', 3: 'blue', 4: 'green'})
poverty_mapping = OrderedDict({1: 'extreme', 2: 'moderate', 3: 'vulnerable', 4: 'non vulnerable'})# 遍历浮点数列
for i, col in enumerate(train.select_dtypes(include=['floating'])):ax = plt.subplot(4, 2, i + 1)# 遍历贫困水平for poverty_level, color in colors.items():# 每一个贫困水平单独画一条线sns.kdeplot(train.loc[train['Target'] == poverty_level, col].dropna(),ax = ax, color = color, label = poverty_mapping[poverty_level])plt.title(f'{col.capitalize()} Distribution'); plt.xlabel(f'{col}'); plt.ylabel('Density')plt.subplots_adjust(top = 2)





由上图可知哪些变量可能与模型最“相关”。例如,代表家庭中成年人平均受教育程度的meaneduc似乎与贫困程度有关:较高的成人平均受教育程度会导致较低贫困程度
3. 对象列

train.select_dtypes(include=['object']).head()


Ididhogar 对象类型有意义,因为它们是标识变量。然而,其他列似乎是字符串和数字的组合。由项目数据文档可知:

  • dependency: Dependency rate,=(小于19岁或大于64岁的家庭成员人数)/(大于19岁的家庭成员人数)
  • edjefe: 男户主受教育年限,基于escolari(受教育年限)、户主与性别的交互作用,yes=1, no=0
  • edjefa: 女户主受教育年限,基于户主escolari(受教育年限)与性别的交互作用,yes=1, no=0

对于这三个变量,“yes”= 1,“no”= 0。我们可以使用映射纠正变量并将其转换为浮点数。

mapping = {"yes": 1, "no": 0}# 在训练集和测试集应用相同操作
for df in [train, test]:# 用正确的映射填充值df['dependency'] = df['dependency'].replace(mapping).astype(np.float64)df['edjefa'] = df['edjefa'].replace(mapping).astype(np.float64)df['edjefe'] = df['edjefe'].replace(mapping).astype(np.float64)train[['dependency', 'edjefa', 'edjefe']].describe()

plt.figure(figsize = (16, 12))# 遍历浮点数列
for i, col in enumerate(['dependency', 'edjefa', 'edjefe']):ax = plt.subplot(3, 1, i + 1)# 遍历贫困水平for poverty_level, color in colors.items():# 每一个贫困水平单独画一条线sns.kdeplot(train.loc[train['Target'] == poverty_level, col].dropna(),ax = ax, color = color, label = poverty_mapping[poverty_level])plt.title(f'{col.capitalize()} Distribution'); plt.xlabel(f'{col}'); plt.ylabel('Density')plt.subplots_adjust(top = 2)



特征工程应对训练集和测试集应用相同的操作,得到相同的特征,再根据目标分理出集合。

# 向测试集添加空目标列
test['Target'] = np.nan
data = train.append(test, ignore_index = True)

4.3. 探索标签的分布

接下来,我们可以通过查看标签的分布来了解问题的不平衡程度。有四个可能的整数级别,表示四个不同的贫困级别。
为了查看正确的标签,我们将只对parentesco1 == 1的列进行子集划分,因为这是一个家庭的负责人。

下面的条形图显示了训练标签的分布(因为没有测试标签)。

heads = data.loc[data['parentesco1'] == 1].copy()# 训练标签
train_labels = data.loc[(data['Target'].notnull()) & (data['parentesco1'] == 1), ['Target', 'idhogar']]# 目标值个数
label_counts = train_labels['Target'].value_counts().sort_index()# 每个标签出现的条形图
label_counts.plot.bar(figsize = (8, 6), color = colors.values(),edgecolor = 'k', linewidth = 2)# 格式化
plt.xlabel('Poverty Level'); plt.ylabel('Count');
plt.xticks([x - 1 for x in poverty_mapping.keys()], list(poverty_mapping.values()), rotation = 60)
plt.title('Poverty Level Breakdown');label_counts


贫困水平条形图表示这是一个极度不平衡的类别问题。与其他家庭相比,被列为非弱势群体的家庭要多得多,极端贫困阶层是最少的
不平衡分类问题的一个问题是,机器学习模型很难预测少数类,因为它看到的例子少得多。
从现实的角度来考虑这个问题:如果我们对贫困进行分类,我们看到的没有贫困的情况比极端贫困的情况多得多,那么由于接触较少,我们将更难确定高贫困家庭。
解决类不平衡的一种潜在方法是通过过度采样


4.4. 解决错误的标签

一般的数据集里面的数据很大可能会出现一定的问题。这就需要花大量的时间用于清洗数据。
对于这个问题,该数据集存在有些标签是不正确的,因为同一家庭的每个人有不同的贫困水平。并没有具体的原因说明为什么会出现这个情况,但文档中提出用户主作为真正的标签。

4.4.1. 识别错误

首先,为了找到家庭成员标签不同的家庭,我们可以根据家庭对数据进行分组,然后检查目标是否只有一个唯一的值。

all_equal = train.groupby('idhogar')['Target'].apply(lambda x: x.nunique() == 1)# 目标不完全相同的家庭
not_equal = all_equal[all_equal != True]
print('总共有{}个家庭存在家庭成员标签值不同'.format(len(not_equal)))


先来看一个具体的例子:

train[train['idhogar'] == not_equal.index[0]][['idhogar', 'parentesco1', 'Target']]


文档提示正确的标签应该是 parentesco1 == 1 。对于这个家庭,所有成员的正确标签是3。我们可以通过给同一户人家的每个人分配户主的标签来纠正所有的标签差异。

4.4.2. 没有户主的家庭

households_leader = train.groupby('idhogar')['parentesco1'].sum()# 找出没有户主的家庭
households_no_head = train.loc[train['idhogar'].isin(households_leader[households_leader == 0].index), :]print('总共有{}个没有户主的家庭'.format(households_no_head['idhogar'].nunique()))

# 寻找那些标签不同的没有户主的家庭
households_no_head_equal = households_no_head.groupby('idhogar')['Target'].apply(lambda x: x.nunique() == 1)
print('总共有{}标签不同的没有户主的家庭'.format(sum(households_no_head_equal == False)))


这就意味着不必担心一个没有户主的家庭,也不用担心每个家庭成员的标签价值不同!
如果一个家庭没有户主,那么就没有真正的标签。因此,实际上不会使用任何没有负责人的家庭进行训练。

4.4.3. 纠正错误

现在来更正那些有户主和家庭成员贫困程度不同的家庭的标签。

# 遍历每个家庭
for household in not_equal.index:# 找到正确的标签(适用于户主)true_target = int(train[(train['idhogar'] == household) & (train['parentesco1'] == 1.0)]['Target'])# 为所有家庭成员设置正确的标签train.loc[train['idhogar'] == household, 'Target'] = true_target# 按户分组,求出唯一值的个数
all_equal = train.groupby('idhogar')['Target'].apply(lambda x: x.nunique() == 1)# 目标不完全相同的家庭
not_equal = all_equal[all_equal != True]
print('总共有{}个家庭,家庭成员并不都有相同的目标'.format(len(not_equal)))


由于只使用家庭户主作为标签,所以这个步骤不是完全必要的。


4.5. 缺失值

填充缺失值:

# 每列缺失的数量
missing = pd.DataFrame(data.isnull().sum()).rename(columns = {0: 'total'})# 创建缺失百分比
missing['percent'] = missing['total'] / len(data)missing.sort_values('percent', ascending = False).head(10).drop('Target')


这里不需要担心Target,因为我们为测试数据做了NaN。但是,需要处理丢失值的百分比很高的3列。
v18q1:表示一个家庭是否拥有平板电脑。因为这是一个家庭变量,所以只有在家庭层面上才有意义,所以我们只选择户主的行。

def plot_value_counts(df, col, heads_only = False):# 选择户主行if heads_only:df = df.loc[df['parentesco1'] == 1].copy()plt.figure(figsize = (8, 6))df[col].value_counts().sort_index().plot.bar(color = 'blue',edgecolor = 'k',linewidth = 2)plt.xlabel(f'{col}')plt.title(f'{col} Value Counts')plt.ylabel('Count')plt.show()
plot_value_counts(heads, 'v18q1')


只看现有的数据,最常见的平板电脑数量应该是1。然而,还需要考虑缺失的数据。
在这种情况下,NAN 的家庭可能根本就没有平板电脑。结合 v18q 的定义和药片的数量,看看我们的假设是否成立。
所以可以根据 v18q 的值分组(1表示拥有平板电脑,0表示没有平板电脑),然后计算 v18q1 的空值数量。
这将告诉我们null值是否表示该家族不拥有平板电脑。

heads.groupby('v18q')['v18q1'].apply(lambda x: x.isnull().sum())


v18q1nan 的家庭都没有平板电脑。因此,我们可以用0来填充这个缺失的值。

data['v18q1'] = data['v18q1'].fillna(0)

v2a1:每月的租金支付
除了查看每月租金支付的缺失值之外,还可以查看tipovivi_的分布情况,显示房屋所有权/出租状态的列。这幅图展示了那些v2a1nan的状态。

# 定义房屋拥有着的变量
own_variables = [x for x in data if x.startswith('tipo')]# 绘图,以表示房屋未付租金
data.loc[data['v2a1'].isnull(), own_variables].sum().plot.bar(figsize = (10, 8),color = 'green',edgecolor = 'k', linewidth = 2)
plt.xticks([0, 1, 2, 3, 4],['拥有并全付', '拥有并正在付', '租房', '不稳定', '其他'],rotation = 60)
plt.title('未缴房租的家庭拥有状况', size = 18)


房屋所有权变量的意义如下:

tipovivi1, =1 拥有并支付了全部费用的房子
tipovivi2, =1 拥有,分期付款
tipovivi3, =1 租
tipovivi4, =1 不稳定的
tipovivi5, =1 其他(分配、借)

大多数情况下,没有月租的家庭通常拥有自己的房子。
对于已经拥有且每月租金未支付的房屋,我们可以将租金支付的价值设为零。
对于其他家庭,我们可以保留缺失值以进行估算,但是我们将添加一个标志列,指示这些家庭存在缺失值。

# 用0租金填写拥有房子的家庭
data.loc[(data['tipovivi1'] == 1), 'v2a1'] = 0# 创建缺失的租金支付栏
data['v2a1-missing'] = data['v2a1'].isnull()data['v2a1-missing'].value_counts()


rez_esc:离开学校的年数
对于零值的家庭,他们可能目前没有孩子上学。可以通过找出不为空值行的年龄来验证这一点。

data.loc[data['rez_esc'].notnull()]['age'].describe()


上图可知,缺失值的最大年龄是17岁。对于比这个年龄大的人,也可以假设他们根本就没在上学。
之后考虑 rez_esc 列为空的人的年龄。

data.loc[data['rez_esc'].isnull()]['age'].describe()


看了一些关于这个部分的讨论,发现这个变量只适用于7到19岁的人。任何小于或大于这个范围的人都应该没有离校时间,因此这个值应该设置为0。
对于这个变量,如果个体大于19,并且他们该值缺失,或者如果他们小于7,并且该值缺失,我们可以把它设为0;对于其他人,将保留原值,并添加一个布尔标记。

# 如果年龄在19岁以上或7岁以下,并且有缺失值,则将其设置为0
data.loc[((data['age'] > 19) | (data['age'] < 7)) & (data['rez_esc'].isnull()), 'rez_esc'] = 0# 其他人添加标志
data['rez_esc-missing'] = data['rez_esc'].isnull()

rez_esc 列中还有一个离群值。同样,由讨论可知这个变量的最大值是5。因此,任何大于5的值都应该设置为5。

data.loc[data['rez_esc'] > 5, 'rez_esc'] = 5

4.6. 两个分类变量

画出两个分类变量的散点图,点的大小表示由每个x值表示的给定y值的百分比。

def plot_categoricals(x, y, data, annotate = True):"""画出两个分类变量数量关系图大小是每个分组的原始计数百分比代表给定的y值"""raw_counts = pd.DataFrame(data.groupby(y)[x].value_counts(normalize = False))raw_counts = raw_counts.rename(columns = {x: 'raw_count'})# 计算每组x和y的计数counts = pd.DataFrame(data.groupby(y)[x].value_counts(normalize = True))counts = counts.rename(columns = {x: 'normalized_count'}).reset_index()counts['percent'] = 100 * counts['normalized_count']counts['raw_count'] = list(raw_counts['raw_count'])plt.figure(figsize = (14, 10))# 按百分比划分的散点图plt.scatter(counts[x], counts[y], edgecolor = 'k', color = 'lightgreen',s = 100 * np.sqrt(counts['raw_count']), marker = 'o',alpha = 0.6, linewidth = 1.5)if annotate:for i, row in counts.iterrows():# 使用适当的偏移量plt.annotate(xy = (row[x] - (1 / counts[x].nunique()), row[y] - (0.15 / counts[y].nunique())),color = 'navy',s = f"{round(row['percent'], 1)}%")# 设置刻度线plt.yticks(counts[y].unique())plt.xticks(counts[x].unique())# 将最小值和最大值转换为均方根空间中最大最小sqr_min = int(np.sqrt(raw_counts['raw_count'].min()))sqr_max = int(np.sqrt(raw_counts['raw_count'].max()))msizes = list(range(sqr_min, sqr_max,int(( sqr_max - sqr_min) / 5)))markers = []# 标记for size in msizes:markers.append(plt.scatter([], [], s = 100 * size, label = f'{int(round(np.square(size) / 100) * 100)}', color = 'lightgreen',alpha = 0.6, edgecolor = 'k', linewidth = 1.5))# 格式plt.legend(handles = markers, title = 'Counts',labelspacing = 3, handletextpad = 2,fontsize = 16,loc = (1.10, 0.19))plt.annotate(f'* Size represents raw count while % is for a given y value.',xy = (0, 1), xycoords = 'figure points', size = 10)# 调整限制plt.xlim((counts[x].min() - (6 / counts[x].nunique()), counts[x].max() + (6 / counts[x].nunique())))plt.ylim((counts[y].min() - (4 / counts[y].nunique()), counts[y].max() + (4 / counts[y].nunique())))plt.grid(None)plt.xlabel(f"{x}"); plt.ylabel(f"{y}"); plt.title(f"{y} vs {x}");
plot_categoricals('rez_esc', 'Target', data)

标记的大小表示原始计数。选择给定的y值,然后读取整行。

例如,在贫困水平为1的情况下,93%的人没有离校年数,总数约为800人,约0.4%的人落后5年,约为50人。

plot_categoricals('escolari', 'Target', data, annotate = False)


每个列中其余缺失的值将被填充,最简单、最有效的填充方法之一是用列的中值来填充。
这里可以绘制出存在缺失值情况下的目标分布。

plot_value_counts(data[(data['rez_esc-missing'] == 1)],'Target')


如上的分布似乎与所有数据一致。

plot_value_counts(data[(data['v2a1-missing'] == 1)],'Target')


缺失值也能展现出一些正确的信息,因为上图显示 2: 中等贫困 较高。


4.7. 特征工程

这里既可以手工进行特征工程,也可以使用一个开源的 pythonFeaturetools 实现自动创建特征(数据位于结构化表中),详细认识可查看文档 [https://www.featuretools.com/][1]

4.7.1. 简单认识

由上可知必须使用数据计算定义在个人级别和家庭级别的列。以不同的方式处理其中一些变量。一旦在每一层定义了变量,就可以根据需要开始聚合它们。

过程如下:

  1. 将变量分解为家庭层次和个体层次
  2. 为单个级别的数据找到合适的聚合
    • 顺序变量可以使用统计聚合
    • 布尔变量也可以聚合
  3. 将单个聚合连接到家庭级别的数据

4.7.2. 定义变量的类别

变量有以下不同的类别:

  1. 个体变量:这些是每个个体的特征,而不是家庭的特征

    • 布尔值:0或1
    • 有序离散型:有顺序的整数
  2. 家庭变量
    • 布尔值:0或1
    • 有序离散型:有顺序的整数
    • 连续的数字
  3. 变量平方:数据中变量的平方
  4. Id变量:标识数据,不应作为特征使用

下面我们将手动定义每个类别中的变量。

id_ = ['Id', 'idhogar', 'Target']
ind_bool = ['v18q', 'dis', 'male', 'female', 'estadocivil1', 'estadocivil2', 'estadocivil3','estadocivil4', 'estadocivil5', 'estadocivil6', 'estadocivil7','parentesco1', 'parentesco2',  'parentesco3', 'parentesco4', 'parentesco5','parentesco6', 'parentesco7', 'parentesco8',  'parentesco9', 'parentesco10','parentesco11', 'parentesco12', 'instlevel1',  'instlevel2', 'instlevel3', 'instlevel4', 'instlevel5', 'instlevel6', 'instlevel7', 'instlevel8', 'instlevel9', 'mobilephone', 'rez_esc-missing']ind_ordered = ['rez_esc', 'escolari', 'age']
hh_bool = ['hacdor', 'hacapo', 'v14a', 'refrig', 'paredblolad', 'paredzocalo', 'paredpreb','pisocemento', 'pareddes', 'paredmad',
'paredzinc', 'paredfibras', 'paredother', 'pisomoscer', 'pisoother',
'pisonatur', 'pisonotiene', 'pisomadera', 'techozinc', 'techoentrepiso', 'techocane', 'techootro', 'cielorazo', 'abastaguadentro', 'abastaguafuera', 'abastaguano', 'public', 'planpri', 'noelec', 'coopele', 'sanitario1', 'sanitario2', 'sanitario3', 'sanitario5', 'sanitario6', 'energcocinar1', 'energcocinar2', 'energcocinar3', 'energcocinar4', 'elimbasu1', 'elimbasu2', 'elimbasu3', 'elimbasu4', 'elimbasu5', 'elimbasu6', 'epared1', 'epared2', 'epared3', 'etecho1', 'etecho2', 'etecho3', 'eviv1', 'eviv2', 'eviv3', 'tipovivi1', 'tipovivi2', 'tipovivi3', 'tipovivi4', 'tipovivi5', 'computer', 'television', 'lugar1', 'lugar2', 'lugar3', 'lugar4', 'lugar5', 'lugar6', 'area1', 'area2', 'v2a1-missing']hh_ordered = [ 'rooms', 'r4h1', 'r4h2', 'r4h3', 'r4m1','r4m2','r4m3', 'r4t1',  'r4t2', 'r4t3', 'v18q1', 'tamhog','tamviv','hhsize','hogar_nin', 'hogar_adul','hogar_mayor','hogar_total',  'bedrooms', 'qmobilephone']hh_cont = ['v2a1', 'dependency', 'edjefe', 'edjefa', 'meaneduc', 'overcrowding']
sqr_ = ['SQBescolari', 'SQBage', 'SQBhogar_total', 'SQBedjefe', 'SQBhogar_nin', 'SQBovercrowding', 'SQBdependency', 'SQBmeaned', 'agesq']
x = ind_bool + ind_ordered + id_ + hh_bool + hh_ordered + hh_cont + sqr_from collections import Counterprint('无重复变量: ', np.all(np.array(list(Counter(x).values())) == 1))
print('覆盖了所有变量 ', len(x) == data.shape[1])


1. 平方变量
这里将移除所有的平方变量。有时变量被平方或转换为特征工程的一部分,因为它可以帮助线性模型学习非线性关系。然而,由于将使用更复杂的模型,这些平方特征是多余的。它们与非平方版本高度相关,因此实际上会通过添加无关信息和减慢训练速度来损害模型。
举个例子,SQBageage 的关系。

sns.lmplot('age', 'SQBage', data = data, fit_reg=False)
plt.title('年龄和年龄的平方')


这些变量是高度相关的,所以不需要在数据中同时保留这两个变量。

#设定axis是为了确定要删的标签是属于column还是index
# 移除平方变量
data = data.drop(labels = sqr_ , axis=1)
data.shape

2. ID变量
将保持数据中ID变量的原样,用来识别。

3. 家庭水平变量
先关注家庭户主的子集,再考虑家庭水平变量

heads = data.loc[data['parentesco1'] == 1, :]
heads = heads[id_ + hh_bool + hh_cont + hh_ordered]
heads.shape


对于大多数家庭级别变量,可以简单地保持原样:因为希望对每个家庭进行预测,所以需要使用这些变量作为特征。但是,也可以删除一些冗余的变量,并从现有数据中添加更多的特征。

4. 冗余家庭变量处理

先显示所有家庭变量之间的相关性。如果有任何高度相关的变量,那么可能需要删除一对高度相关的变量之一。
下面的代码找出任何相关性绝对值大于0.95的变量。

# 创建相关性矩阵
corr_matrix = heads.corr()# 选择相关矩阵的上三角
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))# 找到相关性大于0.95的特征列
to_drop = [column for column in upper.columns if any(abs(upper[column]) > 0.95)]to_drop


这些显示了每一对相关变量中的一个。为了找到另一对,我们可以对 corr_matrix 进行子集化。

corr_matrix.loc[corr_matrix['tamhog'].abs() > 0.9, corr_matrix['tamhog'].abs() > 0.9]

sns.heatmap(corr_matrix.loc[corr_matrix['tamhog'].abs() > 0.9, corr_matrix['tamhog'].abs() > 0.9], annot=True, cmap = plt.cm.autumn_r, fmt='.3f')


这里有几个变量与房子的大小有关:

  • r4t3,家庭总人数
  • tamhog,家庭规模
  • tamviv,居住在这个家庭的人数
  • hhsize,住户人数
  • hogar_total,家庭总人数的#

这些变量都是高度相关的。事实上,hhsize与tamhog和hogar_total有着完美的相关性,所以将删除这两个变量。还可以删除 r4t3 ,因为它与 hhsize 的相关性非常高。tamviv 不一定和 hhsize 相同,因为可能有家庭成员不在家里。

heads = heads.drop(labels = ['tamhog', 'hogar_total', 'r4t3'] , axis=1)
sns.lmplot('tamviv', 'hhsize', data, fit_reg=False, size = 8)
plt.title('家庭规模与居住在家庭中的人数之比')


由图可知,有更多的人生活在家庭里比生活在家里。这提供了一个新特征:这两个度量值之间的差异

heads['hhsize-diff'] = heads['tamviv'] - heads['hhsize']
plot_categoricals('hhsize-diff', 'Target', heads)

corr_matrix.loc[corr_matrix['coopele'].abs() > 0.9, corr_matrix['coopele'].abs() > 0.9]


这个变量表示家里的电是从哪里来的。有四种选择,没有这两种选择之一的家庭要么没有电力(noelec),要么从私人工厂(planpri)获得电力。

5. 序数变量
这里将通过创建序数变量将这四个变量压缩为1。

0: No electricity
1: Electricity from cooperative
2: Electricity from CNFL, ICA, ESPH/JASEC
3: Electricity from private plant

有序变量具有固有的顺序。创建这个新的有序变量之后,可以删除其他四个变量。有几个家庭在这里没有该变量,所以我们将使用 nan 并添加一个布尔列,表示这个变量没有度量。

elec = []for i, row in heads.iterrows():if row['noelec'] == 1:elec.append(0)elif row['coopele'] == 1:elec.append(1)elif row['public'] == 1:elec.append(2)elif row['planpri'] == 1:elec.append(3)else:elec.append(np.nan)# 记录新变量和丢失的标志
heads['elec'] = elec
heads['elec-missing'] = heads['elec'].isnull()
plot_categoricals('elec', 'Target', heads)


可以看出,对于目标的每一个值,最常见的电力来源是来自所列供应商之一。
最后一列是 area2 。这意味着房子在农村地区,但它是多余的,因为我们有一个列表示房子是否在城市地区。因此,我们可以删除这一列。

heads = heads.drop(labels = 'area2', axis = 1)heads.groupby('area1')['Target'].value_counts(normalize = True)


似乎城市地区的家庭(值为1)比农村地区的家庭(值为0)更有可能处于较低的贫困水平。
房子的墙壁、屋顶和地板各有3列:第一列表示“bad”,第二根表示“regular”,第三根表示“good”。它们有一个固有的顺序:bad < regular < good,为此转换成序数变量,并删除原始变量。

heads['walls'] = np.argmax(np.array(heads[['epared1', 'epared2', 'epared3']]), axis = 1)
plot_categoricals('walls', 'Target', heads)

heads['roof'] = np.argmax(np.array(heads[['etecho1', 'etecho2', 'etecho3']]), axis = 1)
heads = heads.drop(labels = ['etecho1', 'etecho2', 'etecho3'], axis = 1)heads['floor'] = np.argmax(np.array(heads[['eviv1', 'eviv2', 'eviv3']]), axis = 1)

可以将刚刚创建的前三个特征加起来,以得到房子结构质量的总体度量。

# 创建新特征
heads['walls+roof+floor'] = heads['walls'] + heads['roof'] + heads['floor']plot_categoricals('walls+roof+floor', 'Target', heads, annotate=False)


图显示,它为4(最低贫困水平)的目标往往具有更高的“房屋质量”。

counts = pd.DataFrame(heads.groupby(['walls+roof+floor'])['Target'].value_counts(normalize = True)).rename(columns = {'Target': 'Normalized Count'}).reset_index()
counts.head()


下一个变量是关于房子质量的警告。这将是一个负值,-1表示没有厕所、电力、地板、供水和天花板。

# 没有厕所,没有电,没有地板,没有自来水,没有天花板
heads['warning'] = 1 * (heads['sanitario1'] + (heads['elec'] == 0) + heads['pisonotiene'] + heads['abastaguano'] + (heads['cielorazo'] == 0))
plt.figure(figsize = (10, 6))
sns.violinplot(x = 'warning', y = 'Target', data = heads);
plt.title('Target vs Warning Variable')

plot_categoricals('warning', 'Target', data = heads)


可以看到一个高度集中的家庭,他们没有任何警示信号,贫困水平最低。
现在能做的最后一个家庭功能是,一个家庭因为拥有冰箱、电脑、平板电脑或电视而得到一分

# 拥有冰箱、电脑、平板电脑和电视
heads['bonus'] = 1 * (heads['refrig'] + heads['computer'] + (heads['v18q1'] > 0) + heads['television'])
sns.violinplot('bonus', 'Target', data = heads,figsize = (10, 6))
plt.title('Target vs Bonus Variable')

4.7.3. 人均特征

我们可以计算出每个家庭中每个人的特定度量的数量。

heads['phones-per-capita'] = heads['qmobilephone'] / heads['tamviv']
heads['tablets-per-capita'] = heads['v18q1'] / heads['tamviv']
heads['rooms-per-capita'] = heads['rooms'] / heads['tamviv']
heads['rent-per-capita'] = heads['v2a1'] / heads['tamviv']

4.7.4. 探索家庭变量

1. 测量关系

这里用两种方法测量:

  1. 皮尔逊相关:从-1到1测量两个变量之间的线性关系
  2. Spearman相关:从-1到1测量两个变量之间的单调关系

如果一个变量增加,另一个变量也增加,那么斯皮尔曼相关系数就是1,即使这个关系不是线性的。另一方面,如果增长完全是线性的话皮尔逊相关性只能是1。

首先,将计算每个变量与目标的Pearson相关性。

from scipy.stats import spearmanrdef plot_corrs(x, y):# 计算相关性spr = spearmanr(x, y).correlationpcr = np.corrcoef(x, y)[0, 1]# 画图data = pd.DataFrame({'x': x, 'y': y})plt.figure( figsize = (6, 4))sns.regplot('x', 'y', data = data, fit_reg = False)plt.title(f'Spearman: {round(spr, 2)}; Pearson: {round(pcr, 2)}')
# 只用训练集
train_heads = heads.loc[heads['Target'].notnull(), :].copy()pcorrs = pd.DataFrame(train_heads.corr()['Target'].sort_values()).rename(columns = {'Target': 'pcorr'}).reset_index()
pcorrs = pcorrs.rename(columns = {'index': 'feature'})print('最负相关变量:')
print(pcorrs.head())print('\n最正相关变量:')
print(pcorrs.dropna().tail())



对于负相关,随着变量的增加,目标降低,说明贫困程度增加。因此,随着警告的增加,贫困水平也会上升,因为这意味着房子可能出现不好的迹象。
hogar_nin 是指家庭中0 - 19岁的儿童人数,年龄较小的儿童可能成为导致贫困程度更高的家庭的经济压力来源。或者,社会经济地位较低的家庭有更多的孩子。无论何种解释,家庭规模和贫困之间确实存在联系。

另一方面,对于正相关而言,越高的值意味着越高的目标值,表明贫困程度降低。最具相关性的家庭水平变量为 meaneduc ,即家庭中成年人的平均教育水平。教育水平越高,贫困水平越低。

相关性值的一般指导原则如下:

  • .00 - .19 “很弱”
  • .20 - .39 “弱”
  • .40 - .59 “温和”
  • .60 - .79 “强”
  • .80 - 1.0 “很强”

现在来看看Spearman相关性。

import warnings
warnings.filterwarnings('ignore', category = RuntimeWarning)feats = []
scorr = []
pvalues = []for c in heads:if heads[c].dtype != 'object':feats.append(c)scorr.append(spearmanr(train_heads[c], train_heads['Target']).correlation)pvalues.append(spearmanr(train_heads[c], train_heads['Target']).pvalue)scorrs = pd.DataFrame({'feature': feats, 'scorr': scorr, 'pvalue': pvalues}).sort_values('scorr')

Spearman相关系数的计算还带有一个表示关系显著性水平的 p-value 。任何小于0.05的 p-value 通常都被认为是显著的,这里用p值除以比较的次数,这个过程称为 Bonferroni 校正。

print('最负Spearman相关性:')
print(scorrs.head())
print('\n最正Spearman相关性:')
print(scorrs.dropna().tail())


寻找距离最远的值:

corrs = pcorrs.merge(scorrs, on = 'feature')
corrs['diff'] = corrs['pcorr'] - corrs['scorr']corrs.sort_values('diff').head()

corrs.sort_values('diff').dropna().tail()

sns.lmplot('dependency', 'Target', fit_reg = True, data = train_heads, x_jitter=0.05, y_jitter=0.05)
plt.title('Target vs  依赖性')


很难看出它们之间的关系,但有一点是负面的:随着dependency 的增加,Target 的价值会降低。dependency 是(小于19岁或大于64岁的家庭成员人数)/(大于19岁的家庭成员人数)。当增加这一价值时,贫困的严重程度往往会增加:有更多依赖家庭成员(他们通常不工作)会导致更高的贫困水平,因为他们必须得到非依赖家庭成员的支持。

sns.lmplot('rooms-per-capita', 'Target', fit_reg = True, data = train_heads, x_jitter=0.05, y_jitter=0.05)
plt.title('Target vs Rooms Per Capita')


2. 描述7个变量,并显示它们与目标之间的相关性:

variables = ['Target', 'dependency', 'warning', 'walls+roof+floor', 'meaneduc', 'floor', 'r4m1', 'overcrowding']corr_mat = train_heads[variables].corr().round(2)plt.rcParams['font.size'] = 18
plt.figure(figsize = (12, 12))
sns.heatmap(corr_mat, vmin = -0.5, vmax = 0.8, center = 0, cmap = plt.cm.RdYlGn_r, annot = True)


这个图说明有许多变量与Target的相关性较弱。某些变量(如floorwalls+roof+floor)之间也存在高度相关性,这可能会因为共线性而引起问题。

至此,家庭变量水平的特征工程暂时结束。

4.7.5. 个人变量

由上面分类可知,有两种类型的个人变量:布尔值(1或0表示真或假)和序数值(有意义排序的离散值)。

ind = data[id_ + ind_bool + ind_ordered]
ind.shape

1. 冗余的个人变量

只关注任何相关系数绝对值大于0.95的变量。

corr_matrix = ind.corr()upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))to_drop = [column for column in upper.columns if any(abs(upper[column]) > 0.95)]to_drop
ind = ind.drop(labels = 'male', axis = 1)

2. 创建序数变量

将重点讨论instlevel_变量,它表示个人受教育的程度,从instlevel1:无教育水平到instlevel9:研究生教育。

要创建序号变量,对于每个个体,只需找到非零列。教育有一个内在的顺序(越高越好),所以这种转换到序号变量是有意义的。

ind[[c for c in ind if c.startswith('instl')]].head()

ind['inst'] = np.argmax(np.array(ind[[c for c in ind if c.startswith('instl')]]), axis = 1)plot_categoricals('inst', 'Target', ind, annotate = False)


较高的教育水平似乎与较不极端的贫困水平相对应。这只是在个人层面上的,最终将在家庭层面上汇总这些数据。

plt.figure(figsize = (10, 8))
sns.violinplot(x = 'Target', y = 'inst', data = ind)
plt.title('Education Distribution by Target')


可以使用现有的数据创建一些特征。例如,可以用受教育的年限除以年龄。

ind['escolari/age'] = ind['escolari'] / ind['age']plt.figure(figsize = (10, 8))
sns.violinplot('Target', 'escolari/age', data = ind)

也可以用新的变量inst,除以年龄;以及平板电脑和移动电话的结合的变量。

ind['inst/age'] = ind['inst'] / ind['age']
ind['tech'] = ind['v18q'] + ind['mobilephone']
ind['tech'].describe()

4.7.6. 聚合

为了将个人数据合并到家庭数据中,需要对每个家庭汇总数据。最简单的方法是按家庭ididhogar分组,然后对数据进行聚合。
对于有序或连续变量的聚合,可以使用6个,其中5个内置到pandas中,其中一个定义为range_
使用相同的方法聚合布尔列,然后删除冗余列。

range_ = lambda x: x.max() - x.min()
range_.__name__ = 'range_'# 聚合
ind_agg = ind.drop(labels = 'Target', axis = 1).groupby('idhogar').agg(['min', 'max', 'sum', 'count', 'std', range_])
ind_agg.head()

# 重命名列
new_col = []
for c in ind_agg.columns.levels[0]:for stat in ind_agg.columns.levels[1]:new_col.append(f'{c}-{stat}')ind_agg.columns = new_col
ind_agg.head()

4.7.7. 特征选择

作为第一轮的特征选择,可以从每对相关性大于0.95的变量中移除一个。

corr_matrix = ind_agg.corr()upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))to_drop = [column for column in upper.columns if any(abs(upper[column]) > 0.95)]print(f'这里有{len(to_drop)}强相关列需要移除')

将删除这些列,然后与heads数据合并,以创建最终的数据集。

ind_agg = ind_agg.drop(labels = to_drop, axis = 1)
ind_agg = ind_agg.reset_index(drop = False)
ind_feats = list(ind_agg.columns)# 合并家庭id
final = heads.merge(ind_agg, on = 'idhogar', how = 'left')
print('最终特征 ', final.shape)
final.head()

4.7.8. 最后的探索

corrs = final.corr()['Target']
corrs.sort_values().head()corrs.sort_values().dropna().tail()plt.figure(figsize = (10, 6))
sns.boxplot(x = 'Target', y = 'escolari-max', data = final)
plt.title('Max Schooling by Target')plt.figure(figsize = (10, 6))
sns.boxplot(x = 'Target', y = 'meaneduc', data = final)
plt.xticks([0, 1, 2, 3], poverty_mapping.values())
plt.title('Average Schooling by Target')plt.figure(figsize = (10, 6))
sns.boxplot(x = 'Target', y = 'overcrowding', data = final)
plt.xticks([0, 1, 2, 3], poverty_mapping.values())
plt.title('Overcrowding by Target')




可以看到特征工程之后的一些变量与目标高度相关。

另一个可能有用的特征是户主的性别。

head_gender = ind.loc[ind['parentesco1'] == 1, ['idhogar', 'female']]
final = final.merge(head_gender, on = 'idhogar', how = 'left').rename(columns = {'female': 'female-head'})final.groupby('female-head')['Target'].value_counts(normalize=True)
sns.violinplot(x = 'female-head', y = 'Target', data = final)
plt.title('Target by Female Head of Household')

看起来户主为女性的家庭更有可能处于严重的贫困状态。

还可以通过家庭中是否有女性户主来观察平均教育水平的差异。

plt.figure(figsize = (8, 8))
sns.boxplot(x = 'Target', y = 'meaneduc', hue = 'female-head', data = final)
plt.title('Average Education by Target and Female Head of Household', size = 16)


Target的每一项价值来看,女性户主的家庭受教育程度更高。然而总的来说,女性户主的家庭更有可能陷入严重贫困。

final.groupby('female-head')['meaneduc'].agg(['mean', 'count'])


总体而言,女性家长的平均教育水平略高于男性家长。


4.8. 机器学习模型

所有的数据(包括训练和测试)都是为每个家庭汇总的,因此可以直接在模型中使用,先使用一些常规的模型再改进。为了评估模型,将对训练数据使用10-折叠交叉验证。这实际上将对训练数据分割,对模型进行10次训练和测试。10-折叠交叉验证是一种评估模型在测试集上性能的有效方法。希望查看交叉验证中的平均性能以及标准差,以了解折间的分数变化情况。使用Macro F1来评估性能。

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score, make_scorer
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import Imputer
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import Pipeline# 自定义交叉验证评估
scorer = make_scorer(f1_score, greater_is_better=True, average = 'macro')
# 训练标签
train_labels = np.array(list(final[final['Target'].notnull()]['Target'].astype(np.uint8)))# 提取训练数据
train_set = final[final['Target'].notnull()].drop(labels = ['Id', 'idhogar', 'Target'], axis = 1)
test_set = final[final['Target'].isnull()].drop(labels = ['Id', 'idhogar', 'Target'], axis = 1)# 提交样例
submission_base = test[['Id', 'idhogar']].copy()

对于估算缺失值和扩展特性(限制每一列的范围在0和1之间),可以创建一个管道。这将适合于训练数据,并用于转换训练和测试数据。

features = list(train_set.columns)pipeline = Pipeline([('imputer', Imputer(strategy = 'median')), ('scaler', MinMaxScaler())])# 拟合和转换训练数据
train_set = pipeline.fit_transform(train_set)
test_set = pipeline.transform(test_set)

数据没有缺失值,并且在0和1之间缩放。这意味着它可以直接在任何Scikit-Learn模型中使用。

model = RandomForestClassifier(n_estimators=100, random_state=10, n_jobs = -1)
# 10-折叠交叉验证
cv_score = cross_val_score(model, train_set, train_labels, cv = 10, scoring = scorer)print(f'10-折叠交叉验证 F1 Score = {round(cv_score.mean(), 4)} 以及 std = {round(cv_score.std(), 4)}')

4.8.1. 特征重要性

model.fit(train_set, train_labels)feature_importances = pd.DataFrame({'feature': features, 'importance': model.feature_importances_})
feature_importances.head()

norm_fi = plot_feature_importances(feature_importances, threshold=0.95)



最重要的变量是家庭平均受教育程度,其次是家庭中任何人的最高受教育程度。有理由怀疑这些变量是高度相关的(共线),这意味着可以从数据中删除其中一个变量。其他最重要的特征是我们创建的变量和数据中已经存在的变量的组合。
只需要106个~180个特征就可以满足90%的重要性。所以应该删除一些特征。然而,特征的重要性并没有说特征的哪个方向是重要的(例如,不能用这些来判断教育程度的增加或减少是否会导致更严重的贫困),它们只显示模型认为哪些特征是相关的。

kde_target(final, 'meaneduc')

kde_target(final, 'escolari/age-range_')

4.8.2. 模型选择

前面已经尝试了随机森林分类器,它提供了0.35的F1 Score。所以接下来将尝试各种不同模型来试着找到最优模型。

model_results.set_index('model', inplace = True)
model_results['cv_mean'].plot.bar(color = 'orange', figsize = (8, 6),yerr = list(model_results['cv_std']),edgecolor = 'k', linewidth = 2)
plt.title('Model F1 Score Results')
plt.ylabel('Mean F1 Score (with error bar)')
model_results.reset_index(inplace = True)

4.8.3. 模型的性能比较

随机森林似乎最优。没有对任何超参数进行优化,因此模型之间的比较并不完美,但这些结果反映基于树的集成方法(包括梯度增强机)在结构化数据集上表现非常好。
超参数性确实能提高了机器学习模型的性能,所以之后会尝试所有模型的所有可能的参数设置组合。
在大多数情况下最坏的模型可能不会通过调优突然变成最好的模型。之后将使用强大的LightGBM版本。

4.8.4. 预测前的准备

在进行预测时,只对每个家庭进行预测,然后将预测数据与家庭id (idhogar)上的所有个人合并。这将为每个家庭成员设定相同的价值目标。对于没有户主的测试家庭,我们可以将这些预测设置为4,因为它们不会被打分。关于预测提交的部分不再详细介绍。

4.8.5. 优化模型:Light Gradient Boosting Machine

下面的功能是使用Stratified Kfold cross validationearly stopping来训练梯度提升机,以防止对训练数据的过度拟合。该函数执行交叉验证的训练,并以概率记录每个折叠的预测。
梯度提升机的超参数选取困难,一般通过模型优化来实现,这里将使用一组超参数。
n_estimators设置为10000,但实际上不会达到这个数字,因为我们使用的是early stopping,当交叉验证度量对early_stopping_round没有改进时,它将退出训练估计器。

def macro_f1_score(labels, predictions):predictions = predictions.reshape(len(np.unique(labels)), -1 ).argmax(axis = 0)metric_value = f1_score(labels, predictions, average = 'macro')return 'macro_f1', metric_value, True
from sklearn.model_selection import StratifiedKFold
import lightgbm as lgb
from IPython.display import displaydef model_gbm(features, labels, test_features, test_ids, nfolds = 5, return_preds = False, hyp = None):'''模型使用GBM和交叉验证每一折训练都提前停止。超参数可能需要调整'''feature_names = list(features.columns)# 用户指定的超参数选项if hyp is not None:# 使用early stopping,所以不需要多少esimatorsif 'n_estimators' in hyp:del hyp['n_estimators']params = hypelse:# hyperparameters模型params = {'boosting_type': 'dart', 'colsample_bytree': 0.88, 'learning_rate': 0.028, 'min_child_samples': 10, 'num_leaves': 36, 'reg_alpha': 0.76, 'reg_lambda': 0.43, 'subsample_for_bin': 40000, 'subsample': 0.54, 'class_weight': 'balanced'}# 建立模型model = lgb.LGBMClassifier(**params, objective = 'multiclass', n_jobs = -1, n_estimators = 10000,random_state = 10)# 使用stratified kfold cross validationstrkfold = StratifiedKFold(n_splits = nfolds, shuffle = True)# 保存所有的预测predictions = pd.DataFrame()importances = np.zeros(len(feature_names))# 转换为数组进行索引features = np.array(features)test_features = np.array(test_features)labels = np.array(labels).reshape((-1 ))valid_scores = []# 遍历这些折叠for i, (train_indices, valid_indices) in enumerate(strkfold.split(features, labels)):# 用于折叠预测的Dataframefold_predictions = pd.DataFrame()# 训练和验证数据X_train = features[train_indices]X_valid = features[valid_indices]y_train = labels[train_indices]y_valid = labels[valid_indices]# 使用early stopping训练model.fit(X_train, y_train, early_stopping_rounds = 100, eval_metric = macro_f1_score,eval_set = [(X_train, y_train), (X_valid, y_valid)],eval_names = ['train', 'valid'],verbose = 200)# 记录验证的折叠分数valid_scores.append(model.best_score_['valid']['macro_f1'])# 以概率的形式做出预测fold_probabilitites = model.predict_proba(test_features)# 将每个类的每个预测记录为一个单独的列for j in range(4):fold_predictions[(j + 1)] = fold_probabilitites[:, j]# 添加预测所需的信息fold_predictions['idhogar'] = test_idsfold_predictions['fold'] = (i+1)# 将预测作为新行添加到现有预测predictions = predictions.append(fold_predictions)# 特征重要性importances += model.feature_importances_ / nfolds   # 显示折叠信息display(f'Fold {i + 1}, Validation Score: {round(valid_scores[i], 5)}, Estimators Trained: {model.best_iteration_}')feature_importances = pd.DataFrame({'feature': feature_names,'importance': importances})valid_scores = np.array(valid_scores)display(f'{nfolds} cross validation score: {round(valid_scores.mean(), 5)} with std: {round(valid_scores.std(), 5)}.')if return_preds:predictions['Target'] = predictions[[1, 2, 3, 4]].idxmax(axis = 1)predictions['confidence'] = predictions[[1, 2, 3, 4]].max(axis = 1)return predictions, feature_importances# 平均折叠的预测predictions = predictions.groupby('idhogar', as_index = False).mean()predictions['Target'] = predictions[[1, 2, 3, 4]].idxmax(axis = 1)predictions['confidence'] = predictions[[1, 2, 3, 4]].max(axis = 1)predictions = predictions.drop(labels = ['fold'], axis = 1)submission = submission_base.merge(predictions[['idhogar', 'Target']], on = 'idhogar', how = 'left').drop(labels = ['idhogar'], axis =1)# 填入没有户主的个人,因为4不会被记分submission['Target'] = submission['Target'].fillna(4).astype(np.int8)return submission, feature_importances, valid_scores

1. Cross Validation与Early Stopping说明

Cross ValidationEarly Stopping是防止训练集过拟合的最有效方法之一,因为一旦验证分数明显没有提高,Cross Validation就会阻止我们继续增加模型的复杂性。在多个折叠中重复这个过程有助于减少使用单个折叠时产生的偏差。Early Stopping还可以让我们更快地训练模型。总的来说,使用Cross ValidationEarly Stopping是在梯度提升机中选择估计器数量的最佳方法。

%%capture --no-display
predictions, gbm_fi = model_gbm(train_set, train_labels, test_set, test_ids, return_preds=True)

predictions.head()


对于每个折叠,1、2、3、4列表示每个目标的概率。目标是其中具有置信概率的最大值。我们有所有5个折叠的预测,所以可以绘制不同折叠的每个目标的置信度。

plt.rcParams['font.size'] = 18g = sns.FacetGrid(predictions, row = 'fold', hue = 'Target', size = 3, aspect = 4)
g.map(sns.kdeplot, 'confidence')
g.add_legend();plt.suptitle('Distribution of Confidence by Fold and Target', y = 1.05)



在这里看到的是,每个类的置信度都相对较低。模型似乎对Target=4的预测更有信心,因为类不平衡和这个标签的高流行度。

plt.figure(figsize = (24, 12))
sns.violinplot(x = 'Target', y = 'confidence', hue = 'fold', data = predictions)


总的来说,这些结果显示了类问题不平衡的问题:我的模型不能很好地区分那些表示不足的类。

当对每个家庭进行预测时,我对每个折线的预测取平均值。因此,本质上是在使用多个模型,因为每个模型都是在数据的略微不同的折叠上训练的。
这个过程如下面的代码所示。

# 将预测结果除以折线平均
predictions = predictions.groupby('idhogar', as_index = False).mean()# 找到类和相关的概率
predictions['Target'] = predictions[[1, 2, 3, 4]].idxmax(axis = 1)
predictions['confidence'] = predictions[[1, 2, 3, 4]].max(axis = 1)
predictions = predictions.drop(labels = ['fold'], axis = 1)# 根据每个目标绘制confidence图
plt.figure(figsize = (10, 6))
sns.boxplot(x = 'Target', y = 'confidence', data = predictions);
plt.title('Confidence by Target')plt.figure(figsize = (10, 6))
sns.violinplot(x = 'Target', y = 'confidence', data = predictions);
plt.title('Confidence by Target')

在effectm中结合了5个不同的模型,每个模型都对数据的一个不同的子集进行了训练。

%%capture
submission, gbm_fi, valid_scores = model_gbm(train_set, train_labels, test_set, test_ids, return_preds=False)
_ = plot_feature_importances(gbm_fi, threshold=0.95)



gbm似乎认为最重要的特征来自于年龄。教育变量也表现在最重要的特征中。
2. 选择特征
LightGBM的下一步是尝试通过递归特征消除所选择的特征。

%%capture --no-display
submission, gbm_fi_selected, valid_scores_selected = model_gbm(train_selected, train_labels, test_selected, test_ids)
model_results = model_results.append(pd.DataFrame({'model': ["GBM", "GBM_SEL"], 'cv_mean': [valid_scores.mean(), valid_scores_selected.mean()],'cv_std':  [valid_scores.std(), valid_scores_selected.std()]}))
model_results.set_index('model', inplace = True)
model_results['cv_mean'].plot.bar(color = 'orange', figsize = (8, 6),yerr = list(model_results['cv_std']),edgecolor = 'k', linewidth = 2)
plt.title('Model F1 Score Results');
plt.ylabel('Mean F1 Score (with error bar)')
model_results.reset_index(inplace = True)

梯度提升机的巨大优势在这里展示。最后一步,尝试对这两个集合使用10折,并将它们添加到绘图中。

最好的模型似乎是梯度提升机训练与10倍的特征选择。还可以通过优化获得更多的性能。
下一步是,即从机器学习模型中获取最大收益的过程。
3. 模型优化
模型优化是通过交叉验证优化超参数,从机器学习模型中提取最优性能的过程。这是必要的,因为对于每个数据集,最好的模型超参数是不同的。
有多种优化的方式:

  • Manual
  • Grid Search
  • Random Search
  • Automated Optimization

5. 不足及未来的改进

5.1. 特征选择方面

改进模型性能的一个潜在方法是特征选择。这里只尝试保留模型中最有用的特征。首先删除相关性大于0.95的一些列(在特性工程中已经做了一些),然后将使用Scikit-Learn库应用递归特征消除。

首先是相关性。0.95是一个任意的阈值——可以随意更改这些值,并查看性能如何更改。

Sklearn中的RFECV表示交叉验证的递归特征消除。选择器以迭代的方式使用具有重要特征的模型。在每次迭代中,它要么删除一部分特征,要么删除一组特征。迭代一直进行,直到交叉验证得分不再提高。
要创建选择器对象,需要传入模型、每次迭代要删除的特征数量、交叉验证折叠、自定义记分器和任何其他参数来指导选择。

然后,将选择器与其他sklearn模型一样匹配到训练数据上,这将继续特征选择,直到交叉验证分数不再提高。找出验证得分与训练特征的数量之间关系,通过对训练目标的检测,可以得到各特征的排序。基本上代表迭代中平均特征的重要性。特征可以共享相同的排名,并且只保留级别为1的特征。

5.2. 降维

作为问题的最终探索,可以对所选择的数据集应用几种不同的降维方法。这些方法可以用于可视化或作为机器学习的预处理方法。来看看四种不同的方法:

  • PCA: Principal Components Analysis 找到数据中变化最大的维度
  • ICA: Independent Components Analysis 试图将多变量信号分离为独立信号
  • TSNE: TSNE: T-distributed Stochastic Neighbor Embedding 将高维数据映射到维护数据中的本地结构的低维流形。它是一种非线性技术,通常只用于可视化。
  • UMAP:Uniform Manifold Approximation and Projection 一种相对较新的技术,它也将数据映射到低维流形,但试图保留比TSNE更多的全局结构。

这四种方法在Python中实现都相对简单。将把选中的特性映射到3维以进行可视化,然后还将使用PCA、ICA和UMAP作为建模的特性(TSNE没有转换方法,因此不能用于预处理)。
最终效果:

Kaggle实例-家庭贫困水平预测相关推荐

  1. 利用python机器学习库进行Kaggle皮马印第安人糖尿病预测分析

    利用python机器学习库进行Kaggle皮马印第安人糖尿病预测分析 项目摘要 本项目主要使用python对各医学参数与糖尿病之间的关系进行可视化分析.描述性分析.使用scikit-learn机器学习 ...

  2. ARIMA模型实例讲解——网络流量预测可以使用啊

    ARIMA模型实例讲解:时间序列预测需要多少历史数据? from:https://www.leiphone.com/news/201704/6zgOPEjmlvMpfvaB.html   雷锋网按:本 ...

  3. matlab里BP神经网络实现实例2汽油辛烷值预测

    一.引入 1.首先得到数据,比如数据是从exce导入,也可以是现成的.mat文件.60组汽油样品,利用傅里叶近红外变换光谱仪对其进行扫描,扫描范围900~1700nm,扫描间隔2nm,每个样品的光谱曲 ...

  4. 【机器学习入门】(9) 逻辑回归算法:原理、精确率、召回率、实例应用(癌症病例预测)附python完整代码和数据集

    各位同学好,今天我和大家分享一下python机器学习中的逻辑回归算法.内容主要有: (1) 算法原理:(2) 精确率和召回率:(3) 实例应用--癌症病例预测. 文末有数据集和python完整代码 1 ...

  5. 【数据竞赛】Kaggle时序建模案例:预测水资源可用性

    Acea Smart Water Analytics 您能否预测水资源未来的的可用性? 每个数据集代表一种不同类型的水体.由于每个水体彼此不同,相关特征也不同.我们会注意到它的特征与湖泊的特征不同.这 ...

  6. 『Kaggle』Elo 用户忠诚度预测

    ★★★ 本文源自AI Studio社区精品项目,[点击此处]查看更多精品内容 >>> > 前言: 本项目来源于Kaggle平台的Elo Merchant Category Re ...

  7. 基于python的kaggle练习(二)——员工离职预测

    前沿 目前社会上呈现出一种公司招不到人,大批失业人员的矛盾现象,且大部分公司的离职率居高不下,很多入职没多久就辞职,所花费的培训招聘等资源都浪费了.为了弄清楚公司员工离职原因,通过kaggle上某一家 ...

  8. 【机器学习kaggle赛事】泰坦尼克号生存预测

    目录 写在前面 数据集情况查看 数据清洗 Embarked: Fare Age Cabin 特征工程 1,探究Sex与Survived的相关性 2,探究Pcalss与Survived的关联性 3,Em ...

  9. svm 交叉验证 python_【python机器学习笔记】SVM实例:有毒蘑菇预测

    假如我手上有一个未知的蘑菇,我知道它的一系列特征(伞盖形状.发现地点.颜色.气味等等),但是仅仅依靠个人的经验判断能不能吃难免会翻车,这就要用到更多的经验.大数据就很好地解决了这个个人经验缺乏的问题: ...

最新文章

  1. python学习笔记(四)——流程控制
  2. Win7系统隐藏文件恢复的方法
  3. IOS数组按中文关键字以字母序排序
  4. 如何实现复杂FPGA设计的时序收敛
  5. 【译】用JavaScript写一个区块链
  6. 【暖*墟】#树链剖分# 树链剖分学习与练习
  7. 如何配置php session使用redis集群
  8. android获得手机目录,关于android手机文件目录的收集
  9. 微信小程序UI组件 开发框架 实用库 经典demo
  10. 51单片机农历转换公历c语言算法,51单片机阳历转农历(仅仅是阳历转阴历例程)...
  11. [SystemC]SystemC中的模块和程序
  12. 如何绕过强制门户——克隆 MAC 地址
  13. 利用callgrind+gprof2dot+dot进行性能分析
  14. python中文名是什么怎么读-python中文读音
  15. Qt QSet 详解:从底层原理到高级用法
  16. win7 ultimate是什么版本?
  17. 每日一道leetcode(python)844. 比较含退格的字符串
  18. 天梯赛 L1-079 天梯赛的善良(c语言)
  19. 微信小程序API promise的实现
  20. 如何防止U盘复制电脑文件?

热门文章

  1. 计算机视觉中的算法幻想性视错觉
  2. swift MT报文处理相关资料
  3. wordpress linux 目录,Linux系统二级目录无法安装Wordpress解决办法 | 无忧主机
  4. socketio使用
  5. 神笔马良——基于 OpenGL 的涂鸦框架
  6. 第十三章 读取游戏资源文件
  7. java tika pdf_TIKA - 提取PDF
  8. 微信撤回消息服务器还有存底么,微信更新又来了,撤回消息大变化!还增加“查岗”功能......
  9. i.MX537多媒体应用处理器简介
  10. el-select嵌套树结构实现多级下拉菜单