选择数据子集
pandas 能够通过切片索引等操作来获取数据子集,他们直接在DataFrame或Series上进行操作。
轴标签
pandas 中选择数据子集是基于轴标签(axis label)的,轴标签可以看作是对行或列的命名。DataFrame 和 ndarray 最大的区别就是他是 label 的。还有一个特殊的就是索引(index),他是 pandas 中的一个非常重要的对象,用来管理行标签,因此索引也可以看作是行标签的指代。
pandas 中轴便签具有许多用途,具有轴标签可以看作是 Pandas 与 NumPy 的核心区别:
- 为对应行、列提供元数据,这对于分析、可视化和交互控制来说非常重要(例如字段标题、主键)
- 启动自动或者显式的数据对齐(根据 axis 的方向来决定列对齐还是行对齐)
- 允许直观的获取和设置数据集的子集,类似字典的操作方式
轴
要注意轴标签和轴是完全不一样的概念,轴(axis)是向量的概念,他在 pandas 的 DataFrame 中指代操作的方向:
axis=0/axis='index'
: 表示沿着 0 轴(竖直方向,也就是行的纬度)操作,即沿着行方向计算。例如多个 DataFrame 沿着行拼接就是增加行数axis=1/axis='columns'
: 表示沿着 1 轴(水平方向,也就是列的纬度)操作,即沿着列方向进行计算,例如多个 DataFrame 沿着列拼接就是行数不变列增加
Note
轴就是分为行、列两个方向,这也是为什么行标签和列标签统称为轴标签
Note
对于行、列维度操作比较绕。需要注意的两个点就是:
- 沿着哪个维度操作哪个维度改变,例如
axis=0
沿着行方向操作,对应的就是行数增加或减少 - 沿着哪个维度聚合该维度会降维,例如
shape(8,4)
沿着axis=0
聚合的结果是shape(4,)
即竖直方向(行方向,维度 0 方向)降维
索引和切片
pandas 中选择数据数据子集最基本的方式就是索引和切片。pandas 支持三种形式的索引:
[]
: 基本的索引切片方式.loc[]
: 基于标签的索引和切片.iloc[]
: 基于整数位置索引和切片(从 0 ~ length-1)
[]: 方式实现
pandas 重构了 []
操作符(通过 __getitem__
方法),支持如下使用方式:
对象类型 | 选择 | 返回值类型 |
---|---|---|
Series | series[index_label] |
标量 |
DataFrame | df[column_label] |
Series |
In [1]: dates = pd.date_range('1/1/2000', periods=8)
In [2]: df = pd.DataFrame(np.random.randn(8,4), index=dates,columns=['A','
...: B','C','D'])
In [3]: df
Out[3]:
A B C D
2000-01-01 -0.846379 0.711637 -1.148989 -1.282312
2000-01-02 1.534979 -1.669740 0.850487 -0.815373
2000-01-03 -0.795900 -1.027772 1.077745 -0.049096
2000-01-04 -2.206887 -0.188843 0.415386 -0.973180
2000-01-05 -0.770502 -0.432951 0.169644 -0.901879
2000-01-06 0.649432 -0.890786 -0.269814 -0.038870
2000-01-07 -1.726979 1.375248 0.227805 0.242211
2000-01-08 0.101482 -1.385984 -0.773157 -1.681190
- 通过列标签来获取一列数据(Series)
In [4]: df['A']
Out[4]:
2000-01-01 -0.846379
2000-01-02 1.534979
2000-01-03 -0.795900
2000-01-04 -2.206887
2000-01-05 -0.770502
2000-01-06 0.649432
2000-01-07 -1.726979
2000-01-08 0.101482
Freq: D, Name: A, dtype: float64
- 返回的 Series 可以通过行标签(索引)来获取标量值
Tips
注意 Series 使用 []
其中一定是列标签,而不能是整数索引。由于默认的索引就是 range(length) 因此可能混淆他们
- 其中可以是列标签列表来选择多列数据
In [6]: df[['B','A']]
Out[6]:
B A
2000-01-01 0.711637 -0.846379
2000-01-02 -1.669740 1.534979
2000-01-03 -1.027772 -0.795900
2000-01-04 -0.188843 -2.206887
2000-01-05 -0.432951 -0.770502
2000-01-06 -0.890786 0.649432
2000-01-07 1.375248 -1.726979
2000-01-08 -1.385984 0.101482
.loc[]: 基于标签索引和切片
.loc[rows, columns]
用于基于标签的索引和切片,它可以包含行、列两个方向上的索引切片值,每个值可能的取值包括:
- 单个标签,例如 5、'a' 等(这里的 5 是迷惑行为,它一定要被解释为标签)
- 标签的列表,例如
['a', 'b', 'c']
- 切片
'a':'f'
,需要注意他和 python 不太一样它包含 start 和 stop - 布尔序列,注意必须是布尔序列,而不能是布尔 Series,还有任何 NA 值都被视为 False
- 可调用对象
In [41]: dfl = pd.DataFrame(np.random.randn(5, 4),
....: columns=list('ABCD'),
....: index=pd.date_range('20130101', periods=5))
....:
In [42]: dfl
Out[42]:
A B C D
2013-01-01 -1.690011 0.319664 -0.522094 -0.888786
2013-01-02 -0.181660 1.114312 0.812464 -1.945866
2013-01-03 -0.293585 -0.779521 -0.305282 -0.870298
2013-01-04 -0.841481 0.217486 1.163833 -0.875083
2013-01-05 -0.071881 -0.464978 -1.323767 0.926922
# TypeError 因为必须使用标签,而不能使用整数索引
In [43]: dfl.loc[2:3]
# 基于标签索引,第一个是针对于行的,因此切片的结果就是筛选行
# 注意它包括 start 和 stop
In [44]: dfl.loc['20130102':'20130104']
Out[44]:
A B C D
2013-01-02 -0.181660 1.114312 0.812464 -1.945866
2013-01-03 -0.293585 -0.779521 -0.305282 -0.870298
2013-01-04 -0.841481 0.217486 1.163833 -0.875083
# 带有 row 和 column,来获取子集
In [45]: dfl.loc['20130103':, ['A','B']]
Out[45]:
A B
2013-01-03 -0.293585 -0.779521
2013-01-04 -0.841481 0.217486
2013-01-05 -0.071881 -0.464978
# 如果只想要 column , row 是不能够省略的它必须是 :, column
In [46]: dfl.loc[:,'A':'C']
Out[46]:
A B C
2013-01-01 -1.690011 0.319664 -0.522094
2013-01-02 -0.181660 1.114312 0.812464
2013-01-03 -0.293585 -0.779521 -0.305282
2013-01-04 -0.841481 0.217486 1.163833
2013-01-05 -0.071881 -0.464978 -1.323767
.iloc[]: 基于整数位置索引和切片
.iloc[row, column]
和 .loc[row, column]
几乎一样,唯一的区别就是它接受基于整数 0 开始的位置来作为索引和切片。还有一个重要的区别就是它包含 start 而不包含 stop,这和 python 的行为一致。
# 1:2 只包含 1
In [47]: dfl.iloc[:, 1:2]
Out[47]:
B
2013-01-01 0.319664
2013-01-02 1.114312
2013-01-03 -0.779521
2013-01-04 0.217486
2013-01-05 -0.464978
结合整数位置和标签的索引和切片
有时候我们希望对列基于标签进行索引,而希望对于行基于整数位置索引(很常见的使用场景),有两种方法可以实现他们:
- 通过
df[columns].iloc[range_index]
即我们先用[]
来获取需要的列然后在使用iloc
筛选行 - 通过
df.index
来将整数索引转为行标签,或df.columns
来将整数索引转换为列标签 - 使用
df.columns|df.index.get_loc(column: str|int)
和df.columns|df.index.get_indexer(columns|list[str|int])
来获取指定的轴标签的整数索引
In [48]: dfl.index[0:3]
Out[48]: DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03'], dtype='datetime64[ns]', freq='D')
In [49]: dfl.index[0]
Out[49]: Timestamp('2013-01-01 00:00:00')
In [50]: dfl.index[[0,3]]
Out[50]: DatetimeIndex(['2013-01-01', '2013-01-04'], dtype='datetime64[ns]', freq=None)
# 说白了 dfl.index 本身就是特殊的数组来容纳行标签,能够使用整数位置索引和切片获取行标签
In [51]: dfl.index
Out[52]:
DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
'2013-01-05'],
dtype='datetime64[ns]', freq='D')
对于列标签则是基于 df.columns
这个特殊的数组:
In [53]: dfl.columns
Out[53]: Index(['A', 'B', 'C', 'D'], dtype='object')
# 接受序列返回序列
In [54]: dfl.columns.get_indexer(['A','C'])
Out[54]: array([0, 2])
# 接受单值返回单值
In [50]: dfl.columns.get_loc('A')
Out[50]: 0
Tips
df.index
和 df.columns
返回的都是 Index 对象,因此他们可以都具有上面的性质,不过通常列数量少更多的是通过 get_indexer
来筛选。而行数量多更多的是使用 index[:]
切片语法来获取
基于布尔序列来筛选
可以通过布尔序列来筛选数据,其中需要注意:
- 对于
[]
来说只能筛选行,因此必须与行长度相同 - 对于
.iloc[row, column]
和loc[row, column]
来说,第一个参数是筛选行,第二个参数是筛选列,他们必须与对应筛选的轴长度相同
Tips
有很多方法能够返回布尔序列,但是他们可以会返回 na,这通常可以通过 fillna(False)
来实现不选择。
基于布尔序列筛选使用最多的就是对行进行过滤,而行通常非常多你无法实现手动的编写布尔序列,因此通常的做法就是对某个列执行向量运算,例如 df['A'] > 0
这样的形式。布尔序列之间能够通过 | & ~
来进行布尔运算。
In [64]: df[df['A'] > 0]
Out[64]:
A B C D
2000-01-02 1.534979 -1.669740 0.850487 -0.815373
2000-01-06 0.649432 -0.890786 -0.269814 -0.038870
2000-01-08 0.101482 -1.385984 -0.773157 -1.681190
需要注意的是 Series 布尔序列不能直接被用于 .iloc
和 .loc
:
In [65]: df.iloc[df['A']>0, [True,True,False,False]] # ValueError
# 他们必须被转为布尔序列, 可以使用 .values 或者 .to_list()
In [66]: df.iloc[(df['A']>0).values, [True,True,False,False]]
Out[66]:
A B
2000-01-02 1.534979 -1.669740
2000-01-06 0.649432 -0.890786
2000-01-08 0.101482 -1.385984
Tips
因此在筛选行的场景下,[]
是最优先的选择
生成布尔序列: isna()/isin()
这两个函数是非常重要的生成布尔序列的方式:
isna()
: 所有 None 为 True 其他为 False,通过取反(~
)是提出 None 值的最好方式isin([values])
: 判断是否包含其中,是很重要的筛选方式
In [175]: s = pd.Series(np.arange(5), index=np.arange(5)[::-1], dtype='int64')
In [176]: s
Out[176]:
4 0
3 1
2 2
1 3
0 4
dtype: int64
In [177]: s.isin([2, 4, 6])
Out[177]:
4 False
3 False
2 True
1 False
0 True
dtype: bool
In [178]: s[s.isin([2, 4, 6])]
Out[178]:
2 2
0 4
dtype: int64
通用的布尔序列生成方式
如果想要对数据进行非常详细的筛选,可以是用 map、生成式等方式来构建布尔序列,需要注意尽管他们非常强大但是速度会很慢:
In [164]: df2 = pd.DataFrame({'a': ['one', 'one', 'two', 'three', 'two', 'one', 'six'],
.....: 'b': ['x', 'y', 'y', 'x', 'y', 'x', 'x'],
.....: 'c': np.random.randn(7)})
.....:
# map 方式
In [165]: criterion = df2['a'].map(lambda x: x.startswith('t'))
In [166]: df2[criterion]
Out[166]:
a b c
2 two y 0.041290
3 three x 0.361719
4 two y -0.238075
# 序列
In [167]: df2[[x.startswith('t') for x in df2['a']]]
Out[167]:
a b c
2 two y 0.041290
3 three x 0.361719
4 two y -0.238075
# Multiple criteria
In [168]: df2[criterion & (df2['b'] == 'x')]
Out[168]:
a b c
3 three x 0.361719
基于 callable 来筛选
都可以接受 callable 对象来作为索引器,他接受 DataFrame/Series 本身返回值必须是合法的轴标签或轴标签序列:
In [8]: df[lambda df: df.columns[1]]
Out[8]:
2000-01-01 0.711637
2000-01-02 -1.669740
2000-01-03 -1.027772
2000-01-04 -0.188843
2000-01-05 -0.432951
2000-01-06 -0.890786
2000-01-07 1.375248
2000-01-08 -1.385984
Freq: D, Name: B, dtype: float64
# 比较奇特的方式
In [68]: df.loc[lambda df: df['A'] > 0, :]
Out[68]:
A B C D
2000-01-02 1.534979 -1.669740 0.850487 -0.815373
2000-01-06 0.649432 -0.890786 -0.269814 -0.038870
2000-01-08 0.101482 -1.385984 -0.773157 -1.681190
In [69]: df.iloc[:, lambda df: [0, 1]]
Out[69]:
A B
2000-01-01 -0.846379 0.711637
2000-01-02 1.534979 -1.669740
2000-01-03 -0.795900 -1.027772
2000-01-04 -2.206887 -0.188843
2000-01-05 -0.770502 -0.432951
2000-01-06 0.649432 -0.890786
2000-01-07 -1.726979 1.375248
2000-01-08 0.101482 -1.385984
随机采样返回: sample()
如果想要随机采样特定数量的行或列,可以使用 simple 方法来实现:
In [71]: df['A'].sample(n=3)
Out[71]:
2000-01-08 0.101482
2000-01-05 -0.770502
2000-01-03 -0.795900
Name: A, dtype: float64
# 根据 index 来采样的
In [72]: df.sample(n=3)
Out[72]:
A B C D
2000-01-06 0.649432 -0.890786 -0.269814 -0.038870
2000-01-03 -0.795900 -1.027772 1.077745 -0.049096
2000-01-04 -2.206887 -0.188843 0.415386 -0.973180
Note
默认采样的权重是相同的,可以根据 weights 参数来设置不同的权重。还有一些更高级的功能可以查看函数手册
通过表达式字符串来筛选: query()
DataFrame 对象提供了 query()
方法,它接受一个字符串,该字符串应当是一个合法的 python 表达式来执行筛选:
In [226]: n = 10
In [227]: df = pd.DataFrame(np.random.rand(n, 3), columns=list('abc'))
In [228]: df
Out[228]:
a b c
0 0.438921 0.118680 0.863670
1 0.138138 0.577363 0.686602
2 0.595307 0.564592 0.520630
3 0.913052 0.926075 0.616184
4 0.078718 0.854477 0.898725
5 0.076404 0.523211 0.591538
6 0.792342 0.216974 0.564056
7 0.397890 0.454131 0.915716
8 0.074315 0.437913 0.019794
9 0.559209 0.502065 0.026437
# pure python
In [229]: df[(df['a'] < df['b']) & (df['b'] < df['c'])]
Out[229]:
a b c
1 0.138138 0.577363 0.686602
4 0.078718 0.854477 0.898725
5 0.076404 0.523211 0.591538
7 0.397890 0.454131 0.915716
# query
In [230]: df.query('(a < b) & (b < c)')
Out[230]:
a b c
1 0.138138 0.577363 0.686602
4 0.078718 0.854477 0.898725
5 0.076404 0.523211 0.591538
7 0.397890 0.454131 0.915716
从上面的例子可以看到 query 的强大。他允许是合法的 python 表达式因此甚至可以是函数字符串:
query 中有一些比较特殊的机制,主要是对外部变量的使用:
- 通过 ` 包围的形式来引入不合法的标识符,主要是出现类似
a a
以及Area (cm^2)
这类轴标签时使用 - 通过
@attr
来引用外部变量的值
总结
对于数据子集的选择推荐的方式:
- 对于大范围的筛选推荐直接使用 query
- 对于行筛选直接使用
df[布尔表达式]
,如果复杂直接使用 map 来构建布尔表达式 .loc
和.iloc
更多的时候是为了避免歧义([]
被赋予的含义太多了),这在修改特定值的时候尤其必要