最近在做一个数据分析类项目,涉及处理7万+名学生的全学程数据,数据以表格型结构化数据为主,涉及学生基本信息、成绩和课程信息、评奖评优、勤工助学及行为数据。借此机会,对项目中频繁使用的基于DataFrame 的Python 数据分析语句进行梳理。此篇主要针对数据转换,包括移除重复数据、利用函数或映射进行数据转换、替换值、重命名轴索引、检测和过滤异常值、离散化和面元划分。
# 导入包
import pandas as pd
import numpy as np
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
'k2': [1, 1, 2, 3, 3, 4, 4]})
data
k1 | k2 | |
---|---|---|
0 | one | 1 |
1 | two | 1 |
2 | one | 2 |
3 | two | 3 |
4 | one | 3 |
5 | two | 4 |
6 | two | 4 |
DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行(前面出现过的行):
data.duplicated()
0 False
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
drop_duplicates方法去掉重复行,duplicated和drop_duplicates默认保留的是第一个出现的值组合:
data.drop_duplicates()
k1 | k2 | |
---|---|---|
0 | one | 1 |
1 | two | 1 |
2 | one | 2 |
3 | two | 3 |
4 | one | 3 |
5 | two | 4 |
这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断,比如仅根据k1列过滤重复项:
data.drop_duplicates(['k1'])
k1 | k2 | |
---|---|---|
0 | one | 1 |
1 | two | 1 |
传入keep='last’则保留最后一个出现的(默认为keep=‘first’):
data.drop_duplicates(keep = 'last')
k1 | k2 | |
---|---|---|
0 | one | 1 |
1 | two | 1 |
2 | one | 2 |
3 | two | 3 |
4 | one | 3 |
6 | two | 4 |
使用map是一种实现元素级转换以及其他数据清理工作的便捷方式。该方法可以接受一个函数或含有映射关系的字典型对象。
对于许多数据集,有些类别信息可能是通过指代码来表示的,在数据处理时,你可能希望回填其具体指代内容。比如下面的例子中,'school’为各学生的学院信息,是通过指代码表示的,我们希望回填其具体学院名称信息:
data = pd.DataFrame({'ID': ['Sally', 'Bob', 'Micheal',
'Sophy', 'Dave', 'Nancy',
'Mike', 'Kevin', 'Sam'],
'school': ['1 ', '2 ', '1', '1 ', '3', '3', '4 ', '4', '5']})
data
ID | school | |
---|---|---|
0 | Sally | 1 |
1 | Bob | 2 |
2 | Micheal | 1 |
3 | Sophy | 1 |
4 | Dave | 3 |
5 | Nancy | 3 |
6 | Mike | 4 |
7 | Kevin | 4 |
8 | Sam | 5 |
先编写一个不同指代码到学院的映射:
code = {
'1': '工程学院',
'2': '外国语学院',
'3': '经济管理学院',
'4': '水产与生命学院',
'5': '食品学院'
}
data.school.unique()
array(['1 ', '2 ', '1', '3', '4 ', '4', '5'], dtype=object)
但这里有个小问题,即有些类别码可能由于输入的错误,字符串后面多了一个空格,而有些则没有。因此我们需要使用str.strip
方法统一把空格去掉。
school_strip = data['school'].str.strip()
data['shool_name'] = school_strip.map(code)
data
ID | school | shool_name | |
---|---|---|---|
0 | Sally | 1 | 工程学院 |
1 | Bob | 2 | 外国语学院 |
2 | Micheal | 1 | 工程学院 |
3 | Sophy | 1 | 工程学院 |
4 | Dave | 3 | 经济管理学院 |
5 | Nancy | 3 | 经济管理学院 |
6 | Mike | 4 | 水产与生命学院 |
7 | Kevin | 4 | 水产与生命学院 |
8 | Sam | 5 | 食品学院 |
也可以传入一个函数,同时实现上述工作:
data['school'].map(lambda x: code[x.strip()])
0 工程学院
1 外国语学院
2 工程学院
3 工程学院
4 经济管理学院
5 经济管理学院
6 水产与生命学院
7 水产与生命学院
8 食品学院
Name: school, dtype: object
问题:有的时候,我们从数据库中读取出数据表后,会发现有些记录其中并不是空值,而是空字符串,这种情况通过
isnull()
或dropna()
是检测不出来的,这时就需要使用replace
方法将空字符串替换成空值再进行dropna()操作。
data = pd.DataFrame({'ID': ['Sally', 'Bob', 'Micheal',
'Sophy', 'Dave', 'Nancy',
'Mike', 'Kevin', ''],
'school': ['1 ', '2 ', '1', '1 ', '3', '3', '4 ', '4', '5']})
data
ID | school | |
---|---|---|
0 | Sally | 1 |
1 | Bob | 2 |
2 | Micheal | 1 |
3 | Sophy | 1 |
4 | Dave | 3 |
5 | Nancy | 3 |
6 | Mike | 4 |
7 | Kevin | 4 |
8 | 5 |
data['ID'].isnull()
0 False
1 False
2 False
3 False
4 False
5 False
6 False
7 False
8 False
Name: ID, dtype: bool
通过下面命令将空字符串替换为pandas可以识别的空值np.nan
:
data.replace(to_replace=r'^\s*$',value=np.nan,regex=True, inplace = True)
data['ID'].isnull()
0 False
1 False
2 False
3 False
4 False
5 False
6 False
7 False
8 True
Name: ID, dtype: bool
轴标签也可以通过函数或映射进行转换。
data = pd.DataFrame(np.arange(12).reshape((3, 4)),
index=['Ohio', 'Colorado', 'New York'],
columns=['one', 'two', 'three', 'four'])
data.index
Index(['Ohio', 'Colorado', 'New York'], dtype='object')
比如如下将index取前4位,并转换成大写形式,作为新的索引:
data.index = data.index.map(lambda x: x[:4].upper())
data
one | two | three | four | |
---|---|---|---|---|
OHIO | 0 | 1 | 2 | 3 |
COLO | 4 | 5 | 6 | 7 |
NEW | 8 | 9 | 10 | 11 |
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:
data.rename(index = str.title, columns = str.upper)
ONE | TWO | THREE | FOUR | |
---|---|---|---|---|
Ohio | 0 | 1 | 2 | 3 |
Colo | 4 | 5 | 6 | 7 |
New | 8 | 9 | 10 | 11 |
rename也可以结合字典型对象,实现对部分轴标签的更新,如果希望就地修改某个数据集,传入inplace=True即可:
data.rename(index={'OHIO':'CHINA'},
columns={'three': 'five'},
inplace = True)
data
one | two | five | four | |
---|---|---|---|---|
CHINA | 0 | 1 | 2 | 3 |
COLO | 4 | 5 | 6 | 7 |
NEW | 8 | 9 | 10 | 11 |
data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()
0 | 1 | 2 | 3 | |
---|---|---|---|---|
count | 1000.000000 | 1000.000000 | 1000.000000 | 1000.000000 |
mean | 0.037158 | 0.001858 | 0.062109 | 0.008150 |
std | 1.061050 | 1.015417 | 0.992209 | 1.003546 |
min | -2.995868 | -3.306813 | -3.095956 | -2.972975 |
25% | -0.688679 | -0.729295 | -0.540290 | -0.625105 |
50% | 0.008646 | 0.016272 | 0.044720 | -0.029676 |
75% | 0.719246 | 0.700257 | 0.695037 | 0.666511 |
max | 3.588541 | 2.848981 | 3.134215 | 3.215407 |
假设你希望把所有值限定在-3到3的区间内,可以先查找全部含有“超过-3或3的值”的行,通过在布尔型DataFrame中使用any方法:
data[(np.abs(data) > 3).any(1)]
0 | 1 | 2 | 3 | |
---|---|---|---|---|
27 | 1.132252 | 2.723375 | -0.836895 | 3.215407 |
35 | 3.588541 | 1.241234 | 0.596239 | -0.300849 |
39 | -0.325007 | 0.216004 | -0.091899 | 3.088453 |
40 | -2.613008 | 1.003565 | 3.061988 | 0.241899 |
164 | 3.312097 | -0.656751 | -0.118566 | -0.401556 |
238 | -0.833591 | 0.155241 | 3.134215 | 0.593582 |
271 | 0.747324 | -0.546848 | 3.051274 | 0.212632 |
631 | -0.359728 | -0.742797 | -3.095956 | 0.559808 |
643 | -0.399871 | -3.306813 | -0.566320 | -0.349444 |
878 | 0.925237 | -3.235506 | 0.894024 | 0.320065 |
908 | 3.414265 | 1.159344 | 1.745452 | -0.807624 |
940 | 0.245908 | -0.425127 | -0.023875 | 3.013146 |
959 | 0.509382 | -1.227860 | -1.187725 | 3.052872 |
下面的代码可以将值限制在区间-3到3以内:
data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()
0 | 1 | 2 | 3 | |
---|---|---|---|---|
count | 1000.000000 | 1000.000000 | 1000.000000 | 1000.000000 |
mean | 0.036814 | 0.002501 | 0.052547 | 0.010712 |
std | 1.093839 | 1.054702 | 1.028542 | 1.041133 |
min | -3.000000 | -3.000000 | -3.000000 | -3.000000 |
25% | -0.697633 | -0.737802 | -0.553868 | -0.636042 |
50% | 0.008646 | 0.016272 | 0.044720 | -0.029676 |
75% | 0.720883 | 0.713274 | 0.697720 | 0.685727 |
max | 3.000000 | 3.000000 | 3.000000 | 3.000000 |
为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组学生的挂科率(0~1)记录数据,而你希望将它们划分为不同的区间,并附上不同的标签:
fail = [0.01, 0, 0.05, 0.1, 0.2, 0.02, 0, 0, 0, 0.3, 0.5, 0.8]
接下来将这些数据划分为“等于0”、“0到0.25”、“0.25到0.5”以及“0.5以上”几个面元。要实现该功能,你需要使用pandas的cut函数:
cats = pd.cut(fail,[-0.1,0,0.25,0.5,1.])
cats
[(0.0, 0.25], (-0.1, 0.0], (0.0, 0.25], (0.0, 0.25], (0.0, 0.25], ..., (-0.1, 0.0], (-0.1, 0.0], (0.25, 0.5], (0.25, 0.5], (0.5, 1.0]]
Length: 12
Categories (4, interval[float64]): [(-0.1, 0.0] < (0.0, 0.25] < (0.25, 0.5] < (0.5, 1.0]]
pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。其中codes属性为数据标签,categories属性为取值范围。
cats.codes
array([1, 0, 1, 1, 1, 1, 0, 0, 0, 2, 2, 3], dtype=int8)
cats.categories
IntervalIndex([(-0.1, 0.0], (0.0, 0.25], (0.25, 0.5], (0.5, 1.0]],
closed='right',
dtype='interval[float64]')
# 面元计数
pd.value_counts(cats)
(0.0, 0.25] 5
(-0.1, 0.0] 4
(0.25, 0.5] 2
(0.5, 1.0] 1
dtype: int64
跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:
pd.cut(fail,[-0.1,0,0.25,0.5,1.], right = False)
[[0.0, 0.25), [0.0, 0.25), [0.0, 0.25), [0.0, 0.25), [0.0, 0.25), ..., [0.0, 0.25), [0.0, 0.25), [0.25, 0.5), [0.5, 1.0), [0.5, 1.0)]
Length: 12
Categories (4, interval[float64]): [[-0.1, 0.0) < [0.0, 0.25) < [0.25, 0.5) < [0.5, 1.0)]
你可以通过传递一个列表或数组到labels,设置自己的面元名称:
group_names = ['正常','黄色预警','橙色预警','红色预警']
cats = pd.cut(fail,[-0.1,0,0.25,0.5,1.],labels = group_names)
cats
['黄色预警', '正常', '黄色预警', '黄色预警', '黄色预警', ..., '正常', '正常', '橙色预警', '橙色预警', '红色预警']
Length: 12
Categories (4, object): ['正常' < '黄色预警' < '橙色预警' < '红色预警']
向cut传入面元的数量,根据数据的最小值和最大值计算取值等长面元:
pd.cut(fail, 3).value_counts()
(-0.0008, 0.267] 9
(0.267, 0.533] 2
(0.533, 0.8] 1
dtype: int64
qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分,可以得到大小基本相等的面元。
pd.qcut(fail,3).value_counts()
(-0.001, 0.00667] 4
(0.00667, 0.133] 4
(0.133, 0.8] 4
dtype: int64
如果数据分布不均匀,在使用qcut指定划分面元数据时,可能会报"Bin edges must be unique"错误。这种情况下,设定`duplicates='drop’将重复的面元边界去掉。
# 如下4个面元最终被合并为3个
pd.qcut(fail,4,duplicates='drop').value_counts()
(-0.001, 0.035] 6
(0.035, 0.225] 3
(0.225, 0.8] 3
dtype: int64
qcut也可以传递自定义分位数(0到1之间的数值,包含端点):
pd.qcut(fail, [0,0.5,1.])
[(-0.001, 0.035], (-0.001, 0.035], (0.035, 0.8], (0.035, 0.8], (0.035, 0.8], ..., (-0.001, 0.035], (-0.001, 0.035], (0.035, 0.8], (0.035, 0.8], (0.035, 0.8]]
Length: 12
Categories (2, interval[float64]): [(-0.001, 0.035] < (0.035, 0.8]]
往期:
利用Python进行数据分析:准备工作
利用Python进行数据分析:缺失数据(基于DataFrame)