数据分析之Pandas缺失数据处理

↑↑↑关注后" 星标 "Datawhale

每日干货 &  每月组队学习 ,不错过

Datawhale干货 

作者:耿远昊,Datawhale成员,华东师范大学

Pandas 是一个强大的分析结构化数据的工具集,它的使用基础是Numpy(提供高性能的矩阵运算),用于数据挖掘和数据分析,同时也提供数据清洗功能。

在往期文章中,已经详细讲解了Pandas做分析数据的四种基本操作:索引、 分组变形 及合并。现在,开始正式介绍Pandas的数据结构类型:缺失数据、文本数据、分类数据和时序数据。

在接下来的两章中,会接触到数据预处理中比较麻烦的类型,即缺失数据和文本数据(尤其是混杂型文本)。今天,我们首先对缺失数据进行系统地梳理。

本文目录

1. 基础概念

1.1. 缺失值分类

1.2. 缺失值处理方法

    2. 缺失观测及其类型

    2.1. 了解缺失信息

    2.2. 三种缺失符号

    2.3. Nullable类型与NA符号

    2.4. NA的特性

    2.5. convert_dtypes方法

3. 缺失数据的运算与分组  

3.1. 加号与乘号规则

3.2. groupby方法中的缺失值

4. 填充与剔除

4.1. fillna方法

4.2. dropna方法

5. 插值

    5.1. 线性插值

    5.2. 高级插值方法  

    5.3. interpolate中的限制参数

     6. 问题及练习

    6.1. 问题

    6.2. 练习

基础概念

首先,对缺失值分类和缺失值处理方法进行讲解。

缺失值的分类

按照数据缺失机制可分为:

  • 可忽略的缺失

    • 完全随机缺失(missing completely at random, MCAR), 所缺失的数据发生的概率既与已观察到的数据无关,也与未观察到的数据无关

    • 随机缺失(missing at random, MAR), 假设缺失数据发生的概率与所观察到的变量是有关的,而与未观察到的数据的特征是无关的。

  • 不可忽略的缺失(non-ignorable missing ,NIM) 或非随机缺失(not missing at random, NMAR, or, missing not at random, MNAR), 如果不完全变量中数据的缺失既依赖于完全变量又依赖于不完全变量本身,这种缺失即为不可忽略的缺失。

【注意】:Panda读取的数值型数据,缺失数据显示“ NaN ”(not a number)。

数据值的处理方法

主要就是两种方法:

  • 删除存在缺失值的个案;

  • 缺失值插补。

【注意】缺失值的插补只能用于客观数据。由于主观数据受人的影响,其所涉及的真实值不能保证。

1、删除含有缺失值的个案(2种方法)

(1)简单删除法

简单删除法是对缺失值进行处理的最原始方法。 它将存在缺失值的个案删除。如果数据缺失问题可以通过简单的删除小部分样本来达到目标,那么这个方法是最有效的。

(2)权重法

当缺失值的类型为非完全随机缺失的时候,可以通过对完整的数据加权来减小偏差。 把数据不完全的个案标记后,将完整的数据个案赋予不同的权重,个案的权重可以通过logistic或probit回归求得。

如果解释变量中存在对权重估计起决定行因素的变量,那么这种方法可以有效减小偏差。 如果解释变量和权重并不相关,它并不能减小偏差。

对于存在多个属性缺失的情况,就需要对不同属性的缺失组合赋不同的权重,这将大大增加计算的难度,降低预测的准确性,这时权重法并不理想。

2、可能值插补缺失值

【思想来源】:以最可能的值来插补缺失值比全部删除不完全样本所产生的信息丢失要少。

(1)均值插补

属于单值插补。 数据的属性分为定距型和非定距型。如果缺失值是定距型的,就以该属性存在值的平均值来插补缺失的值;如果缺失值是非定距型的,就用该属性的众数来补齐缺失的值。

(2)利用同类均值插补

属于单值插补。 用层次聚类模型预测缺失变量的类型,再以该类型的均值插补。

假设 为信息完全的变量, 为存在缺失值的变量,那么首先对 或其子集行聚类,然后按缺失个案所属类来插补不同类的均值。

如果在以后统计分析中还需以引入的解释变量和 做分析,那么这种插补方法将在模型中引入自相关,给分析造成障碍。

(3)极大似然估计(Max Likelihood ,ML)

在缺失类型为随机缺失的条件下,假设模型对于完整的样本是正确的,那么通过观测数据的边际分布可以对未知参数进行极大似然估计(Little and Rubin)。

