Pandas之旅(二): 有关数据清理的点点滴滴

40次阅读

共计 7444 个字符,预计需要花费 19 分钟才能阅读完成。

数据清洗
大家好,这一期我将为大家带来我的 pandas 学习心得第二期:数据清理。这一步非常重要,一般在获取数据源之后,我们紧接着就要开始这一步,以便为了之后的各种操作,简单来说,我们的目标就是让数据看起来赏心悦目,规规矩矩的,所以我们会对原始的 dataframe 做一些必要的美容,包括规范命名,去除异常值,重新选择合适的 index 啊,处理缺失值,统一列的命名等等。
这一期我会和大家分享一些比较好用常见的清洗方法。首先还是让我们来简单看一下本文将会用到的数据源:

property_data.csv 这是一个超小型的房地产行业的数据集,大家会在文章最后找到下载地址。
这篇文章我会从以下几个方面来和大家分享我的心得体会:

有关缺失值的处理
有关列的处理
设置 Index
源码及数据下载地址

1. 有关缺失值的处理
这里我们会用到 property_data.csv 这个数据集,在开始处理缺失值之前,我们可以先话一分钟仔细想想,为什么实际生活中的数据从来是不完整的,原因基本有几个方面:

用户忘记填写字段
从旧数据库手动传输时数据丢失
代码中有 bug
用户不填写非必须字段(比如注册的时候)

因为这些原因,我每次在处理 missing value 的时候都会问自己两个基础问题:

数据集每一列有什么特点?
我们想要在处理后得到什么类型的数据(int,float,string,boolean)?

带着这些疑问,我们可以开始了,首先让我们简单读取一下数据,利用 head 函数看看前 5 行,如果你还对 pandas 的基础知识有疑问,可以看看我上一篇文章:Pandas 之旅(一 ): 让我们把基础知识一次撸完,申精干货
import pandas as pd
import numpy as np
import os
os.chdir(“F:\\Python 教程 \\segmentfault\\pandas_share\\Pandas 之旅_02 数据清洗 ”)
# Read csv file into a pandas dataframe
df = pd.read_csv(“property_data.csv”)

# Take a look at the first few rows
df.head()

PID
ST_NUM
ST_NAME
OWN_OCCUPIED
NUM_BEDROOMS
NUM_BATH
SQ_FT

0
100001000.0
104.0
PUTNAM
Y
3
1
1000

1
100002000.0
197.0
LEXINGTON
N
3
1.5

2
100003000.0
NaN
LEXINGTON
N
NaN
1
850

3
100004000.0
201.0
BERKELEY
12
1
NaN
700

4
NaN
203.0
BERKELEY
Y
3
2
1600

现在让我们看看数据的一些关键列是什么:

ST_NUM:街道号码
ST_NAME: 街道名称
OWN_OCCUPIED: 是否用于自住
NUM_BEDROOMS:卧室数量
SQ_FT:面积

这里可以给大家普及点房地产知识,有的时候房屋用途被明确规定,比如有的房产写的是 ”owner occupied only “)意思是说如果你买了,那这个房子会成为你的主要住所,不能用于出租之类的,简单理解就是自住
所以现在我可以自问自答第一个问题: 数据集每一列有什么特点?

ST_NUM:float 或 int …
ST_NAME:string
OWN_OCCUPIED:string … Y(“是”)或 N(“否”)
NUM_BEDROOMS:float 或 int,数字类型
SQ_FT:float 或 int,数字类型

1.1 规范的缺失值标记
现在让我们关注 ST_NUM 这一列:
# Looking at the ST_NUM column
df[‘ST_NUM’]
0 104.0
1 197.0
2 NaN
3 201.0
4 203.0
5 207.0
6 NaN
7 213.0
8 215.0
Name: ST_NUM, dtype: float64

如果想查看该列的缺失值情况,我们可以利用 isnull()方法,如果出现缺失值,会返回 True,反之返回 false
df[‘ST_NUM’].isnull()
0 False
1 False
2 True
3 False
4 False
5 False
6 True
7 False
8 False
Name: ST_NUM, dtype: bool

但是其实如果我们打开 csv 文件,你会发现第 3 行是空白,还有一行在该列显示的是 NA,所以结论已经有了:在 pandas 里表示缺省值的符号及时 NA, 换句话说,如果我们要表示缺省值,标准写法是 NA

1.2 不规范的缺失值标记
同样的,这回让我们关注一下 NUM_BEDROOMS 这一列,我们发现出现了 4 种类型的表达缺省值的标记:

n/a
NA

na

通过刚才的实践,我们已经确定 NA 是 pandas 可以识别的,那么其他的符号呢,现在让我们来测试一下
df[‘NUM_BEDROOMS’]
0 3
1 3
2 NaN
3 1
4 3
5 NaN
6 2
7 1
8 na
Name: NUM_BEDROOMS, dtype: object

df[‘NUM_BEDROOMS’].isnull()
0 False
1 False
2 True
3 False
4 False
5 True
6 False
7 False
8 False
Name: NUM_BEDROOMS, dtype: bool

