Pandas索引访问

索引操作可以方便地获取 Pandas 基础数据结构中的指定元素。

1
import pandas as pd

索引器

  • DataFrame 的索引形式是列索引,即通过 [列名] 的形式可以从 DataFrame 中取出相应的列。如果是单个列名,则返回一个 Series 对象;如果是多个列名组成的列表,则返回一个 DataFrame。此外,若要取出单列,且列名中不包含空格,则还可以使用 .列名 的形式取出。

    1
    2
    3
    4
    df = pd.read_csv('./data/learn_pandas.csv', usecols=['School', 'Grade', 'Name',
    'Gender', 'Weight', 'Transfer'])
    print(df['Name'].head()) # 单列Series对象 == df.Name.head()
    print(df[['Gender', 'Name']].head()) # 多列组成的DataFrame
  • Series 的索引形式是行索引,即通过 [idx] 的形式可以从 Series 中取出相应的行,这里的 idx 可以是字符串也可以是整数(包括未指定索引时从0开始的缺省值)。如果 idx 对应的只有单个值,则返回这个标量值;否则返回 Series 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 以字符串为索引的Series
    s = pd.Series([1, 2, 3, 4, 5, 6], index=['a', 'b', 'a', 'a', 'a', 'c'])
    print(s['a']) # Series([1, 3, 4, 5], index=['a', 'a', 'a', 'a'])
    print(s['b']) # 2
    print(s[['c', 'b']]) # Series([6, 2], index=['c', 'b'])
    # 如果想要取出某两个索引之间的元素,并且这两个索引是在整个索引中唯一出现,则可以使用切片
    # 需要注意的是这里的切片会包含两个端点!
    print(s['c': 'b': -2]) # Series([6, 4, 2], index=['c', 'a', 'b'])

    # 以整数为索引的Series
    s = pd.Series(['a', 'b', 'c', 'd', 'e', 'f'], index=[1, 3, 1, 2, 5, 4])
    print(s[1]) # Series(['a', 'c'], index=[1, 1])
    print(s[[2, 3]]) # Series(['d', 'b'], index=[2, 3])
    # 如果使用整数切片,则会取出对应索引位置的值,注意这里的整数切片同 Python 中的切片一样不包含右端点
    print(s[1:-1:2]) # Series(['b', 'd'], index=[3, 2])

DataFrame 默认的中括号索引是列索引,还需要支持行索引才可以方便我们按行访问数据,所以 Pandas 为 DataFrame 提供了另外两种行索引器:一种是基于元素的 loc索引器,另一种是基于位置的 iloc索引器

  • loc 索引器的一般形式是 loc[*, *] ,其中第一个 * 代表行的选择,第二个 * 代表列的选择,如果省略第二个位置写作 loc[*] ,这个 * 是指行的筛选。其中,* 的位置一共有五类合法对象,分别是:单个元素、元素列表、元素切片、布尔列表以及函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    df_demo = df.set_index('Name') # 将“姓名”设置为索引
    print(df_demo.loc['Qiang Sun']) # 改名字存在多人,则返回DataFrame; 如果只有一个,则返回Series
    print(df_demo.loc['Qiang Sun', 'School']) # 同时选择行和列
    print(df_demo.loc[['Qiang Sun','Quan Zhao'], ['School','Gender']])

    # 使用切片: 无论是整数还是字符串类型的索引,切片都包含起点与终点,且不允许有重复值,否则报错
    print(df_demo.loc['Gaojuan You':'Gaoqiang Qian', 'School':'Gender'])

    # 使用布尔列表: 要求传入的布尔列表长度与DataFrame相同
    print(df_demo.loc[df_demo.Weight>70])
    condition_1_1 = df_demo.School == 'Fudan University'
    condition_1_2 = df_demo.Grade == 'Senior'
    condition_1_3 = df_demo.Weight > 70
    condition_1 = condition_1_1 & condition_1_2 & condition_1_3
    print(df_demo.loc[condition_1]) # 复合逻辑判断组成的布尔列表

    # 使用函数: 返回值必须是索引支持的类型(上述几种),函数的输入是Dataframe本身
    def condition(x):
    condition_2_1 = x.School == 'Peking University'
    condition_2_2 = x.Grade == 'Senior'
    condition_2_3 = x.Weight > 80
    condition_2 = condition_2_1 & (~condition_2_2) & condition_2_3
    return condition_2
    print(df_demo.loc[condition])
  • iloc 索引器的使用与 loc 完全类似,只不过是针对位置进行筛选,在相应的* 位置处一共也有五类合法对象,分别是:整数、整数列表、整数切片、布尔列表以及函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    print(df_demo.iloc[1, 1]) # 第二行第二列
    print(df_demo.iloc[[0, 1], [0, 1]]) # 前两行前两列

    # 使用切片: 注意不包含终点!
    print(df_demo.iloc[1: 4, 2:4])

    # 使用布尔列表: 注意需要出入的是条件序列的 values 属性!
    print(df_demo.iloc[(df_demo.Weight>80).values])

    # 使用函数: 返回值必须是索引支持的类型(上述几种),函数的输入是Dataframe本身
    print(df_demo.iloc[lambda x: slice(1, 4)]) # lambda函数的返回值是切片

