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

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

最近在做一个数据分析类项目,涉及处理7万+名学生的全学程数据,数据以表格型结构化数据为主,涉及学生基本信息、成绩和课程信息、评奖评优、勤工助学及行为数据。借此机会,对项目中频繁使用的基于DataFrame 的Python 数据分析语句进行梳理。此篇主要针对数据转换,包括移除重复数据、利用函数或映射进行数据转换、替换值、重命名轴索引、检测和过滤异常值、离散化和面元划分。

文章目录

  • 利用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)

你可能感兴趣的:(数据分析,数据分析,python,数据挖掘)