可以看到 pandas 识别了 n /a 和 NA 两种符号,但是接下来我们要考虑一个问题,假设你是房地产公司的地区总经理,你每周会收到不同地区的负责人提交的表格,这些人中有的喜欢用 – 表示空白值,有的人喜欢用 na,那应该怎么办?
最简单的方式就是将所有表示空白值的符号统一放在 list 中,让后让 pandas 一次性识别:
# Making a list of missing value types
missing_values = [“na”, “–“]
df = pd.read_csv(“property_data.csv”, na_values = missing_values)
现在我们来看看到底发生了什么?
df

PID
ST_NUM
ST_NAME
OWN_OCCUPIED
NUM_BEDROOMS
NUM_BATH
SQ_FT

0
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0

1
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN

2
100003000.0
NaN
LEXINGTON
N
NaN
1
850.0

3
100004000.0
201.0
BERKELEY
12
1.0
NaN
700.0

4
NaN
203.0
BERKELEY
Y
3.0
2
1600.0

5
100006000.0
207.0
BERKELEY
Y
NaN
1
800.0

6
100007000.0
NaN
WASHINGTON
NaN
2.0
HURLEY
950.0

7
100008000.0
213.0
TREMONT
Y
1.0
1
NaN

8
100009000.0
215.0
TREMONT
Y
NaN
2
1800.0

我们可以发现只要 missing_value 中记录的表达空白值的符号,全部变成了规整的 NaN
1.3 类型不一致的异常值
刚刚我们已经简单了解了在 pandas 中如何处理缺失值的,还有一种情况,让我们来看 OWN_OCCUPIED 这一列,这一列的答案只能是 Y,N 但是我们发现数据集意外地出现了 12,属于类型不对称

df[‘OWN_OCCUPIED’].isnull()
0 False
1 False
2 False
3 False
4 False
5 False
6 True
7 False
8 False
Name: OWN_OCCUPIED, dtype: bool

现在我们发现 12 是异常值,因为它是类型错误,所以我们可以简单通过下面这个方法来检测,
# Detecting numbers
cnt=0
for row in df[‘OWN_OCCUPIED’]:
try:
int(row)
df.loc[cnt, ‘OWN_OCCUPIED’]=np.nan
except ValueError:
pass
cnt+=1
我们这里的策略是:

循环遍历 OWN_OCCUPIED 列
尝试将条目转换为整数
如果条目可以更改为整数,请输入缺失值
如果数字不能是整数,我们知道它是一个字符串,所以继续

这样我们会把 OWN_OCCUPIED 这一列中所有类型不对的值转化为 NaN,现在来看结果:
df[‘OWN_OCCUPIED’]
0 Y
1 N
2 N
3 NaN
4 Y
5 Y
6 NaN
7 Y
8 Y
Name: OWN_OCCUPIED, dtype: object

1.4 汇总缺失值
pandas 提供了更为简洁的方式,可以让我们整体了解所有 column 的空值:
df.isnull().sum()
PID 1
ST_NUM 2
ST_NAME 0
OWN_OCCUPIED 2
NUM_BEDROOMS 3
NUM_BATH 1
SQ_FT 2
dtype: int64

或者如果我们只想知道数据是否存在空值,那么可以使用以下的命令:
# Any missing values?
df.isnull().values.any()
True

1.5 替换缺失值
如果我们想要替换掉缺失值,可以用 fillna 方法
# Replace missing values with a number
df[‘ST_NUM’].fillna(125, inplace=True)
或者我们可以通过准确定位来替换缺失值:
# Location based replacement
df.loc[2,’ST_NUM’] = 125
替换缺失值的一种非常常见的方法是使用中位数:
# Replace using median
median = df[‘NUM_BEDROOMS’].median()
df[‘NUM_BEDROOMS’].fillna(median, inplace=True)
df

PID
ST_NUM
ST_NAME
OWN_OCCUPIED
NUM_BEDROOMS
NUM_BATH
SQ_FT

0
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0

1
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN

2
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0

3
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0

4
NaN
203.0
BERKELEY
Y
3.0
2
1600.0

5
100006000.0
207.0
BERKELEY
Y
2.5
1
800.0

6
100007000.0
125.0
WASHINGTON
NaN
2.0
HURLEY
950.0

7
100008000.0
213.0
TREMONT
Y
1.0
1
NaN

8
100009000.0
215.0
TREMONT
Y
2.5
2
1800.0

2. 有关列的处理
2.1 统一修改列名
现在假设因为一些需求,需要我们统一修改列名,把列名改为小写,我们可以结合列表推导式轻易实现
df.rename(str.lower, axis=’columns’,inplace =True)
df.columns
Index([‘pid’, ‘st_num’, ‘st_name’, ‘own_occupied’, ‘num_bedrooms’, ‘num_bath’,
‘sq_ft’],
dtype=’object’)

或者需要把列名中的_改为 -:
new_cols = [c.replace(“_”,”-“) for c in df.columns]
change_dict =dict(zip(df.columns,new_cols))
df.rename(columns=change_dict,inplace=True)
df

pid
st-num
st-name
own-occupied
num-bedrooms
num-bath
sq-ft

0
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0

1
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN

2
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0

3
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0