这种方法也被称为忽略缺失值的极大似然估计,对于极大似然的参数估计实际中常采用的计算方法是期望值最大化(Expectation Maximization,EM)。

该方法比删除个案和单值插补更有吸引力, 前提是适用于大样本,有效样本的数量足够以保证ML估计值是渐近无偏的并服从正态分布。这种方法可能会陷入局部极值,收敛速度也不是很快,并且计算很复杂。

(4)多重插补(Multiple Imputation,MI)

多值插补的思想来源于贝叶斯估计,认为待插补的值是随机的,它的值来自于已观测到的值。 具体实践上通常是估计出待插补的值,然后再加上不同的噪声,形成多组可选插补值。根据某种选择依据,选取最合适的插补值。

多重插补方法的三个步骤:

  • 为每个空值产生一套可能的插补值,这些值反映了无响应模型的不确定性;每个值都可以被用来插补数据集中的缺失值,产生若干个完整数据集合。

  • 每个插补数据集合都用针对完整数据集的统计方法进行统计分析。

  • 对来自各个插补数据集的结果,根据评分函数进行选择,产生最终的插补值。

多重插补方法举例:

假设一组数据,包括三个变量 ,它们的联合分布为正态分布,将这组数据处理成三组,A组保持原始数据,B组仅缺失 ,C组缺失 。在多值插补时,对A组将不进行任何处理,对B组产生 的一组估计值(作 关于 的回归),对C组作产生 的一组成对估计值(作 关于 的回归)。

当用多值插补时,对A组将不进行处理,对B、C组将完整的样本随机抽取形成为 组( 为可选择的 组插补值),每组个案数只要能够有效估计参数就可以了。对存在缺失值的属性的分布作出估计,然后基于这 组观测值,对于这 组样本分别产生关于参数的 组估计值,给出相应的预测即,这时采用的估计方法为极大似然法,在计算机中具体的实现算法为期望最大化法(EM)。对B组估计出一组 的值,对C将利用 它们的联合分布为正态分布这一前提,估计出一组( )。

上例中假定了 的联合分布为正态分布。这个假设是人为的,但是已经通过验证(Graham和Schafer于1999),非正态联合分布的变量,在这个假定下仍然可以估计到很接近真实值的结果。

多重插补弥补贝叶斯估计的不足之处:

  • 贝叶斯估计以极大似然的方法估计,极大似然的方法要求模型的形式必须准确,如果参数形式不正确,将得到错误得结论,即先验分布将影响后验分布的准确性。而多重插补所依据的是大样本渐近完整的数据的理论,在数据挖掘中的数据量都很大,先验分布将极小的影响结果,所以先验分布的对结果的影响不大。

  • 贝叶斯估计仅要求知道未知参数的先验分布,没有利用与参数的关系。而多重插补对参数的联合分布作出了估计,利用了参数间的相互关系。

缺失观测及其类型

首先导入数据:

import pandas as pd

import numpy as np

df = pd.read_csv('data/table_missing.csv')

df.head()

了解缺失信息

1、isn a和notna方法

对Series使用会返回布尔列表

df['Physics'].isna().head()

df['Physics'].notna().head()

对DataFrame使用会返回布尔表

df.isna().head()

但对于DataFrame我们更关心到底每列有多少缺失值
df.isna().sum()

此外,可以通过第1章中介绍的info函数查看缺失信息

df.info()

2、查看缺失值的所以在行

以最后一列为例,挑出该列缺失值的行

df[df['Physics'].isna()]

3、挑选出所有非缺失 值列

使用all就是全部非缺失值,如果是any就是至少有一个不是缺失值

df[df.notna().all(1)]

三种缺失符号

1、np.nan

np.nan是一个麻烦的东西,首先它不等与任何东西,甚至不等于自己。

np.nan == np.nan
False
np.nan == 0
False
np.nan == None
False

在用equals函数比较时,自动略过两侧全是np.nan的单元格,因此结果不会影响。

df.equals(df)
True

其次,它在numpy中的类型为浮点,由此导致数据集读入时,即使原来是整数的列,只要有缺失值就会变为浮点型。

type(np.nan)
float
pd.Series([1,2,3]).dtype
dtype('int64')
pd.Series([1,np.nan,3]).dtype
dtype('float64')

此外,对于布尔类型的列表,如果是np.nan填充,那么它的值会自动变为True而不是False。

pd.Series([1,np.nan,3],dtype='bool')

