利用 Pandas 的 transform 和 apply 来处理组级别的缺失数据
根据 Businessbroadway 的一项分析,数据专业人员将会花高达 60% 的时间用于收集、清理和可视化数据。
资料来源:Businessbroadway
清理和可视化数据的一个关键方面是如何处理丢失的数据。Pandas 以 fillna 方法的形式提供了一些基本功能。虽然 fillna 在最简单的情况下工作得很好,但只要数据中的组或数据顺序变得相关,它就会出现问题。本文将讨论解决这些更复杂情况的技术。
这些情况通常是发生在由不同的区域(时间序列)、组甚至子组组成的数据集上。不同区域情况的例子有月、季(通常是时间范围)或一段时间的大雨。性别也是数据中群体的一个例子,子组的例子有年龄和种族。
这篇文章附带了代码。所以你可以随意启动一个 Notebook,直接开始。
文章结构:
-
Pandas fillna 概述
-
当排序不相关时,处理丢失的数据
-
当排序相关时,处理丢失的数据
Pandas fillna 概述
图片来自 Pixabay
Pandas 有三种通过调用 fillna()处理丢失数据的模式:
-
method='ffill':ffill 或 forward fill 向前查找非空值,直到遇到另一个非空值
-
method='bfill':bfill 或 backward fill 将第一个观察到的非空值向后传播,直到遇到另一个非空值
-
显式值:也可以设置一个精确的值来替换所有的缺失值。例如,这个替换值可以是 -999,以表示缺少该值。
例子:
当排序不相关时,处理丢失的数据
来自 Pixabay 公共领域的图片
通常,在处理丢失的数据时,排序并不重要,因此,用于替换丢失值的值可以基于可用数据的整体来决定。在这种情况下,你通常会用你猜测的最佳值(即,可用数据的平均值或中等值)替换丢失的值。
让我们快速回顾一下为什么应该小心使用此方法。假设你调查了 1000 个男孩和 1000 个女孩的体重。不幸的是,在收集数据的过程中,有些数据丢失了。
# imports
import numpy as np
# sample 1000 boys and 1000 girls
boys = np.random.normal(70,5,1000)
girls = np.random.normal(50,3,1000)
# unfortunately, the intern running the survey on the girls got distracted and lost 100 samples
for i in range(100):
girls[np.random.randint(0,1000)] = np.nan
# build DataFrame
boys = pd.DataFrame(boys, columns=['weight'])
boys['gender'] = 'boy'
girls = pd.DataFrame(girls, columns=['weight'])
girls['gender'] = 'girl'
df = pd.concat([girls,boys],axis=0)
df['weight'] = df['weight'].astype(float)
子组
如果不是很在意缺失值填充什么,我们可以用整个样本的平均值填充缺失的值。不过,结果看起来有些奇怪。女孩的 KDE 有两个驼峰。有人可能会得出结论,在我们的样本中有一个子组的女孩体重较重。因为我们预先构建了分布,所以我们知道情况并非如此。但如果这是真实的数据,我们可能会从中得出错误的结论。
男孩和女孩的体重 KDE,我们用样本均值替换缺失的数据(下附代码)
# PLOT CODE:
sns.set_style('white')
fig, ax = plt.subplots(figsize=(16, 7))
mean = df['weight'].mean()
sns.distplot(
df[df['gender'] == 'girl']['weight'].fillna(mean),
kde=True,
hist=False,
ax=ax,
label='girls'
)
sns.distplot(
df[df['gender'] == 'boy']['weight'],
kde=True,
hist=False,
ax=ax,
label='boys'
)
plt.title('Kernel density estimation of weight for boys and girls')
sns.despine()
用组的平均值填充缺失值
在这种情况下,Pandas 的转换函数就派上了用场,它使用变换提供了一种简洁的方法来解决这个问题:
df['filled_weight'] = df.groupby('gender')['weight'].transform(
lambda grp: grp.fillna(np.mean(grp))
)
运行上述命令并绘制填充的权重值的 KDE 将得到:
男孩和女孩权重的 KDE,我们用组平均值替换缺失值(下面附代码)
# PLOT CODE:
sns.set_style('white')
fig, ax = plt.subplots(figsize=(16, 7))
sns.distplot(
df[df['gender'] == 'girl']['filled_weight'],
kde=True,
hist=False,
ax=ax,
label='girls'
)
sns.distplot(
df[df['gender'] == 'boy']['filled_weight'],
kde=True,
hist=False,
ax=ax,
label='boys'
)
plt.title('Kernel density estimation of weight for boys and girls')
sns.despine()
多个子组
让我们使用前面的例子,但是这次,我们进一步将数据细分为年龄组。我们先创建一些模拟数据:
# paramter for the weight distribution (mean, std)
param_map = {
'boy':{
'<10':(40,4),
'<20':(60,4),
'20+':(70,5),
},
'girl':{
'<10':(30,2),
'<20':(40,3),
'20+':(50,3),
}
}
# generate 10k records
df = pd.DataFrame({
'gender':np.random.choice(['girl','boy'],10000),
'age_cohort':np.random.choice(['<10','<20','20+'],10000)
})
# set random weight based on parameters
df['weight'] = df.apply(
lambda x: np.random.normal(
loc=param_map[x['gender']][x['age_cohort']][0],
scale=param_map[x['gender']][x['age_cohort']][1]
),axis=1
)
# set 500 values missing
for i in range(500):
df.loc[np.random.randint(0,len(df)),'weight'] = np.nan
绘制数据图,会出现一些奇怪的双峰分布(后面有代码)。
用样本平均值代替缺失值
# PLOT CODE
df['filled_weight'] = df['weight'].fillna(
df['weight'].mean()
)
g = sns.FacetGrid(
df,
col='age_cohort',
row='gender',
col_order=['<10','<20','20+']
)
g.map(sns.kdeplot,'filled_weight')
现在,如果我们只用性别的平均值来代替缺失的值,就远远不够,因为男孩和女孩不仅体重不同,而且不同年龄组的体重也大不相同。
幸运的是,可以像前面一样使用转换。我们将对两列进行分组,代码如下:
df['filled_weight'] = df.groupby(['gender','age_cohort'])
['weight'].transform(
lambda grp: grp.fillna(np.mean(grp))
)
运行上述代码片段将生成更清晰的曲线:
按年龄、性别分组的体重 KDE 用各组的平均值代替缺失值
当顺序相关时,处理丢失的数据
Jake Hills 在 Unsplash 上的照片
在处理时间序列数据时,经常会出现两种情况:
-
调整日期范围:假设你有一份关于各国的 GDP、教育水平和人口年增长率的数据。对一些国家来说,你缺失了最初几年、最后几年或者中间几年的数据。当然,你可以忽略它们。不过,为了可视化,你可能想要填充这些数据。
-
插值:看时间序列数据插值,你会发现排序变得非常相关。如果用基于截至 2019 年的数据计算出的平均值来替换 2012 年丢失的股票数据,势必会产生一些古怪的结果。
我们将以《2019 年世界幸福报告》(World Happiness Report 2019)中的数据为基础来看一个例子,在这个例子中,我们将处理这两种情况。《世界幸福报告》试图回答影响全世界幸福的因素。该报告调查了 2005 年至 2018 年的数据。
载入数据
# Load the data
df = pd.read_csv('https://raw.githubusercontent.com/FBosler/you- datascientist/master/happiness_with_continent.csv')
样本检验
与 df.head(5)相反,df.sample(5) 选择五个随机行,从而使你有一个偏差更小的数据可视化图。
下载数据帧中的数据示例
让我们看看我们每年有多少国家的数据。
每年有数据的国家数量
# PLOT CODE:
df.groupby(['Year']).size().plot(
kind='bar',
title='Number of countries with data',
figsize=(10,5)
)
我们可以看到,特别是在早些年,我们没有多少国家的数据,而且整个样本周期都有一些波动。为了减轻丢失数据的影响,我们将执行以下操作:
-
按国家分组并重新索引到整个日期范围
-
在对每个国家分组的范围之外的年份内插和外推
1.按国家分组并重新索引日期范围
# Define helper function
def add_missing_years(grp):
_ = grp.set_index('Year')
_ = _.reindex(list(range(2005,2019)))
del _['Country name']
return _
# Group by country name and extend
df = df.groupby('Country name').apply(add_missing_years)
df = df.reset_index()
我们现在大约有 600 行数据。然而,这些观察结果现在是无效的。
扩展数据帧,所有国家在 2005 年到 2018 年间都有数据
2.在对每个国家分组的范围之外的年份内插和外推
# Define helper function
def fill_missing(grp):
res = grp.set_index('Year')\
.interpolate(method='linear',limit=5)\
.fillna(method='ffill')\
.fillna(method='bfill')
del res['Country name']
return res
# Group by country name and fill missing
df = df.groupby(['Country name']).apply(
lambda grp: fill_missing(grp)
)
df = df.reset_index()
fill_missing 函数在末尾和开头进行插值和外推,结果是:
很完美!现在我们有样本中所有国家 2005 年至 2018 年的数据。当我写这篇关于可视化的文章时,上面的方法对我来说很有意义。如果你想了解更多关于这篇报告的信息,可以查看: https://towardsdatascience.com/plotting-with-python-c2561b8c0f1f?source=post_page-----cb6ccf060531---------------------- 。
via:
https://towardsdatascience.com/using-pandas-transform-and-apply-to-deal-with-missing-data-on-a-group-level-cb6ccf060531
雷锋网年度评选—— 寻找19大行业的最佳AI落地实践
创立于2017年的「AI最佳掘金案例年度榜单」,是业内首个人工智能商业案例评选活动。雷锋网从商用维度出发,寻找人工智能在各个行业的最佳落地实践。
第三届评选已正式启动,关注微信公众号“雷锋网”,回复关键词“榜单”参与报名。详情可咨询微信号:xqxq_xq
雷锋网雷锋网
(公众号:雷锋网)
雷锋网
雷锋网版权文章,未经授权禁止转载。详情见。