Skip to content

选择数据子集

pandas 能够通过切片索引等操作来获取数据子集,他们直接在DataFrameSeries上进行操作。

轴标签

pandas 中选择数据子集是基于轴标签(axis label)的,轴标签可以看作是对行或列的命名。DataFrame 和 ndarray 最大的区别就是他是 label 的。还有一个特殊的就是索引(index),他是 pandas 中的一个非常重要的对象,用来管理行标签,因此索引也可以看作是行标签的指代。

pandas 中轴便签具有许多用途,具有轴标签可以看作是 Pandas 与 NumPy 的核心区别:

  1. 为对应行、列提供元数据,这对于分析、可视化和交互控制来说非常重要(例如字段标题、主键)
  2. 启动自动或者显式的数据对齐(根据 axis 的方向来决定列对齐还是行对齐)
  3. 允许直观的获取和设置数据集的子集,类似字典的操作方式

要注意轴标签和轴是完全不一样的概念,轴(axis)是向量的概念,他在 pandas 的 DataFrame 中指代操作的方向:

  • axis=0/axis='index': 表示沿着 0 轴(竖直方向,也就是行的纬度)操作,即沿着行方向计算。例如多个 DataFrame 沿着行拼接就是增加行数
  • axis=1/axis='columns': 表示沿着 1 轴(水平方向,也就是列的纬度)操作,即沿着列方向进行计算,例如多个 DataFrame 沿着列拼接就是行数不变列增加

Note

轴就是分为行、列两个方向,这也是为什么行标签和列标签统称为轴标签

Note

对于行、列维度操作比较绕。需要注意的两个点就是:

  1. 沿着哪个维度操作哪个维度改变,例如 axis=0 沿着行方向操作,对应的就是行数增加或减少
  2. 沿着哪个维度聚合该维度会降维,例如 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
Python
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)
Python
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 可以通过行标签(索引)来获取标量值
Python
In [5]: df['A'][dates[4]]
Out[5]: -0.7705017386586456

Tips

注意 Series 使用 [] 其中一定是列标签,而不能是整数索引。由于默认的索引就是 range(length) 因此可能混淆他们

  • 其中可以是列标签列表来选择多列数据
Python
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
  • 可调用对象
Python
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 的行为一致

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

结合整数位置和标签的索引和切片

有时候我们希望对列基于标签进行索引,而希望对于行基于整数位置索引(很常见的使用场景),有两种方法可以实现他们:

  1. 通过 df[columns].iloc[range_index] 即我们先用 [] 来获取需要的列然后在使用 iloc 筛选行
  2. 通过 df.index 来将整数索引转为行标签,或 df.columns 来将整数索引转换为列标签
  3. 使用 df.columns|df.index.get_loc(column: str|int)df.columns|df.index.get_indexer(columns|list[str|int]) 来获取指定的轴标签的整数索引
Python
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 这个特殊的数组:

Python
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.indexdf.columns 返回的都是 Index 对象,因此他们可以都具有上面的性质,不过通常列数量少更多的是通过 get_indexer 来筛选。而行数量多更多的是使用 index[:] 切片语法来获取

基于布尔序列来筛选

可以通过布尔序列来筛选数据,其中需要注意:

  • 对于 [] 来说只能筛选行,因此必须与行长度相同
  • 对于 .iloc[row, column]loc[row, column] 来说,第一个参数是筛选行,第二个参数是筛选列,他们必须与对应筛选的轴长度相同

Tips

有很多方法能够返回布尔序列,但是他们可以会返回 na,这通常可以通过 fillna(False) 来实现不选择。

基于布尔序列筛选使用最多的就是对行进行过滤,而行通常非常多你无法实现手动的编写布尔序列,因此通常的做法就是对某个列执行向量运算,例如 df['A'] > 0 这样的形式。布尔序列之间能够通过 | & ~ 来进行布尔运算。

Python
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:

Python
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]): 判断是否包含其中,是很重要的筛选方式
Python
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、生成式等方式来构建布尔序列,需要注意尽管他们非常强大但是速度会很慢:

Python
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 本身返回值必须是合法的轴标签或轴标签序列:

Python
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 方法来实现:

Python
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 表达式来执行筛选:

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 表达式因此甚至可以是函数字符串:

Python
df.query("year==2010 & id.str.startswith('AP') & ~source.isna()")

query 中有一些比较特殊的机制,主要是对外部变量的使用:

  • 通过 ` 包围的形式来引入不合法的标识符,主要是出现类似 a a 以及 Area (cm^2) 这类轴标签时使用
  • 通过 @attr 来引用外部变量的值

总结

对于数据子集的选择推荐的方式:

  1. 对于大范围的筛选推荐直接使用 query
  2. 对于行筛选直接使用 df[布尔表达式],如果复杂直接使用 map 来构建布尔表达式
  3. .loc.iloc 更多的时候是为了避免歧义([]被赋予的含义太多了),这在修改特定值的时候尤其必要

参考