4
NaN
203.0
BERKELEY
Y
3.0
2
1600.0

5
100006000.0
207.0
BERKELEY
Y
2.5
1
800.0

6
100007000.0
125.0
WASHINGTON
NaN
2.0
HURLEY
950.0

7
100008000.0
213.0
TREMONT
Y
1.0
1
NaN

8
100009000.0
215.0
TREMONT
Y
2.5
2
1800.0

这里我没有写的精简一些,反而是复杂了,主要是想让大家回忆起之前我分享的 dict 使用技巧中的内容,注意这里 inplace=True,导致的结果是我们的的确确修改了 df 所有的列名
2.1 根据需求新增列
假如目前我们需要新增一列,根据房屋面积大小来赋值, 我们先随意把缺失值补上:
df[‘sq-ft’].fillna(‘0.0’)
0 1000
1 0.0
2 850
3 700
4 1600
5 800
6 950
7 0.0
8 1800
Name: sq-ft, dtype: object

然后新建一列 rank 来根据房屋面积大小赋值 S =small,M=medium,B=big:
df[“rank”]= pd.cut(df[‘sq-ft’], [0, 800, 1600, np.inf], labels=(“S”,”M”,”B”))
df

pid
st-num
st-name
own-occupied
num-bedrooms
num-bath
sq-ft
rank

0
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0
M

1
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN
NaN

2
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0
M

3
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0
S

4
NaN
203.0
BERKELEY
Y
3.0
2
1600.0
M

5
100006000.0
207.0
BERKELEY
Y
2.5
1
800.0
S

6
100007000.0
125.0
WASHINGTON
NaN
2.0
HURLEY
950.0
M

7
100008000.0
213.0
TREMONT
Y
1.0
1
NaN
NaN

8
100009000.0
215.0
TREMONT
Y
2.5
2
1800.0
B

具体实现方法我们之后会说,这里主要是用到了 pandas 的 cut 方法,非常便捷
3. 设置 Index
在许多情况下,使用数据的唯一值标识字段作为其索引是有帮助的。这里可能我们的数据不太合适,因此我们先伪造一列 Fake_Index 来模拟真实场景中的真正索引
df[“Fake_Index”]=[“A00″+str(i) for i in range(len(df))]
df

pid
st-num
st-name
own-occupied
num-bedrooms
num-bath
sq-ft
rank
Fake_Index

0
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0
M
A000

1
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN
NaN
A001

2
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0
M
A002

3
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0
S
A003

4
NaN
203.0
BERKELEY
Y
3.0
2
1600.0
M
A004

5
100006000.0
207.0
BERKELEY
Y
2.5
1
800.0
S
A005

6
100007000.0
125.0
WASHINGTON
NaN
2.0
HURLEY
950.0
M
A006

7
100008000.0
213.0
TREMONT
Y
1.0
1
NaN
NaN
A007

8
100009000.0
215.0
TREMONT
Y
2.5
2
1800.0
B
A008

现在我们添加的最后一列非常像真正的房屋 Id 了,让我们来看看这个伪造的索引是不是唯一值,可以利用 is_unique 来检验:
df.Fake_Index.is_unique
True

没有问题,现在我们可以放心地把这列设置为我们真正的索引:
df = df.set_index(‘Fake_Index’)
df

pid
st-num
st-name
own-occupied
num-bedrooms
num-bath
sq-ft
rank

Fake_Index

A000
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0
M

A001
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN
NaN

A002
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0
M

A003
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0
S

A004
NaN
203.0
BERKELEY
Y
3.0
2
1600.0
M

A005
100006000.0
207.0
BERKELEY
Y
2.5
1
800.0
S

A006
100007000.0
125.0
WASHINGTON
NaN
2.0
HURLEY
950.0
M

A007
100008000.0
213.0
TREMONT
Y
1.0
1
NaN
NaN

A008
100009000.0
215.0
TREMONT
Y
2.5
2
1800.0
B

现在对数据的操作容易多了,我们很多事情可以通过索引完成:
# 根据索引名称切片
df[‘A000′:’A003’]

pid
st-num
st-name
own-occupied
num-bedrooms
num-bath
sq-ft
rank

Fake_Index

A000
100001000.0
104.0
PUTNAM
Y
3.0
1
1000.0
M

A001
100002000.0
197.0
LEXINGTON
N
3.0
1.5
NaN
NaN

A002
100003000.0
125.0
LEXINGTON
N
2.5
1
850.0
M

A003
100004000.0
201.0
BERKELEY
NaN
1.0
NaN
700.0
S

# 根据索引位置切片

df.iloc[1:3, 0:3]

pid
st-num
st-name

Fake_Index

A001
100002000.0
197.0
LEXINGTON

A002
100003000.0
125.0
LEXINGTON

# 定位到具体元素
df.iloc[1,2]
‘LEXINGTON’

总结
我把这一期的 ipynb 文件和 py 文件放到了 GIthub 上,大家如果想要下载可以点击下面的链接:
Github 仓库地址:https://github.com/yaozeliang/pandas_share

这一期先讲到这里,希望大家能够继续支持我,完结,撒花

正文完
 0