利用Python进行数据分析:数据转换(基于DataFrame)

利用Python进行数据分析:数据转换

最近在做一个数据分析类项目,涉及处理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
k1k2
0one1
1two1
2one2
3two3
4one3
5two4
6two4

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()
k1k2
0one1
1two1
2one2
3two3
4one3
5two4

这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断,比如仅根据k1列过滤重复项:

data.drop_duplicates(['k1'])
k1k2
0one1
1two1

传入keep=’last’则保留最后一个出现的(默认为keep=‘first’):

data.drop_duplicates(keep = 'last')
k1k2
0one1
1two1
2one2
3two3
4one3
6two4

利用函数或映射进行数据转换

使用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
IDschool
0Sally1
1Bob2
2Micheal1
3Sophy1
4Dave3
5Nancy3
6Mike4
7Kevin4
8Sam5

先编写一个不同指代码到学院的映射:

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
IDschoolshool_name
0Sally1工程学院
1Bob2外国语学院
2Micheal1工程学院
3Sophy1工程学院
4Dave3经济管理学院
5Nancy3经济管理学院
6Mike4水产与生命学院
7Kevin4水产与生命学院
8Sam5食品学院

也可以传入一个函数,同时实现上述工作:

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
IDschool
0Sally1
1Bob2
2Micheal1
3Sophy1
4Dave3
5Nancy3
6Mike4
7Kevin4
85
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
onetwothreefour
OHIO0123
COLO4567
NEW891011

如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:

data.rename(index = str.title, columns = str.upper)
ONETWOTHREEFOUR
Ohio0123
Colo4567
New891011

rename也可以结合字典型对象,实现对部分轴标签的更新,如果希望就地修改某个数据集,传入inplace=True即可:

data.rename(index={'OHIO':'CHINA'},
           columns={'three': 'five'},
           inplace = True)
data
onetwofivefour
CHINA0123
COLO4567
NEW891011

检测和过滤异常值

data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()
0123
count1000.0000001000.0000001000.0000001000.000000
mean0.0371580.0018580.0621090.008150
std1.0610501.0154170.9922091.003546
min-2.995868-3.306813-3.095956-2.972975
25%-0.688679-0.729295-0.540290-0.625105
50%0.0086460.0162720.044720-0.029676
75%0.7192460.7002570.6950370.666511
max3.5885412.8489813.1342153.215407

假设你希望把所有值限定在-3到3的区间内,可以先查找全部含有“超过-3或3的值”的行,通过在布尔型DataFrame中使用any方法:

data[(np.abs(data) > 3).any(1)]
0123
271.1322522.723375-0.8368953.215407
353.5885411.2412340.596239-0.300849
39-0.3250070.216004-0.0918993.088453
40-2.6130081.0035653.0619880.241899
1643.312097-0.656751-0.118566-0.401556
238-0.8335910.1552413.1342150.593582
2710.747324-0.5468483.0512740.212632
631-0.359728-0.742797-3.0959560.559808
643-0.399871-3.306813-0.566320-0.349444
8780.925237-3.2355060.8940240.320065
9083.4142651.1593441.745452-0.807624
9400.245908-0.425127-0.0238753.013146
9590.509382-1.227860-1.1877253.052872

下面的代码可以将值限制在区间-3到3以内:

data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()
0123
count1000.0000001000.0000001000.0000001000.000000
mean0.0368140.0025010.0525470.010712
std1.0938391.0547021.0285421.041133
min-3.000000-3.000000-3.000000-3.000000
25%-0.697633-0.737802-0.553868-0.636042
50%0.0086460.0162720.044720-0.029676
75%0.7208830.7132740.6977200.685727
max3.0000003.0000003.0000003.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)

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年5月26日
下一篇 2022年5月26日

相关推荐