Python for Data Analysis V

Posted by Gloomymoon on 2016-12-27

7.2 重塑和轴向旋转

重塑层次化索引

  • stack:将数据的列旋转为行
  • unstack:将数据的行旋转为列
1
2
3
4
data = DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(['Ohio', 'Colorado'], name='state'),
columns=pd.Index(['one', 'two', 'three'], name='number'))
data

Data 1:

1
2
3
4
number	one	two	three
state
Ohio 0 1 2
Colorado 3 4 5

使用stack方法可以将列转换为行,得到一个Series:

1
2
result = data.stack()
result

Output:

1
2
3
4
5
6
7
8
state     number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int64

对于一个层次化索引的Series,看可以用unstack将其重排为一个DataFrame:

1
result.unstack()

Output like Data 1

默认情况下,unstackstack操作的是最内层索引,但可以传入分层级别的编号或名称对其他级别进行重塑。

1
2
result.unstack(0)
result.unstack('state')

Output:

1
2
3
4
5
state	Ohio	Colorado
number
one 0 3
two 1 4
three 2 5

如果在重塑时,不是所有的分组都有值,则unstack操作会引入缺失值,而stack操作会滤除缺失值,因此这两个操作互相可逆。在DataFrame进行unstack操作时,作为旋转轴的级别会成为结果中最低级别。

将“长格式”旋转为“宽格式”

一般时间序列数据是以“长格式”或“堆叠格式”存储的,时间作为主键或主键之一,记录数据的关系型数据大多这样保存原始的时间序列数据。

1
ldata[:10]

Data:

1
2
3
4
5
6
7
8
9
10
11
	date	item	value
0 1959-03-31 realgdp 2710.349
1 1959-03-31 infl 0.000
2 1959-03-31 unemp 5.800
3 1959-06-30 realgdp 2778.801
4 1959-06-30 infl 2.340
5 1959-06-30 unemp 5.100
6 1959-09-30 realgdp 2775.488
7 1959-09-30 infl 2.740
8 1959-09-30 unemp 5.300
9 1959-12-31 realgdp 2785.204

在DataFrame中,你可能更喜欢这样操作数据:不同的item值分别形成一列,data列中的时间值则用做索引。DataFrame的pivot方法可以实现这个转换。

1
2
pivoted = ldata.pivot('date', 'item', 'value')
pivoted.head()

Output:

1
2
3
4
5
6
7
item	infl	realgdp	unemp
date
1959-03-31 0.00 2710.349 5.8
1959-06-30 2.34 2778.801 5.1
1959-09-30 2.74 2775.488 5.3
1959-12-31 0.27 2785.204 5.6
1960-03-31 2.31 2847.699 5.2

pivot的前两个参数值分别用作行和列索引的列名,最后一个参数值则是用于填充DataFrame的数据列的列名。如果有两个需要参与重塑的数据列,忽略最后一个参数得到的DataFrame就会带有层次化的列。

1
2
ldata['value2'] = np.random.randn(len(ldata))
ldata[:10]

Data:

1
2
3
4
5
6
7
8
9
10
11
	date	item	value	value2
0 1959-03-31 realgdp 2710.349 1.669025
1 1959-03-31 infl 0.000 -0.438570
2 1959-03-31 unemp 5.800 -0.539741
3 1959-06-30 realgdp 2778.801 0.476985
4 1959-06-30 infl 2.340 3.248944
5 1959-06-30 unemp 5.100 -1.021228
6 1959-09-30 realgdp 2775.488 -0.577087
7 1959-09-30 infl 2.740 0.124121
8 1959-09-30 unemp 5.300 0.302614
9 1959-12-31 realgdp 2785.204 0.523772

本质上pivot等同于:用set_index创建层次化索引后再使用unstack重塑。下面两段代码的效果相同。

1
2
3
4
pivoted = ldata.pivot('date', 'item')
pivoted[:5]
unstacked = ldata.set_index(['date', 'item']).unstack('item')
unstacked[:5]

Output:

1
2
3
4
5
6
7
8
	value	value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 0.00 2710.349 5.8 -0.438570 1.669025 -0.539741
1959-06-30 2.34 2778.801 5.1 3.248944 0.476985 -1.021228
1959-09-30 2.74 2775.488 5.3 0.124121 -0.577087 0.302614
1959-12-31 0.27 2785.204 5.6 0.000940 0.523772 1.343810
1960-03-31 2.31 2847.699 5.2 -0.831154 -0.713544 -2.370232

7.3 数据转换

移除重复数据

DataFrame中常常会出现重复行,DataFrame的duplicated返回一个布尔型Series,表示各行是否重复,DataFrame的dorp_duplicates返回一个移除重复行的DataFrame。默认情况下判断所有列,但是也可以传入希望过滤的列列表。默认情况下保留的是第一个出现的值组合,传入take_last=True参数则保留最后一个。