但当修改一个布尔列表时,会改变列表类型,而不是赋值为True。

s = pd.Series([True,False],dtype='bool')

s[1]=np.nan

s

在所有的表格读取后,无论列是存放什么类型的数据,默认的缺失值全为np.nan类型。

因此整型列转为浮点; 而字符由于无法转化为浮点,因此只能归并为object类型('O'),原来是浮点型的则类型不变

df['ID'].dtype
dtype('float64')
df['Math'].dtype
dtype('float64')
df['Class'].dtype
dtype('O')

2、None

None比前者稍微好些,至少它会等于自身

None == None
True

它的布尔值为False

pd.Series([None],dtype='bool')
0    False
dtype: bool

修改布尔列表不会改变数据类型

s = pd.Series([True,False],dtype='bool')

s[0]=None

s

0    False
1    False
dtype: bool

s = pd.Series([1,0],dtype='bool')

s[0]=None

s

0    False
1    False
dtype: bool

在传入数值类型后,会自动变为np.nan

type(pd.Series([1,None])[1])
numpy.float64

只有当传入object类型是保持不动,几乎可以认为,除非人工命名None,它基本不会自动出现在Pandas中

type(pd.Series([1,None],dtype='O')[1])
NoneType

在使用equals函数时不会被略过,因此下面的情况下返回False

pd.Series([None]).equals(pd.Series([np.nan]))
False

3、NaT

NaT是针对时间序列的缺失值,是Pandas的内置类型,可以完全看做时序版本的np.nan,与自己不等,且使用equals是也会被跳过

s_time = pd.Series([pd.Timestamp('20120101')]*5)

s_time

s_time[2] = None

s_time

s_time[2] = np.nan

s_time

s_time[2] = pd.NaT

s_time

type(s_time[2])
pandas._libs.tslibs.nattype.NaTType
s_time[2] == s_time[2]
False
s_time.equals(s_time)
True

s = pd.Series([True,False],dtype='bool')

s[1]=pd.NaT

s

Nullable类型与NA符号

这是Pandas在1.0新版本中引入的重大改变,其目的就是为了(在若干版本后)解决之前出现的混乱局面,统一缺失值处理方法。

"The goal of pd.NA is provide a “missing” indicator that can be used consistently across data types (instead of np.nan, None or pd.NaT depending on the data type)."——User Guide for Pandas v-1.0

官方鼓励用户使用新的数据类型和缺失类型pd.NA

1、Nullable整形

对于该种类型而言,它与原来标记int上的符号区别在于首字母大写:'Int'

s_original = pd.Series([1, 2], dtype="int64")

s_original

s_new = pd.Series([1, 2], dtype="Int64")

s_new

它的好处就在于,其中前面提到的三种缺失值都会被替换为统一的NA符号,且不改变数据类型。

s_original[1] = np.nan

s_original

s_new[1] = np.nan

s_new

s_new[1] = None

s_new

s_new[1] = pd.NaT

s_new

2、Nullable布尔

对于该种类型而言,作用与上面的类似,记号为boolean

s_original = pd.Series([1, 0], dtype="bool")

s_original

s_new = pd.Series([0, 1], dtype="boolean")

s_new

s_original[0] = np.nan

s_original

s_original = pd.Series([1, 0], dtype="bool") #此处重新加一句是因为前面赋值改变了bool类型

s_original[0] = None

s_original

s_new[0] = np.nan

s_new

s_new[0] = None

s_new

s_new[0] = pd.NaT

s_new

需要注意的是,含有pd.NA的布尔列表在1.0.2之前的版本作为索引时会报错,这是一个之前的bug,现已经修复。

s = pd.Series(['dog','cat'])

s[s_new]

3、string类型

该类型是1.0的一大创新,目的之一就是为了区分开原本含糊不清的object类型,这里将简要地提及string,因为它是第7章的主题内容。 它本质上也属于Nullable类型,因为并不会因为含有缺失而改变类型。

s = pd.Series(['dog','cat'],dtype='string')

s

s[0] = np.nan

s

s[0] = None

s

s[0] = pd.NaT

s

此外,和object类型的一点重要区别就在于,在调用字符方法后,string类型返回的是Nullable类型,object则会根据缺失类型和数据类型而改变。

s = pd.Series(["a", None, "b"], dtype="string")

s.str.count('a')

s2 = pd.Series(["a", None, "b"], dtype="object")

s2.str.count("a")