在使用索引对表或者序列赋值时需要特别注意,应当在使用一层索引器后直接进行赋值操作,这样做是由于进行多次索引后赋值是赋在临时返回的 copy 副本上的,而没有真正修改元素。

query方法

另一种对 DataFrame 进行查询的方法是 query 方法,它支持直接使用字符串形式的查询表达式进行查询数据,其表达式的执行结果必须返回布尔列表。在 query 表达式中,帮用户注册了所有来自 DataFrame 的列名,所有属于列 Series 的方法都可以被调用,和正常的函数调用并没有区别

1
2
3
4
5
6
7
8
9
10
11
print(df.query('((School == "Fudan University")&'
' (Grade == "Senior")&'
' (Weight > 70)) or '
'((School == "Peking University") and '
' (Grade != "Senior")&'
' (Weight > 80))'))
# Weight列作为Series的内置函数,可以直接 . 调用
print(df.query('Weight > Weight.mean()').head())
# 如果要引用外部变量,只需在变量名前加 @ 符号
low, high = 70, 80
print(df.query('Weight>@low and Weight<@high').head())

可以看出,query 表达式支持 python 中的逻辑单词(or, and, is in等);对于含有空格的列名,需要使用反引号括住列名(`col name`)的方式进行引用。

随机抽样

还有一种返回 DataFrame 数据的方法就是随机抽样,如果把 DataFrame 的每一行看作一个样本,而把每一列看作一个特征,再把整个 DataFrame 看作总体,有时候想要对样本或特征进行随机抽样就可以用 sample 函数。

1
2
3
4
df_sample = pd.DataFrame({'id': list('abcde'),
'value': [1, 2, 3, 4, 90]})
# 以数据的value值的相对大小为抽样概率进行有放回抽样,抽样数量为3
print(df_sample.sample(3, replace=True, weights=df_sample.value))

sample 函数中的主要参数为 n, axis, frac, replace, weights,前三个分别是指抽样数量、抽样的方向(0 为行、1 为列)和抽样比例(0.3 则为从总体中抽出30% 的样本)。replace 和 weights 分别是指是否放回和每个样本的抽样相对概率,当replace = True 则表示有放回抽样

多级索引

我们基于上面的 DataFrame 表格数据,人工造一个具有多级索引的新表格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
np.random.seed(0)
multi_index = pd.MultiIndex.from_product([list('ABCD'), df.Gender.unique()],
names=('School', 'Gender'))
multi_column = pd.MultiIndex.from_product([['Height', 'Weight'], df.Grade.unique()],
names=('Indicator', 'Grade'))
# 身高和体重都是8行4列的数据,基于163cm和65kg上下浮动5个单位左右
df_multi = pd.DataFrame(np.c_[(np.random.randn(8,4)*5 + 163).tolist(),
(np.random.randn(8,4)*5 + 65).tolist()],
index = multi_index,
columns = multi_column).round(1)
print(df_multi)

'''
--------------------------------------------------------------------------------
Indicator Height Weight
Grade Freshman Senior Sophomore Junior Freshman Senior Sophomore Junior
School Gender
A Female 171.8 165.0 167.9 174.2 60.6 55.1 63.3 65.8
Male 172.3 158.1 167.8 162.2 71.2 71.0 63.1 63.5
B Female 162.5 165.1 163.7 170.3 59.8 57.9 56.5 74.8
Male 166.8 163.6 165.2 164.7 62.5 62.8 58.7 68.9
C Female 170.5 162.0 164.6 158.7 56.9 63.9 60.5 66.9
Male 150.2 166.3 167.3 159.3 62.4 59.1 64.9 67.1
D Female 174.3 155.7 163.2 162.1 65.3 66.5 61.8 63.2
Male 170.7 170.3 163.8 164.9 61.6 63.2 60.9 56.4
--------------------------------------------------------------------------------
'''