1
2
3
4
5
6
data = DataFrame({'k1': ['one'] * 3 + ['two'] * 4,
'k2': [1, 1, 2, 3, 3, 4, 4]})
data['v1'] = range(7)
data.drop_duplicates(['k1'])

data.drop_duplicates(['k1', 'k2'], take_last=True)

Output:

1
2
3
4
5
6
7
8
9
10
	k1	k2	v1
0 one 1 0
3 two 3 3

/Library/Python/2.7/site-packages/ipykernel/__main__.py:1: FutureWarning: the take_last=True keyword is deprecated, use keep='last' instead
k1 k2 v1
1 one 1 1
2 one 2 2
4 two 3 4
6 two 4 6

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

更多的时候在处理数据时,我们希望根据某种逻辑关系对值进行转换。在说明前先看下如下关于肉类的演示数据:

1
2
data = DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 'corned beef', 'Bacon', 'pastrami', 'honey ham', 'nova lox'], 'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
data

Data:

1
2
3
4
5
6
7
8
9
10
	food	ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 corned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0

最常见的处理是创建新分类/分组,例如需要添加一列表示food来源的动物类型,肉类到动物的映射关系如下:

1
2
3
4
5
6
7
8
meat_to_animal = {
'bacon': 'pig',
'pulled pork': 'pig',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pig',
'nova lox': 'salmon'
}

Series的map方法可以根据一个函数或含有映射关系的字典对象,产生新的Series。

1
2
data['animal'] = data['food'].map(str.lower).map(meat_to_animal)
data

Output:

1
2
3
4
5
6
7
8
9
10
	food	ounces	animal
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 Pastrami 6.0 cow
4 corned beef 7.5 cow
5 Bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon

注意因为原始数据里包含有大小写,因此首先做了一次小写转换。当然也可以直接传入一个完成全部工作的lambda函数,效果是一样的:

1
data['food'].map(lambda x: meat_to_animal[x.lower()])

值替换

fillna方法是一种值替换的特殊情况,replace方法是更加通用、更加简单的方法。

1
2
3
4
5
6
7
8
9
10
data = Series([1., -999., 2., -999., -1000., 3.])
data

0 1.0
1 -999.0
2 2.0
3 -999.0
4 -1000.0
5 3.0
dtype: float64

-999可能是一个表示缺失的标记值,可以用replace方法替换为pandas的nan

1
data.replace(-999, np.nan)

如果希望一次性替换多个值,可以传入一个由待替换值和替换值组成的列表或字典,下面两种方式等效:

1
2
data.replace([-999, -1000], [np.nan, 0]))
data.replace({-999: np.nan, -1000: 0})

重命名轴索引

map方法同样可以对数据的轴标签使用。DataFrame的rename方法可以直接返回修改index和columns的对象,如果希望就地修改不产生新对象,可以传入参数inplace=True

离散化和面元划分

为了便于分析,连续数据常常被离散化或分bin,例如将年龄划分为不同的年龄组。

Data:

1
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

如果要将这些数据分为“18到25”、“26到35”、“35到60”和“60以上”几个组,可以使用pandas的cut函数:

1
2
3
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats

Output:

1
2
3
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, object): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

cut函数返回的是一个特殊的Categorical对象。它含有一个表示不同分类名称的categories索引对象和一个为分组进行标号的levels数组。

1
2
3
4
5
cats.labels

/Library/Python/2.7/site-packages/ipykernel/__main__.py:1: FutureWarning: 'labels' is deprecated. Use 'codes' instead
if __name__ == '__main__':
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
1
2
3
cats.categories

Index([u'(18, 25]', u'(25, 35]', u'(35, 60]', u'(60, 100]'], dtype='object')

categories是一个Index对象,实质是一个由python对象组成的 NumPy数组,其中存放的是每个分组的描述字符串。

如果想cut传入的是分组的数量而不是确切的分组边界,则会根据数据的最小值和最大值计算等长分组。qcut是个类似于cut的函数,它根据样本的分位数对数据进行分组划分。

1
2
3
data = np.random.randn(1000) # 正态分布
cats = pd.qcut(data, 4) # 按四分位数进行切割
cats

Output:

1
2
3
[(-0.022, 0.634], [-3.745, -0.648], (0.634, 3.26], (-0.022, 0.634], (-0.648, -0.022], ..., (0.634, 3.26], (-0.022, 0.634], [-3.745, -0.648], (-0.022, 0.634], (-0.022, 0.634]]
Length: 1000
Categories (4, object): [[-3.745, -0.648] < (-0.648, -0.022] < (-0.022, 0.634] < (0.634, 3.26]]

cut一样,可以设置自定义的分位数(0到1之间的数值,包含端点)。

1
pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])

Output:

1
2
3
[(-0.022, 1.298], [-3.745, -1.274], (-0.022, 1.298], (-0.022, 1.298], (-1.274, -0.022], ..., (-0.022, 1.298], (-0.022, 1.298], [-3.745, -1.274], (-0.022, 1.298], (-0.022, 1.298]]
Length: 1000
Categories (4, object): [[-3.745, -1.274] < (-1.274, -0.022] < (-0.022, 1.298] < (1.298, 3.26]]

检测和过滤异常值

异常值(这里指异常点或离群值)的过滤或变换本质上就是数组的运算,利用布尔型DataFrame方法可以选出异常值,然后就可以轻松设置。

1
2
3
4
np.random.seed(12345)
data = DataFrame(np.random.randn(1000, 4))
data[np.abs(data) > 3 ] = np.sign(data) * 3 # 将绝对值超过3的值限制在-3到3之间
data[(np.abs(data) > 3).any(1)]

No output.

排列和随机采样

利用numpy.random.permutation函数可以实现对Series或DataFrame的列重排,传入需要排列的轴长度值,可产生一个表示新顺序的整数数组。

1
2
3
df = DataFrame(np.arange(5 * 4).reshape(5, 4))
sampler = np.random.permutation(5)
sampler

Output:

1
array([1, 0, 2, 3, 4])

然后就可以在基于ix的索引操作或take函数中使用该数组实现对DataFrame的重排序。

1
df.take(sampler)

Output:

1
2
3
4
5
6
	0	1	2	3
1 4 5 6 7
0 0 1 2 3
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19

如果希望进行重复采样,可以通过np.random.randint得到一组随机整数。

1
2
sampler = np.random.randint(0, len(df), size=10) # 随机抽取10条
df.take(sampler)

Oupput:

1
2
3
4
5
6
7
8
9
10
11
	0	1	2	3
2 8 9 10 11
2 8 9 10 11
4 16 17 18 19
3 12 13 14 15
3 12 13 14 15
0 0 1 2 3
1 4 5 6 7
4 16 17 18 19
1 4 5 6 7
4 16 17 18 19

计算指标/哑变量

另一种常用于统计建模或机器学习的转换方式是将分类变量转换为“哑变量矩阵”或“指标矩阵”。例如某一个列中含有k个不同的取值,则可以派生出一个k列矩阵,每列的值都是1或0。pandas有一个get_dummies函数可以直接实现该功能。

1
2
df = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)})
pd.get_dummies(df['key'])

Output:

1
2
3
4
5
6
7
	a	b	c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0

如果DataFrame中某分类变量列的值属于多个分类,就需要做一些数据规整操作。

1
2
3
4
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('../../../pydata-book-master/ch02/movielens/movies.dat', sep='::', header=None,
names=mnames)
movies[:10]

Data:

1
2
3
4
5
6
7
8
9
10
11
	movie_id	title	genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy
5 6 Heat (1995) Action|Crime|Thriller
6 7 Sabrina (1995) Comedy|Romance
7 8 Tom and Huck (1995) Adventure|Children's
8 9 Sudden Death (1995) Action
9 10 GoldenEye (1995) Action|Adventure|Thriller

以之前MovieLens 1M数据为例,首先需要从genres列中抽取出不同的genre值g。

1
2
3
genre_iter = (set(x.split('|')) for x in movies.genres)
genres = sorted(set.union(*genre_iter))
genres

genre_iter是一个表达式生成器,set.union参数中,*genre_iter表示将生成器的每次迭代都作为一个set对象,最后传给union方法获得并集。Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
['Action',
'Adventure',
'Animation',
"Children's",
'Comedy',
'Crime',
'Documentary',
'Drama',
'Fantasy',
'Film-Noir',
'Horror',
'Musical',
'Mystery',
'Romance',
'Sci-Fi',
'Thriller',
'War',
'Western']

为了构建指标,我们先从一个全零的DataFrame对象dummies开始,迭代每一部电影并将dummies各行的项设置为1或0,最后再将其与movies合并。

1
2
3
4
5
dummies = DataFrame(np.zeros((len(movies), len(genres))), columns=genres)
for i, gen in enumerate(movies.genres):
dummies.ix[i, gen.split('|')] = 1
movies_windic = movies.join(dummies.add_prefix('Gere_'))
movies_windic.ix[0]

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
movie_id                                      1
title Toy Story (1995)
genres Animation|Children's|Comedy
Gere_Action 0
Gere_Adventure 0
Gere_Animation 1
Gere_Children's 1
Gere_Comedy 1
Gere_Crime 0
Gere_Documentary 0
Gere_Drama 0
Gere_Fantasy 0
Gere_Film-Noir 0
Gere_Horror 0
Gere_Musical 0
Gere_Mystery 0
Gere_Romance 0
Gere_Sci-Fi 0
Gere_Thriller 0
Gere_War 0
Gere_Western 0
Name: 0, dtype: object

一个实用的秘诀是将get_dummies和诸如cut的离散化函数生成你需要的哑变量指标。