s.str.isdigit()
<img data-ratio="0.1120162932790224" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_png/vI9nYe94fsEiczjPicHQZ9SibWXicePXIjeErUMDdgcyMzgd9Oib8Wuqh8P3exXh0MZ2troxlCCowMIk5EO8ndmOQNA/640?wx_fmt=png" data-type="png" data-w="1473" style="white-space:normal;letter-spacing:0.544px;font-family:-apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;text-align:center;" />
s2.str.isdigit()

NA的特性

1、逻辑运算

只需看该逻辑运算的结果是否依赖pd.NA的取值,如果依赖,则结果还是NA,如果不依赖,则直接计算结果。

True | pd.NA
True
pd.NA | True
True
False | pd.NA
<NA>
False & pd.NA
False
True & pd.NA
<NA>

取值不明直接报错

#bool(pd.NA)

2、算术运算和比较运算

这里只需记住除了下面两类情况,其他结果都是NA即可

pd.NA ** 0
1 ** pd.NA

其他情况:

pd.NA + 1
<NA>
"a" * pd.NA
<NA>
pd.NA == pd.NA
<NA>
pd.NA < 2.5
<NA>
np.log(pd.NA)
<NA>
np.add(pd.NA, 1)
<NA>

convert_dtypes方法

这个函数的功能往往就是在读取数据时,就把数据列转为Nullable类型,是1.0的新函数。

pd.read_csv('data/table_missing.csv').dtypes

pd.read_csv('data/table_missing.csv').convert_dtypes().dtypes

缺失数据的运算与分组

加号与乘号规则

使用加法时,缺失值为0

s = pd.Series([2,3,np.nan,4])

s.sum()

9.0

使用乘法时,缺失值为1

s.prod()
24.0

使用累计函数时,缺失值自动略过

s.cumsum()

s.cumprod()

s.pct_change()

groupby方法中的缺失值

自动忽略为缺失值的组

df_g = pd.DataFrame({'one':['A','B','C','D',np.nan],'two':np.random.randn(5)})

df_g

df_g.groupby('one').groups

填充与剔除

fillna方法

1、值填充与前后向填充 (分别与ffill方法和bfill方法等价)

df['Physics'].fillna('missing').head()

df['Physics'].fillna(method='ffill').head()

df['Physics'].fillna(method='backfill').head()

2、填充中的对齐特性

df_f = pd.DataFrame({'A':[1,3,np.nan],'B':[2,4,np.nan],'C':[3,5,np.nan]})

df_f.fillna(df_f.mean())

返回的结果中没有C,根据对齐特点不会被填充

df_f.fillna(df_f.mean()[['A','B']])

dropna方法

1、axis参数

df_d = pd.DataFrame({'A':[np.nan,np.nan,np.nan],'B':[np.nan,3,2],'C':[3,2,1]})

df_d

df_d.dropna(axis=0)
df_d.dropna(axis=1)

2、how参数 (可以选all或者any,表示全为缺失去除和存在缺失去除)

df_d.dropna(axis=1,how='all')

3、subset参数 (即在某一组列范围中搜索缺失值)¶

df_d.dropna(axis=0,subset=['B','C'])

线性插值

1、索引无关的线性插值

默认状态下,interpolate会对缺失的值进行线性插值

s = pd.Series([1,10,15,-5,-2,np.nan,np.nan,28])

s

s.interpolate()

s.interpolate().plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7fe7df20af50>

此时的插值与索引无关

s.index = np.sort(np.random.randint(50,300,8))

s.interpolate()

#值不变

s.interpolate().plot()

#后面三个点不是线性的(如果几乎为线性函数,请重新运行上面的一个代码块,这是随机性导致的)

<matplotlib.axes._subplots.AxesSubplot at 0x7fe7dfc69890>

2、与索引有关的插值

method中的index和time选项可以使插值线性地依赖索引,即插值为索引的线性函数

s.interpolate(method='index').plot()

#可以看到与上面的区别

<matplotlib.axes._subplots.AxesSubplot at 0x7fe7dca0c4d0>

如果索引是时间,那么可以按照时间长短插值,对于时间序列将在第9章详细介绍

s_t = pd.Series([0,np.nan,10]

,index=[pd.Timestamp('2012-05-01'),pd.Timestamp('2012-05-07'),pd.Timestamp('2012-06-03')])

s_t

s_t.interpolate().plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7fe7dc964850>

s_t.interpolate(method='time').plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7fe7dc8eda10>

高级插值方法

此处的高级指的是与线性插值相比较,例如样条插值、多项式插值、阿基玛插值等(需要安装Scipy)。