与单层索引的表一样,具备元素值、行索引和列索引三个部分。其中,这里的行索引和列索引都 MultiIndex 类型,只不过索引中的一个元素是元组而不是单层索引中的标量。例如,行索引的第四个元素为(”B”, ”Male”) ,列索引的第二个元素为(”Height”, ”Senior”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
print(df_multi.index.names)
print(df_multi.columns.names)
print(df_multi.index.values) # 是元组, df_multi.columns.values 也一样
print(df_multi.index.get_level_values(0)) # 得到第一层索引的取值
# 任何索引(单层或多层)仅可以访问,不能通过赋值的方式修改索引取值

'''
['School', 'Gender']
['Indicator', 'Grade']
[('A', 'Female') ('A', 'Male') ('B', 'Female') ('B', 'Male')
('C', 'Female') ('C', 'Male') ('D', 'Female') ('D', 'Male')]
Index(['A', 'A', 'B', 'B', 'C', 'C', 'D', 'D'], dtype='object', name='School')
'''

接下来,我们来看一看多级索引中的 loc 索引器如何使用,我们使用原表重新构建一个具有多级行索引的 DataFrame(列仍然是单级索引)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
df_multi = df.set_index(['School', 'Grade'])
print(df_multi.head())

'''
----------------------------------------------------------------------
Name ... Transfer
School Grade ...
Shanghai Jiao Tong University Freshman Gaopeng Yang ... N
Peking University Freshman Changqiang You ... N
Shanghai Jiao Tong University Senior Mei Sun ... N
Fudan University Sophomore Xiaojuan Sun ... N
Sophomore Gaojuan You ... N
----------------------------------------------------------------------
'''

很显然,多级索引应该用元组为下标进行访问,loc 的方法和前面所讲的完全一样,只需要把下标从单级索引的标量替换成多级索引的元组(在索引前最好对 MultiIndex 进行排序以避免性能警告):

1
2
3
4
5
6
7
8
9
df_multi = df_multi.sort_index()
# 复旦的大三学生
print(df_multi.loc[('Fudan University', 'Junior')].head())
# 复旦的大四学生和上交的大一学生
print(df_multi.loc[[('Fudan University', 'Senior'),
('Shanghai Jiao Tong University', 'Freshman')]].head())
# 所有北大和复旦的大二大三学生(多级索引彼此间的交叉组合)
print(df_multi.loc[(['Peking University', 'Fudan University'],
['Sophomore', 'Junior']), :].head())

布尔列表和函数同样是可以使用的,多级索引元组的元素如果是列表,则可以自动交叉组合成新的索引。此外,iloc 的方法同样可以完全照搬。

前面介绍的方法,即使在索引不重复的时候,也只能对元组整体进行切片,而不能对每层进行切片,也不允许将切片和布尔列表混合使用,引入 IndexSlice 对象就能解决这个问题。Slice 对象一共有两种形式,第一种为 loc[idx[*,*]] 型,第二种为 loc[idx[*,*],idx[*,*]] 型。这里我们就不详细展开了,大家遇到具体问题可以上网查找相应的使用方法。

索引的常用方法

  • 交换:由 swaplevel( ) 和 reorder_levels( ) 来实现,前者只能交换两个层,后者可以交换任意层,两者都可以指定交换的轴是哪一个(即行索引或列索引)
  • 删除:若想删除某一层的索引,可以使用 droplevel( ) 方法
  • 修改:通过 rename_axis( ) 可以对索引层的名字进行修改,常用的修改方式是传入字典的映射;通过 rename( ) 可以对索引的值进行修改,如果是多级索引需要指定修改的层号 level;还有一个 map( ) 函数接收多级索引的元组作为参数
  • 设置:使用 set_index( ) 完成索引的设置,这里的主要参数是 append ,表示是否来保留原来的索引,直接把新设定的添加到原索引的内层;reset_index( ) 是 set_index( ) 的逆函数,其主要参数是 drop ,表示是否要把去掉的索引层丢弃,而不是添加到列中

我们构建了一个更为复杂的具有三级索引的 DataFrame,来演示上述方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
np.random.seed(0)
L1,L2,L3 = ['A','B'],['a','b'],['alpha','beta']
mul_index1 = pd.MultiIndex.from_product([L1,L2,L3],
names=('Upper', 'Lower','Extra'))
L4,L5,L6 = ['C','D'],['c','d'],['cat','dog']
mul_index2 = pd.MultiIndex.from_product([L4,L5,L6],
names=('Big', 'Small', 'Other'))
df_ex = pd.DataFrame(np.random.randint(-9,10,(8,8)),
index=mul_index1,
columns=mul_index2)
print(df_ex)
'''
---------------------------------------------------
Big C D
Small c d c d
Other cat dog cat dog cat dog cat dog
Upper Lower Extra
A a alpha 3 6 -9 -6 -6 -2 0 9
beta -5 -3 3 -8 -3 -2 5 8
b alpha -4 4 -1 0 7 -4 6 6
beta -9 9 -6 8 5 -2 -9 -8
B a alpha 0 -9 1 -6 2 9 -7 -9
beta -9 -5 -4 -3 -1 8 6 -5
b alpha 0 1 -8 -8 -2 0 -6 -3
beta 2 5 9 -9 5 -6 3 1
--------------------------------------------------
'''

print(df_ex.swaplevel(0,2,axis=1).head()) # 列索引的第一层和第三层交换
'''
----------------------------------------------------
Other cat dog cat dog cat dog cat dog
Small c c d d c c d d
Big C C C C D D D D
Upper Lower Extra
----------------------------------------------------
'''
df_ex.reorder_levels([2,0,1], axis=0) # 列表数字指代原来索引中的层
df_ex.droplevel(1, axis=1) # 删除某一层,也可以传入列表以删除 [0, 1]

# 对索引层的名字进行修改
print(df_ex.rename_axis(index={'Upper':'Changed_row'},
columns={'Other':'Changed_Col'}).head())
'''
--------------------------------------------------------
Big C D
Small c d c d
Changed_Col cat dog cat dog cat dog cat dog
Changed_row Lower Extra
--------------------------------------------------------
'''
df_ex.rename(columns={'cat':'not_cat'}, level=2) # 修改索引的"cat" -> "not cat"
df_ex.rename(index=lambda x:str.upper(x), level=2) # 也可以传入函数 alpha->ALPHA ...
# 使用map方法实现第二层索引的大写转换(区别在于传入map的参数本身就是个元组)
df_temp = df_ex.copy()
new_idx = df_temp.index.map(lambda x: (x[0], x[1], str.upper(x[2])))
df_temp.index = new_idx

# 设置索引
df_new = pd.DataFrame({'A':list('aacd'), 'B':list('PQRT'), 'C':[1,2,3,4]})
df_new.set_index(['A', 'B'])
my_index = pd.Series(list('WXYZ'), name='D') # 增加一个自定义的新索引
df_new = df_new.set_index(['A', my_index])
df_new.reset_index(['D'], drop=True) # 去掉上面新增的自定义索引

# 索引变形
df_reindex = pd.DataFrame({"Weight":[60,70,80], "Height":[176,180,179]},
index=['1001','1003','1002'])
# 增加一名1004号员工,同时删除身高列,增加性别列
print(df_reindex.reindex(index=['1001','1002','1003','1004'],
columns=['Weight','Gender']))

索引运算

如果存在两张表,它们拥有相同的索引,我们可以通过对索引进行相应的集合(逻辑)操作,来实现对表格数据(索引)的筛选。

1
2
3
4
5
6
7
df_set_1 = pd.DataFrame([[0,1],[1,2],[3,4]], index = pd.Index(['a','b','a'], name='id1'))
df_set_2 = pd.DataFrame([[4,5],[2,6],[7,1]], index = pd.Index(['b','b','c'],name='id2'))
id1, id2 = df_set_1.index.unique(), df_set_2.index.unique()
print(id1.intersection(id2)) # 交集 == d1 & d2:Index(['b'], dtype='object')
print(id1.union(id2)) # 并集 == d1 | d2:Index(['a', 'b', 'c'], dtype='object')
print(id1.difference(id2)) # 差集 == (d1 ^ d2) & d1: Index(['a'], dtype='object')
print(id1.symmetric_difference(id2)) # 对称差集 == d1 ^ d2: Index(['a', 'c'], dtype='object')