当遇到多维数据,数据索引超过一两个键,可通过层级索引(也叫多级索引)配合多个有不同等级的一级索引一起使用。
import numpy as np
import pandas as pd
如何用一维的Series对象表示二维数据
用元组表示索引
index = [('California', 2000), ('California', 2010),
('New York', 2000), ('New York', 2010),
('Texas', 2000), ('Texas', 2010)]
populations = [33871648, 37253956,
18976457, 19378102,
20851820, 25145561]
pop = pd.Series(populations, index=index)
pop
(California, 2000) 33871648 (California, 2010) 37253956 (New York, 2000) 18976457 (New York, 2010) 19378102 (Texas, 2000) 20851820 (Texas, 2010) 25145561 dtype: int64
# 通过元组构成的多级索引,直接取值或切片
pop[('California', 2010):('Texas', 2000)]
(California, 2010) 37253956 (New York, 2000) 18976457 (New York, 2010) 19378102 (Texas, 2000) 20851820 dtype: int64
# 不方便直接获得元组中的项的所有数据,如2010年的所有数据
pop[[i for i in pop.index if i[1] == 2010]]
(California, 2010) 37253956 (New York, 2010) 19378102 (Texas, 2010) 25145561 dtype: int64
pd可以用元组创建多级索引:MultiIndex类型
index = pd.MultiIndex.from_tuples(index)
index
MultiIndex(levels=[['California', 'New York', 'Texas'], [2000, 2010]], labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]])
level属性表示索引的等级,每个数据都有多级的不同的标签
# 将pop数据的索引重置,可见层级索引
pop = pop.reindex(index)
pop
California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
# 现在用切片的方式查询2010年的全部数据
pop[:, 2010]
California 37253956 New York 19378102 Texas 25145561 dtype: int64
pop['California'] # 不用 pop['California', :]
2000 33871648 2010 37253956 dtype: int64
二维多级索引的Series对象可以和DF对象相互转化,有专用的方法
# 二维Series转DF
pop_df = pop.unstack()
pop_df
2000 | 2010 | |
---|---|---|
California | 33871648 | 37253956 |
New York | 18976457 | 19378102 |
Texas | 20851820 | 25145561 |
# DF转二维Series
pop_df.stack()
California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
通过添加数据,可以提升数据的维数,而这很轻松方便
# 添加18岁以下人口的数据
pop_df = pd.DataFrame({'total': pop,
'under18': [9267089, 9284094,
4687374, 4318033,
5906301, 6879014]})
pop_df
total | under18 | ||
---|---|---|---|
California | 2000 | 33871648 | 9267089 |
2010 | 37253956 | 9284094 | |
New York | 2000 | 18976457 | 4687374 |
2010 | 19378102 | 4318033 | |
Texas | 2000 | 20851820 | 5906301 |
2010 | 25145561 | 6879014 |
通用函数和其他功能也适用于多级索引
# 计算18岁以下人口占总人口的比例
f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()
2000 | 2010 | |
---|---|---|
California | 0.273594 | 0.249211 |
New York | 0.247010 | 0.222831 |
Texas | 0.283251 | 0.273568 |
为Series或DF创建多级索引最直接的办法就是将index参数设置为至少二维的索引数组:
df = pd.DataFrame(np.random.rand(4, 2),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=['data1', 'data2'])
df
data1 | data2 | ||
---|---|---|---|
a | 1 | 0.987919 | 0.985122 |
2 | 0.115032 | 0.677066 | |
b | 1 | 0.433856 | 0.914717 |
2 | 0.859339 | 0.461447 |
MultiIndex的创建工作将在后台完成。 同理,如果把将元组作为键的字典传递给pd,也会默认转换为MultiIndex:
data = {('California', 2000): 33871648,
('California', 2010): 37253956,
('Texas', 2000): 20851820,
('Texas', 2010): 25145561,
('New York', 2000): 18976457,
('New York', 2010): 19378102}
pd.Series(data)
California 2000 33871648 2010 37253956 Texas 2000 20851820 2010 25145561 New York 2000 18976457 2010 19378102 dtype: int64
但有时候显式地创建MultiIndex也是很有用的,下面是一些方法:
用数组列表——一个有不同等级的若干简单数组组成的列表:
pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
MultiIndex(levels=[['a', 'b'], [1, 2]], labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
用元组列表——包含多个索引值的元组构成的列表:
pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
MultiIndex(levels=[['a', 'b'], [1, 2]], labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
用两个索引的笛卡尔积创建:
pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
MultiIndex(levels=[['a', 'b'], [1, 2]], labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
直接提供levels和labels参数创建:
pd.MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
MultiIndex(levels=[['a', 'b'], [1, 2]], labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
MultiIndex对象可作为创建Series或DF对象的index参数,或用reindex方法更新。
给MI对象的等级加上名称,会为某些操作提供方便。可在MI对象创建时用names参数添加名称,也可直接为MI对象的names属性来修改名称:
pop.index.names = ['state', 'year']
pop
state year California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
pop_df.index.names = ['state', 'year']
pop_df
total | under18 | ||
---|---|---|---|
state | year | ||
California | 2000 | 33871648 | 9267089 |
2010 | 37253956 | 9284094 | |
New York | 2000 | 18976457 | 4687374 |
2010 | 19378102 | 4318033 | |
Texas | 2000 | 20851820 | 5906301 |
2010 | 25145561 | 6879014 |
DF对象的行列是对称的,可以有多级行索引,也可以有多级列索引:
# 多级行索引和列索引的创建
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]],
names=['year', 'visit'])
columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'], ['HR', 'Temp']],
names=['subject', 'type'])
# 模拟数据
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37
# 创建DataFrame
health_data = pd.DataFrame(data, index=index, columns=columns)
health_data
subject | Bob | Guido | Sue | ||||
---|---|---|---|---|---|---|---|
type | HR | Temp | HR | Temp | HR | Temp | |
year | visit | ||||||
2013 | 1 | 24.0 | 38.4 | 58.0 | 36.0 | 28.0 | 36.9 |
2 | 57.0 | 36.0 | 32.0 | 37.4 | 42.0 | 36.9 | |
2014 | 1 | 50.0 | 37.5 | 43.0 | 37.9 | 56.0 | 38.7 |
2 | 30.0 | 36.4 | 37.0 | 38.8 | 22.0 | 38.5 |
多级行列索引的创建非常简单,上边创建了一个四维数据。可在列索引的第一级查询姓名,从而获得一人的全部检查信息的DF。
health_data['Guido']
type | HR | Temp | |
---|---|---|---|
year | visit | ||
2013 | 1 | 58.0 | 36.0 |
2 | 32.0 | 37.4 | |
2014 | 1 | 43.0 | 37.9 |
2 | 37.0 | 38.8 |
如果想获取包含多种标签的数据,需要通过对多维度(如姓名、国家、城市等标签)的多次查询才能实现,这时用多级行列索引进行查询会非常方便。
对MI的取值和切片操作很直观,可以直接把索引看成额外增加的维度。
pop
state year California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
可用多个级别的索引值获取单个数据元素:
pop['California', 2000]
33871648
MI可以支持局部取值,即只取索引的某一个层级。假如只取最高级的索引,获得结果是一个新Series对象,未被选中的低层索引值会被保留:
pop['California']
year 2000 33871648 2010 37253956 dtype: int64
类似的还有局部切片,但要求切片参数是按照MI顺序排列的:
pop.loc['California':'New York']
state year California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 dtype: int64
如果索引已经排序,可以用较低层级的索引取值,第一层索引用空切片:
pop[:, 2000]
state California 33871648 New York 18976457 Texas 20851820 dtype: int64
还有其他可用的取值方式,如布尔掩码
pop[pop > 22000000]
state year California 2000 33871648 2010 37253956 Texas 2010 25145561 dtype: int64
或花哨索引
pop[['California', 'Texas']]
state year California 2000 33871648 2010 37253956 Texas 2000 20851820 2010 25145561 dtype: int64
health_data
subject | Bob | Guido | Sue | ||||
---|---|---|---|---|---|---|---|
type | HR | Temp | HR | Temp | HR | Temp | |
year | visit | ||||||
2013 | 1 | 24.0 | 38.4 | 58.0 | 36.0 | 28.0 | 36.9 |
2 | 57.0 | 36.0 | 32.0 | 37.4 | 42.0 | 36.9 | |
2014 | 1 | 50.0 | 37.5 | 43.0 | 37.9 | 56.0 | 38.7 |
2 | 30.0 | 36.4 | 37.0 | 38.8 | 22.0 | 38.5 |
由于DF的基本索引是列索引,多级索引用在列上,如:
health_data['Guido', 'HR']
year visit 2013 1 58.0 2 32.0 2014 1 43.0 2 37.0 Name: (Guido, HR), dtype: float64
health_data['Guido']
type | HR | Temp | |
---|---|---|---|
year | visit | ||
2013 | 1 | 58.0 | 36.0 |
2 | 32.0 | 37.4 | |
2014 | 1 | 43.0 | 37.9 |
2 | 37.0 | 38.8 |
loc、iloc等索引器都可以使用,如:
health_data.iloc[:2, :2]
subject | Bob | ||
---|---|---|---|
type | HR | Temp | |
year | visit | ||
2013 | 1 | 24.0 | 38.4 |
2 | 57.0 | 36.0 |
虽然这些索引器将多维数据当作二维数据处理,但是在loc和iloc中可以传递多个层级的索引元组,如:
health_data.loc[:, ('Bob', 'HR')]
year visit 2013 1 24.0 2 57.0 2014 1 50.0 2 30.0 Name: (Bob, HR), dtype: float64
在元组中使用切片语法会导致错误,这时可以使用IndexSlice对象,如:
idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']]
# 获得行索引的每年第一次访问、和列索引的每人的HR数据
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | HR | HR | |
year | visit | |||
2013 | 1 | 24.0 | 58.0 | 28.0 |
2014 | 1 | 50.0 | 43.0 | 56.0 |
使用多级索引的关键是掌握有效数据转换的方法。pd提供了许多操作,可以让数据在内容不变的同时,按照需要进行行列转换。除了前述的stack和unstack方法之外,还有许多合理控制层级行列索引的方法
如果MultiIndex不是有序的索引(没有排好序),那么大多数切片操作都会失败。 首先创建一个不按照字典顺序排列的多级索引Series:
index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])
data = pd.Series(np.random.rand(6), index=index)
data.index.names = ['char', 'int']
data
char int a 1 0.991095 2 0.843417 c 1 0.227163 2 0.755347 b 1 0.276651 2 0.702083 dtype: float64
如果想对索引使用局部切片,就会出错:
try:
data['a':'b']
except KeyError as e:
print(type(e))
print(e)
'Key length (1) was greater than MultiIndex lexsort depth (0)'
问题就在MI对象的顺序无序上,而切片等操作要求字典序。解决这问题可用pd的方法sort_index()和sortlevel()来先给索引排序。之后就可切片。
data = data.sort_index()
data
char int a 1 0.991095 2 0.843417 b 1 0.276651 2 0.702083 c 1 0.227163 2 0.755347 dtype: float64
data['a':'b']
char int a 1 0.991095 2 0.843417 b 1 0.276651 2 0.702083 dtype: float64
如前述,可将一个多级索引数据集转换成简单的二维形式,可通过level参数设置转换的索引层级:
pop
state year California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
pop.unstack(level=0)
state | California | New York | Texas |
---|---|---|---|
year | |||
2000 | 33871648 | 18976457 | 20851820 |
2010 | 37253956 | 19378102 | 25145561 |
pop.unstack(level=1)
year | 2000 | 2010 |
---|---|---|
state | ||
California | 33871648 | 37253956 |
New York | 18976457 | 19378102 |
Texas | 20851820 | 25145561 |
pop.unstack() # 默认不动0级而把1级拉到列
year | 2000 | 2010 |
---|---|---|
state | ||
California | 33871648 | 37253956 |
New York | 18976457 | 19378102 |
Texas | 20851820 | 25145561 |
unstack是stack的逆操作,同时使用两方法,数据不变:
pop.unstack().stack()
state year California 2000 33871648 2010 37253956 New York 2000 18976457 2010 19378102 Texas 2000 20851820 2010 25145561 dtype: int64
层级数据维度转换的另一种方法是行列标签转换,可通过reset_index方法实现。如在上面的人口数据Series中使用该方法,则会生成一个列标签中包含之前行索引标签的state和year的DF。也可以用数据的name属性为列设置名称:
pop_flat = pop.reset_index(name='population')
pop_flat
state | year | population | |
---|---|---|---|
0 | California | 2000 | 33871648 |
1 | California | 2010 | 37253956 |
2 | New York | 2000 | 18976457 |
3 | New York | 2010 | 19378102 |
4 | Texas | 2000 | 20851820 |
5 | Texas | 2010 | 25145561 |
在解决实际问题的时候,如果能将类似这样的原始输入数据的列直接转换成MultiIndex,通常将大有裨益。实际上可通过DF的set_index方法实现,返回结果就是一个带多级索引的DF:
pop_flat.set_index(['state', 'year'])
population | ||
---|---|---|
state | year | |
California | 2000 | 33871648 |
2010 | 37253956 | |
New York | 2000 | 18976457 |
2010 | 19378102 | |
Texas | 2000 | 20851820 |
2010 | 25145561 |
实践中,用这种重建索引的方法处理数据集非常好用。
pd自带的数据累计方法有mean,sum,max等,而对于层级索引数据,可以设置参数level实现对数据子集的累计操作。
health_data
subject | Bob | Guido | Sue | ||||
---|---|---|---|---|---|---|---|
type | HR | Temp | HR | Temp | HR | Temp | |
year | visit | ||||||
2013 | 1 | 24.0 | 38.4 | 58.0 | 36.0 | 28.0 | 36.9 |
2 | 57.0 | 36.0 | 32.0 | 37.4 | 42.0 | 36.9 | |
2014 | 1 | 50.0 | 37.5 | 43.0 | 37.9 | 56.0 | 38.7 |
2 | 30.0 | 36.4 | 37.0 | 38.8 | 22.0 | 38.5 |
如果计算每一年各项指标的平均值,可将参数level设置为索引year:
data_mean = health_data.mean(level='year')
data_mean
subject | Bob | Guido | Sue | |||
---|---|---|---|---|---|---|
type | HR | Temp | HR | Temp | HR | Temp |
year | ||||||
2013 | 40.5 | 37.20 | 45.0 | 36.70 | 35.0 | 36.9 |
2014 | 40.0 | 36.95 | 40.0 | 38.35 | 39.0 | 38.6 |
在data_mean的基础上,如果再设置axis参数,就可以对列索引进行类似累计操作了:
data_mean.mean(axis=1, level='type')
# 必须设定axis=1才能把level设为列中的type参数,因为行中无此索引
type | HR | Temp |
---|---|---|
year | ||
2013 | 40.166667 | 36.933333 |
2014 | 39.666667 | 37.966667 |
pd还有Panel对象和Panel4D对象,分别可看成一维数组Series和二维数组DF的三维与四维形式。但大多数情况多级索引已经够用。