关于这部分仅给出一个官方的例子,因为插值方法是数值分析的内容,而不是Pandas中的基本知识:

ser = pd.Series(np.arange(1, 10.1, .25) ** 2 + np.random.randn(37))

missing = np.array([4, 13, 14, 15, 16, 17, 18, 20, 29])

ser[missing] = np.nan

methods = ['linear', 'quadratic', 'cubic']

df = pd.DataFrame({m: ser.interpolate(method=m) for m in methods})

df.plot()

<matplotlib.axes._subplots.AxesSubplot at 0x7fe7dc86f810>

interpolate中的限制参数

1、limit表示最多插入多少个

s = pd.Series([1,np.nan,np.nan,np.nan,5])

s.interpolate(limit=2)

2、limit_direction表示插值方向,可选forward,backward,both,默认前向。

s = pd.Series([np.nan,np.nan,1,np.nan,np.nan,np.nan,5,np.nan,np.nan,])

s.interpolate(limit_direction='backward')

3、limit_area表示插值区域,可选inside,outside,默认None

s = pd.Series([np.nan,np.nan,1,np.nan,np.nan,np.nan,5,np.nan,np.nan,])

s.interpolate(limit_area='inside')

s = pd.Series([np.nan,np.nan,1,np.nan,np.nan,np.nan,5,np.nan,np.nan,])

s.interpolate(limit_area='outside')

问题与练习

问题

【问题一】 如何删除缺失值占比超过25%的列?

  • 第一步,计算单列缺失值的数量,计算单列总样本数

  • 第二步,算出比例,得到一个列的布尔列表

  • 第三步,利用这个布尔列表进行列索引或列删除

df.loc[:,(df.isna().sum()/df.isna().count()<0.25).values]

【问题二】 什么是Nullable类型?请谈谈为什么要引入这个设计?

Nullable类型是一种为了统一NaN,Null,NaT三类缺失值而诞生的新的类型。 是在原来的数值、布尔、字符等类型的基础上进行小改,优化了当出现缺失值情况时的应对。 引入这个设计时为了更好的处理缺失值,统一缺失值处理方法

【问题三】 对于一份有缺失值的数据,可以采取哪些策略或方法深化对它的了解?

  • 可以查看缺失值出现的比例;

  • 查看缺失值之间的关联性;

  • 查看总体的缺失信息;

  • 根据缺失信息判断是否为有效数据;

  • 根据缺失信息清洗数据等等。

练习

【练习一】 现有一份虚拟数据集,列类型分别为string/浮点/整型,请解决如下问题。

q1 = pd.read_csv('data/Missing_data_one.csv')

q1.head()

A B C

0 not_NaN 0.922 4.0

1 not_NaN 0.700 NaN

2 not_NaN 0.503 8.0

3 not_NaN 0.938 4.0

4 not_NaN 0.952 10.0

1.1 请以列类型读入数据,并选出C为缺失值的行。

q1[q1['C'].isna()]

1.2 现需要将A中的部分单元转为缺失值,单元格中的最小转换概率为25%,且概率大小与所在行B列单元的值成正比

q1['A'] = pd.Series(list(zip(q1['A'].values,q1['B'].values))).apply(lambda x:x[0] if np.random.rand()>0.25*x[1]/q1['B'].min() else np.nan)

【练习二】 现有一份缺失的数据集,记录了36个人来自的地区、身高、体重、年龄和工资,解决如下问题:

pd.read_csv('data/Missing_data_two.csv').head()

编号 地区 身高 体重 年龄 工资

0 1 A 157.50 NaN 47.0 15905.0

1 2 B 202.00 91.80 25.0 NaN

2 3 C 169.09 62.18 NaN NaN

3 4 A 166.61 59.95 77.0 5434.0

4 5 B 185.19 NaN 62.0 4242.0

2.1 统计各列缺失的比例并选出在后三列中至少有两个非缺失值的行

q2.isna().sum()/q2.shape[0]

q2[q2.iloc[:,-3:].isna().sum(1)<=1].head()

2.2  请结合 身高列和地区列中的数据,对体重进行合理插值

q2_new = q2.copy()

for area,group in q2.groupby('地区'):

q2_new.loc[group.index,'体重'] = group[['身高','体重']].sort_values(by='身高').interpolate()['体重']

q2_new = q2_new.round(decimals=2)

q2_new.head()

本文电子版 后台回复 缺失数据 获取

“感谢你的在看,点赞,分享三

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章