# 多层级索引和高级索引

本章节包含了使用多层级索引 以及 其他高级索引特性

请参阅 索引与选择数据来获得更多的通用索引方面的帮助文档

警告

基于实际的使用场景不同,返回的内容也会不尽相同(返回一个数据的副本,或者返回数据的引用)。有时,这种情况被称作连锁赋值,但是这种情况应当被尽力避免。参见返回视图or返回副本

参见 cookbook,获取更多高级的使用技巧。

# 分层索引(多层级索引)

分层/多级索引在处理复杂的数据分析和数据操作方面为开发者奠定了基础,尤其是在处理高纬度数据处理上。本质上,它使您能够在较低维度的数据结构(如 Series(1d)和DataFrame (2d))中存储和操作任意维数的数据。

在本节中,我们将展示“层次”索引的确切含义,以及它如何与上面和前面部分描述的所有panda索引功能集成。稍后,在讨论分组数据透视与重塑性数据时,我们将展示一些重要的应用程序,以说明它如何帮助构建分析数据的结构。

请参阅cookbook,查看一些高级策略.

在0.24.0版本中的改变:MultIndex.labels被更名为MultiIndex.codes (opens new window) ,同时 MultiIndex.set_labels 更名为 MultiIndex.set_codes (opens new window).

# 创建多级索引和分层索引对象

MultiIndex (opens new window) 对象是标准索引对象 (opens new window) 的分层模拟,标准索引对象通常将axis标签存储在panda对象中。您可以将MultiIndex看作一个元组数组,其中每个元组都是惟一的。可以从数组列表(使用 MultiIndex.from_arrays() (opens new window))、元组数组(使用MultiIndex.from_tuples() (opens new window))或交叉迭代器集(使用MultiIndex.from_product() (opens new window))或者将一个 DataFrame (opens new window)(使用using MultiIndex.from_frame() (opens new window))创建多索引。当传递一个元组列表时,索引构造函数将尝试返回一个MultiIndex。下面的示例演示了初始化多索引的不同方法。

In [1]: arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
   ...:           ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
   ...: 

In [2]: tuples = list(zip(*arrays))

In [3]: tuples
Out[3]: 
[('bar', 'one'),
 ('bar', 'two'),
 ('baz', 'one'),
 ('baz', 'two'),
 ('foo', 'one'),
 ('foo', 'two'),
 ('qux', 'one'),
 ('qux', 'two')]

In [4]: index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])

In [5]: index
Out[5]: 
MultiIndex([('bar', 'one'),
            ('bar', 'two'),
            ('baz', 'one'),
            ('baz', 'two'),
            ('foo', 'one'),
            ('foo', 'two'),
            ('qux', 'one'),
            ('qux', 'two')],
           names=['first', 'second'])

In [6]: s = pd.Series(np.random.randn(8), index=index)

In [7]: s
Out[7]: 
first  second
bar    one       0.469112
       two      -0.282863
baz    one      -1.509059
       two      -1.135632
foo    one       1.212112
       two      -0.173215
qux    one       0.119209
       two      -1.044236
dtype: float64

当您想要在两个迭代器中对每个元素进行配对时,可以更容易地使用 MultiIndex.from_product() (opens new window)方法:

In [8]: iterables = [['bar', 'baz', 'foo', 'qux'], ['one', 'two']]

In [9]: pd.MultiIndex.from_product(iterables, names=['first', 'second'])
Out[9]: 
MultiIndex([('bar', 'one'),
            ('bar', 'two'),
            ('baz', 'one'),
            ('baz', 'two'),
            ('foo', 'one'),
            ('foo', 'two'),
            ('qux', 'one'),
            ('qux', 'two')],
           names=['first', 'second'])

还可以使用 MultiIndex.from_frame() (opens new window)方法直接将一个DataFrame对象构造一个多索引。这是MultiIndex.to_frame() (opens new window)的一个补充方法。

0.24.0版本新增。

In [10]: df = pd.DataFrame([['bar', 'one'], ['bar', 'two'],
   ....:                    ['foo', 'one'], ['foo', 'two']],
   ....:                   columns=['first', 'second'])
   ....: 

In [11]: pd.MultiIndex.from_frame(df)
Out[11]: 
MultiIndex([('bar', 'one'),
            ('bar', 'two'),
            ('foo', 'one'),
            ('foo', 'two')],
           names=['first', 'second'])

为了方便,您可以将数组列表直接传递到SeriesDataFrame中,从而自动构造一个MultiIndex:

In [12]: arrays = [np.array(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux']),
   ....:           np.array(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'])]
   ....: 

In [13]: s = pd.Series(np.random.randn(8), index=arrays)

In [14]: s
Out[14]: 
bar  one   -0.861849
     two   -2.104569
baz  one   -0.494929
     two    1.071804
foo  one    0.721555
     two   -0.706771
qux  one   -1.039575
     two    0.271860
dtype: float64

In [15]: df = pd.DataFrame(np.random.randn(8, 4), index=arrays)

In [16]: df
Out[16]: 
                0         1         2         3
bar one -0.424972  0.567020  0.276232 -1.087401
    two -0.673690  0.113648 -1.478427  0.524988
baz one  0.404705  0.577046 -1.715002 -1.039268
    two -0.370647 -1.157892 -1.344312  0.844885
foo one  1.075770 -0.109050  1.643563 -1.469388
    two  0.357021 -0.674600 -1.776904 -0.968914
qux one -1.294524  0.413738  0.276662 -0.472035
    two -0.013960 -0.362543 -0.006154 -0.923061

MultiIndex构造函数都接受names参数,该参数存储级别本身的字符串名称。如果没有提供name属性,将分配None:

In [17]: df.index.names
Out[17]: FrozenList([None, None])

此索引可以支持panda对象的任何轴,索引的级别由开发者决定:

In [18]: df = pd.DataFrame(np.random.randn(3, 8), index=['A', 'B', 'C'], columns=index)

In [19]: df
Out[19]: 
first        bar                 baz                 foo                 qux          
second       one       two       one       two       one       two       one       two
A       0.895717  0.805244 -1.206412  2.565646  1.431256  1.340309 -1.170299 -0.226169
B       0.410835  0.813850  0.132003 -0.827317 -0.076467 -1.187678  1.130127 -1.436737
C      -1.413681  1.607920  1.024180  0.569605  0.875906 -2.211372  0.974466 -2.006747

In [20]: pd.DataFrame(np.random.randn(6, 6), index=index[:6], columns=index[:6])
Out[20]: 
first              bar                 baz                 foo          
second             one       two       one       two       one       two
first second                                                            
bar   one    -0.410001 -0.078638  0.545952 -1.219217 -1.226825  0.769804
      two    -1.281247 -0.727707 -0.121306 -0.097883  0.695775  0.341734
baz   one     0.959726 -1.110336 -0.619976  0.149748 -0.732339  0.687738
      two     0.176444  0.403310 -0.154951  0.301624 -2.179861 -1.369849
foo   one    -0.954208  1.462696 -1.743161 -0.826591 -0.345352  1.314232
      two     0.690579  0.995761  2.396780  0.014871  3.357427 -0.317441

我们已经“稀疏化”了更高级别的索引,使控制台的输出更容易显示。注意,可以使用pandas.set_options()中的multi_sparse选项控制索引的显示方式:

In [21]: with pd.option_context('display.multi_sparse', False):
   ....:     df
   ....:

值得记住的是,没有什么可以阻止您使用元组作为轴上的原子标签:

In [22]: pd.Series(np.random.randn(8), index=tuples)
Out[22]: 
(bar, one)   -1.236269
(bar, two)    0.896171
(baz, one)   -0.487602
(baz, two)   -0.082240
(foo, one)   -2.182937
(foo, two)    0.380396
(qux, one)    0.084844
(qux, two)    0.432390
dtype: float64

MultiIndex之所以重要,是因为它允许您进行分组、选择和重新构造操作,我们将在下面的文档和后续部分中进行描述。正如您将在后面的部分中看到的,您可以发现自己使用分层索引的数据,而不需要显式地创建一个MultiIndex。然而,当从文件中加载数据时,您可能希望在准备数据集时生成自己的MultiIndex

# 重构层次标签

get_level_values() (opens new window)方法将返回特定级别每个位置的标签向量:

In [23]: index.get_level_values(0)
Out[23]: Index(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

In [24]: index.get_level_values('second')
Out[24]: Index(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], dtype='object', name='second')

# 基本索引轴上的多索引

层次索引的一个重要特性是,您可以通过partial标签来选择数据,该标签标识数据中的子组。局部 选择“降低”层次索引的级别,其结果完全类似于在常规数据Dataframe中选择列:

In [25]: df['bar']
Out[25]: 
second       one       two
A       0.895717  0.805244
B       0.410835  0.813850
C      -1.413681  1.607920

In [26]: df['bar', 'one']
Out[26]: 
A    0.895717
B    0.410835
C   -1.413681
Name: (bar, one), dtype: float64

In [27]: df['bar']['one']
Out[27]: 
A    0.895717
B    0.410835
C   -1.413681
Name: one, dtype: float64

In [28]: s['qux']
Out[28]: 
one   -1.039575
two    0.271860
dtype: float64

有关如何在更深层次上进行选择,请参见具有层次索引的横切

# 定义不同层次索引

MultiIndex (opens new window) 保存了所有被定义的索引层级,即使它们实际上没有被使用。在切割索引时,您可能会注意到这一点。例如:

In [29]: df.columns.levels  # original MultiIndex
Out[29]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

In [30]: df[['foo','qux']].columns.levels  # sliced
Out[30]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

这样做是为了避免重新计算级别,从而使切片具有很高的性能。如果只想查看使用的级别,可以使用remove_unused_levels() (opens new window)方法。

In [31]: df[['foo', 'qux']].columns.to_numpy()
Out[31]: 
array([('foo', 'one'), ('foo', 'two'), ('qux', 'one'), ('qux', 'two')],
      dtype=object)

# for a specific level
In [32]: df[['foo', 'qux']].columns.get_level_values(0)
Out[32]: Index(['foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

若要仅使用已使用的级别来重构MultiIndex,可以使用remove_unused_levels() (opens new window)方法。

0.20.0.中更新

In [33]: new_mi = df[['foo', 'qux']].columns.remove_unused_levels()

In [34]: new_mi.levels
Out[34]: FrozenList([['foo', 'qux'], ['one', 'two']])

# 数据对齐和使用 reindex

在轴上具有MultiIndex的不同索引对象之间的操作将如您所期望的那样工作;数据对齐的工作原理与元组索引相同:

In [35]: s + s[:-2]
Out[35]: 
bar  one   -1.723698
     two   -4.209138
baz  one   -0.989859
     two    2.143608
foo  one    1.443110
     two   -1.413542
qux  one         NaN
     two         NaN
dtype: float64

In [36]: s + s[::2]
Out[36]: 
bar  one   -1.723698
     two         NaN
baz  one   -0.989859
     two         NaN
foo  one    1.443110
     two         NaN
qux  one   -2.079150
     two         NaN
dtype: float64

Series/DataFrames对象的 reindex() (opens new window) 方法可以调用另一个MultiIndex ,甚至一个列表或数组元组:

In [37]: s.reindex(index[:3])
Out[37]: 
first  second
bar    one      -0.861849
       two      -2.104569
baz    one      -0.494929
dtype: float64

In [38]: s.reindex([('foo', 'two'), ('bar', 'one'), ('qux', 'one'), ('baz', 'one')])
Out[38]: 
foo  two   -0.706771
bar  one   -0.861849
qux  one   -1.039575
baz  one   -0.494929
dtype: float64

# 具有层次索引的高级索引方法

语法上,使用.loc方法,在高级索引中加入 MultiIndex(多层索引)是有一些挑战的,但是我们一直在尽己所能地去实现这个功能。简单来说,多层索引的索引键(keys)来自元组的格式。例如,下列代码将会按照你的期望工作:

In [39]: df = df.T

In [40]: df
Out[40]: 
                     A         B         C
first second                              
bar   one     0.895717  0.410835 -1.413681
      two     0.805244  0.813850  1.607920
baz   one    -1.206412  0.132003  1.024180
      two     2.565646 -0.827317  0.569605
foo   one     1.431256 -0.076467  0.875906
      two     1.340309 -1.187678 -2.211372
qux   one    -1.170299  1.130127  0.974466
      two    -0.226169 -1.436737 -2.006747

In [41]: df.loc[('bar', 'two')]
Out[41]: 
A    0.805244
B    0.813850
C    1.607920
Name: (bar, two), dtype: float64

注意 df.loc['bar', 'two']也将会在这个用例中正常工作,但是这种便捷的简写方法总的来说是容易产生歧义的。

如果你也希望使用 .loc对某个特定的列进行索引,你需要使用如下的元组样式:

In [42]: df.loc[('bar', 'two'), 'A']
Out[42]: 0.8052440253863785

你可以只输入元组的第一个元素,而不需要写出所有的多级索引的每一个层级。例如,你可以使用“局部”索引,来获得所有在第一层为bar的元素,参见下例:

df.loc[‘bar’]

这种方式是对于更为冗长的方式df.loc[('bar',),]的一个简写(在本例中,等同于df.loc['bar',]

您也可以类似地使用“局部”切片。

In [43]: df.loc['baz':'foo']
Out[43]: 
                     A         B         C
first second                              
baz   one    -1.206412  0.132003  1.024180
      two     2.565646 -0.827317  0.569605
foo   one     1.431256 -0.076467  0.875906
      two     1.340309 -1.187678 -2.211372

您可以通过使用一个元组的切片,提供一个值的范围(a ‘range’ of values),来进行切片

In [44]: df.loc[('baz', 'two'):('qux', 'one')]
Out[44]: 
                     A         B         C
first second                              
baz   two     2.565646 -0.827317  0.569605
foo   one     1.431256 -0.076467  0.875906
      two     1.340309 -1.187678 -2.211372
qux   one    -1.170299  1.130127  0.974466

In [45]: df.loc[('baz', 'two'):'foo']
Out[45]: 
                     A         B         C
first second                              
baz   two     2.565646 -0.827317  0.569605
foo   one     1.431256 -0.076467  0.875906
      two     1.340309 -1.187678 -2.211372

类似于重命名索引(reindexing),您可以通过输入一个标签的元组来实现:

In [46]: df.loc[[('bar', 'two'), ('qux', 'one')]]
Out[46]: 
                     A         B         C
first second                              
bar   two     0.805244  0.813850  1.607920
qux   one    -1.170299  1.130127  0.974466

小技巧

在pandas中,元组和列表,在索引时,是有区别的。一个元组会被识别为一个多层级的索引值(key),而列表被用于表明多个不同的索引值(several keys)。换句话说,元组是按照横向展开的,即水平层级(trasvering levels),而列表是纵向的,即扫描层级(scanning levels)。

注意,一个元组构成的列表提供的是完整的多级索引,而一个列表构成的元组提供的是同一个级别中的多个值:

In [47]: s = pd.Series([1, 2, 3, 4, 5, 6],
   ....:               index=pd.MultiIndex.from_product([["A", "B"], ["c", "d", "e"]]))
   ....: 

In [48]: s.loc[[("A", "c"), ("B", "d")]]  # list of tuples
Out[48]: 
A  c    1
B  d    5
dtype: int64

In [49]: s.loc[(["A", "B"], ["c", "d"])]  # tuple of lists
Out[49]: 
A  c    1
   d    2
B  c    4
   d    5
dtype: int64

# 使用切片器

你可以使用多级索引器来切片一个``多级索引

你可以提供任意的选择器,就仿佛你按照标签索引一样,参见按照标签索引, 包含切片,标签构成的列表,标签,和布尔值索引器。

你可以使用slice(None)来选择所有的该级别的内容。你不需要指明所有的深层级别,他们将按照slice(None)的方式来做默认推测。

一如既往,切片器的两侧都会被包含进来,因为这是按照标签索引的方式进行的。

警告

你需要在.loc中声明所有的维度,这意味着同时包含行索引以及列索引。在一些情况下,索引器中的数据有可能会被错误地识别为在两个维度同时进行索引,而不是只对行进行多层级索引。

建议使用下列的方式:

df.loc[(slice('A1', 'A3'), ...), :]             # noqa: E999

不建议使用下列的方式:

df.loc[(slice('A1', 'A3'), ...)]                # noqa: E999
In [50]: def mklbl(prefix, n):
   ....:     return ["%s%s" % (prefix, i) for i in range(n)]
   ....: 

In [51]: miindex = pd.MultiIndex.from_product([mklbl('A', 4),
   ....:                                       mklbl('B', 2),
   ....:                                       mklbl('C', 4),
   ....:                                       mklbl('D', 2)])
   ....: 

In [52]: micolumns = pd.MultiIndex.from_tuples([('a', 'foo'), ('a', 'bar'),
   ....:                                        ('b', 'foo'), ('b', 'bah')],
   ....:                                       names=['lvl0', 'lvl1'])
   ....: 

In [53]: dfmi = pd.DataFrame(np.arange(len(miindex) * len(micolumns))
   ....:                       .reshape((len(miindex), len(micolumns))),
   ....:                     index=miindex,
   ....:                     columns=micolumns).sort_index().sort_index(axis=1)
   ....: 

In [54]: dfmi
Out[54]: 
lvl0           a         b     
lvl1         bar  foo  bah  foo
A0 B0 C0 D0    1    0    3    2
         D1    5    4    7    6
      C1 D0    9    8   11   10
         D1   13   12   15   14
      C2 D0   17   16   19   18
...          ...  ...  ...  ...
A3 B1 C1 D1  237  236  239  238
      C2 D0  241  240  243  242
         D1  245  244  247  246
      C3 D0  249  248  251  250
         D1  253  252  255  254

[64 rows x 4 columns]

使用切片,列表和标签来进行简单的多层级切片

In [55]: dfmi.loc[(slice('A1', 'A3'), slice(None), ['C1', 'C3']), :]
Out[55]: 
lvl0           a         b     
lvl1         bar  foo  bah  foo
A1 B0 C1 D0   73   72   75   74
         D1   77   76   79   78
      C3 D0   89   88   91   90
         D1   93   92   95   94
   B1 C1 D0  105  104  107  106
...          ...  ...  ...  ...
A3 B0 C3 D1  221  220  223  222
   B1 C1 D0  233  232  235  234
         D1  237  236  239  238
      C3 D0  249  248  251  250
         D1  253  252  255  254

[24 rows x 4 columns]

你可以使用 pandas.IndexSlice (opens new window),即使用:,一个更为符合习惯的语法,而不是使用slice(None)。

In [56]: idx = pd.IndexSlice

In [57]: dfmi.loc[idx[:, :, ['C1', 'C3']], idx[:, 'foo']]
Out[57]: 
lvl0           a    b
lvl1         foo  foo
A0 B0 C1 D0    8   10
         D1   12   14
      C3 D0   24   26
         D1   28   30
   B1 C1 D0   40   42
...          ...  ...
A3 B0 C3 D1  220  222
   B1 C1 D0  232  234
         D1  236  238
      C3 D0  248  250
         D1  252  254

[32 rows x 2 columns]

您可以使用这种方法在两个维度上同时实现非常复杂的选择。

In [58]: dfmi.loc['A1', (slice(None), 'foo')]
Out[58]: 
lvl0        a    b
lvl1      foo  foo
B0 C0 D0   64   66
      D1   68   70
   C1 D0   72   74
      D1   76   78
   C2 D0   80   82
...       ...  ...
B1 C1 D1  108  110
   C2 D0  112  114
      D1  116  118
   C3 D0  120  122
      D1  124  126

[16 rows x 2 columns]

In [59]: dfmi.loc[idx[:, :, ['C1', 'C3']], idx[:, 'foo']]
Out[59]: 
lvl0           a    b
lvl1         foo  foo
A0 B0 C1 D0    8   10
         D1   12   14
      C3 D0   24   26
         D1   28   30
   B1 C1 D0   40   42
...          ...  ...
A3 B0 C3 D1  220  222
   B1 C1 D0  232  234
         D1  236  238
      C3 D0  248  250
         D1  252  254

[32 rows x 2 columns]

使用布尔索引器,您可以对数值进行选择。

In [60]: mask = dfmi[('a', 'foo')] > 200

In [61]: dfmi.loc[idx[mask, :, ['C1', 'C3']], idx[:, 'foo']]
Out[61]: 
lvl0           a    b
lvl1         foo  foo
A3 B0 C1 D1  204  206
      C3 D0  216  218
         D1  220  222
   B1 C1 D0  232  234
         D1  236  238
      C3 D0  248  250
         D1  252  254

您也可以使用.loc来明确您所希望的维度(axis),从而只在一个维度上来进行切片。

In [62]: dfmi.loc(axis=0)[:, :, ['C1', 'C3']]
Out[62]: 
lvl0           a         b     
lvl1         bar  foo  bah  foo
A0 B0 C1 D0    9    8   11   10
         D1   13   12   15   14
      C3 D0   25   24   27   26
         D1   29   28   31   30
   B1 C1 D0   41   40   43   42
...          ...  ...  ...  ...
A3 B0 C3 D1  221  220  223  222
   B1 C1 D0  233  232  235  234
         D1  237  236  239  238
      C3 D0  249  248  251  250
         D1  253  252  255  254

[32 rows x 4 columns]

进一步,您可以使用下列的方式来赋值

In [63]: df2 = dfmi.copy()

In [64]: df2.loc(axis=0)[:, :, ['C1', 'C3']] = -10

In [65]: df2
Out[65]: 
lvl0           a         b     
lvl1         bar  foo  bah  foo
A0 B0 C0 D0    1    0    3    2
         D1    5    4    7    6
      C1 D0  -10  -10  -10  -10
         D1  -10  -10  -10  -10
      C2 D0   17   16   19   18
...          ...  ...  ...  ...
A3 B1 C1 D1  -10  -10  -10  -10
      C2 D0  241  240  243  242
         D1  245  244  247  246
      C3 D0  -10  -10  -10  -10
         D1  -10  -10  -10  -10

[64 rows x 4 columns]

您也可以在等号的右侧使用一个可以被“重命名”的对象来赋值

In [66]: df2 = dfmi.copy()

In [67]: df2.loc[idx[:, :, ['C1', 'C3']], :] = df2 * 1000

In [68]: df2
Out[68]: 
lvl0              a               b        
lvl1            bar     foo     bah     foo
A0 B0 C0 D0       1       0       3       2
         D1       5       4       7       6
      C1 D0    9000    8000   11000   10000
         D1   13000   12000   15000   14000
      C2 D0      17      16      19      18
...             ...     ...     ...     ...
A3 B1 C1 D1  237000  236000  239000  238000
      C2 D0     241     240     243     242
         D1     245     244     247     246
      C3 D0  249000  248000  251000  250000
         D1  253000  252000  255000  254000

[64 rows x 4 columns]

# 交叉选择

DataFramexs() (opens new window)方法接受一个额外的参数,从而可以简便地在某个特定的多级索引中的某一个层级进行数据的选取。

In [69]: df
Out[69]: 
                     A         B         C
first second                              
bar   one     0.895717  0.410835 -1.413681
      two     0.805244  0.813850  1.607920
baz   one    -1.206412  0.132003  1.024180
      two     2.565646 -0.827317  0.569605
foo   one     1.431256 -0.076467  0.875906
      two     1.340309 -1.187678 -2.211372
qux   one    -1.170299  1.130127  0.974466
      two    -0.226169 -1.436737 -2.006747

In [70]: df.xs('one', level='second')
Out[70]: 
              A         B         C
first                              
bar    0.895717  0.410835 -1.413681
baz   -1.206412  0.132003  1.024180
foo    1.431256 -0.076467  0.875906
qux   -1.170299  1.130127  0.974466
# using the slicers
In [71]: df.loc[(slice(None), 'one'), :]
Out[71]: 
                     A         B         C
first second                              
bar   one     0.895717  0.410835 -1.413681
baz   one    -1.206412  0.132003  1.024180
foo   one     1.431256 -0.076467  0.875906
qux   one    -1.170299  1.130127  0.974466

您也可以用xs()并填写坐标参数来选择列。

In [72]: df = df.T

In [73]: df.xs('one', level='second', axis=1)
Out[73]: 
first       bar       baz       foo       qux
A      0.895717 -1.206412  1.431256 -1.170299
B      0.410835  0.132003 -0.076467  1.130127
C     -1.413681  1.024180  0.875906  0.974466
# using the slicers
In [74]: df.loc[:, (slice(None), 'one')]
Out[74]: 
first        bar       baz       foo       qux
second       one       one       one       one
A       0.895717 -1.206412  1.431256 -1.170299
B       0.410835  0.132003 -0.076467  1.130127
C      -1.413681  1.024180  0.875906  0.974466

xs也接受多个键(keys)来进行选取

In [75]: df.xs(('one', 'bar'), level=('second', 'first'), axis=1)
Out[75]: 
first        bar
second       one
A       0.895717
B       0.410835
C      -1.413681
# using the slicers
In [76]: df.loc[:, ('bar', 'one')]
Out[76]: 
A    0.895717
B    0.410835
C   -1.413681
Name: (bar, one), dtype: float64

您可以向xs传入 drop_level=False 来保留那些已经选取的层级。

In [77]: df.xs('one', level='second', axis=1, drop_level=False)
Out[77]: 
first        bar       baz       foo       qux
second       one       one       one       one
A       0.895717 -1.206412  1.431256 -1.170299
B       0.410835  0.132003 -0.076467  1.130127
C      -1.413681  1.024180  0.875906  0.974466

请比较上面,使用 drop_level=True(默认值)的结果。

In [78]: df.xs('one', level='second', axis=1, drop_level=True)
Out[78]: 
first       bar       baz       foo       qux
A      0.895717 -1.206412  1.431256 -1.170299
B      0.410835  0.132003 -0.076467  1.130127
C     -1.413681  1.024180  0.875906  0.974466

# 高级重命名索引及对齐

level参数已经被加入到pandas对象中的 reindex() (opens new window)align() (opens new window) 方法中。这将有助于沿着一个层级来广播值(broadcast values)。例如:

In [79]: midx = pd.MultiIndex(levels=[['zero', 'one'], ['x', 'y']],
   ....:                      codes=[[1, 1, 0, 0], [1, 0, 1, 0]])
   ....: 

In [80]: df = pd.DataFrame(np.random.randn(4, 2), index=midx)

In [81]: df
Out[81]: 
               0         1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

In [82]: df2 = df.mean(level=0)

In [83]: df2
Out[83]: 
             0         1
one   1.060074 -0.109716
zero  1.271532  0.713416

In [84]: df2.reindex(df.index, level=0)
Out[84]: 
               0         1
one  y  1.060074 -0.109716
     x  1.060074 -0.109716
zero y  1.271532  0.713416
     x  1.271532  0.713416

# aligning
In [85]: df_aligned, df2_aligned = df.align(df2, level=0)

In [86]: df_aligned
Out[86]: 
               0         1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

In [87]: df2_aligned
Out[87]: 
               0         1
one  y  1.060074 -0.109716
     x  1.060074 -0.109716
zero y  1.271532  0.713416
     x  1.271532  0.713416

# 使用swaplevel来交换层级

swaplevel() (opens new window)函数可以用来交换两个层级

In [88]: df[:5]
Out[88]: 
               0         1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

In [89]: df[:5].swaplevel(0, 1, axis=0)
Out[89]: 
               0         1
y one   1.519970 -0.493662
x one   0.600178  0.274230
y zero  0.132885 -0.023688
x zero  2.410179  1.450520

# 使用reorder_levels来进行层级重排序

reorder_levels() (opens new window)是一个更一般化的 swaplevel方法,允许您用简单的一步来重排列索引的层级:

In [90]: df[:5].reorder_levels([1, 0], axis=0)
Out[90]: 
               0         1
y one   1.519970 -0.493662
x one   0.600178  0.274230
y zero  0.132885 -0.023688
x zero  2.410179  1.450520

#索引多层索引进行重命名

rename() (opens new window)方法可以用来重命名多层索引,并且他经常被用于DataFrame的列名重命名。renamescolumns参数可以接受一个字典,从而仅仅重命名你希望更改名字的列。

In [91]: df.rename(columns={0: "col0", 1: "col1"})
Out[91]: 
            col0      col1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

该方法也可以被用于重命名一些DataFrame的特定主索引的名称。

In [92]: df.rename(index={"one": "two", "y": "z"})
Out[92]: 
               0         1
two  z  1.519970 -0.493662
     x  0.600178  0.274230
zero z  0.132885 -0.023688
     x  2.410179  1.450520

rename_axis() (opens new window) 方法可以用于对Index 或者 MultiIndex进行重命名。尤其的,你可以明确MultiIndex中的不同层级的名称,这可以被用于在之后使用 reset_index() ,把多层级索引的值转换为一个列

In [93]: df.rename_axis(index=['abc', 'def'])
Out[93]: 
                 0         1
abc  def                    
one  y    1.519970 -0.493662
     x    0.600178  0.274230
zero y    0.132885 -0.023688
     x    2.410179  1.450520

注意,DataFrame的列也是一个索引,因此在rename_axis中使用 columns 参数,将会改变那个索引的名称

In [94]: df.rename_axis(columns="Cols").columns
Out[94]: RangeIndex(start=0, stop=2, step=1, name='Cols')

renamerename_axis都支持一个明确的字典,Series 或者一个映射函数,将标签,名称映射为新的值

#多索引进行排序

对于拥有多层级索引的对象来说,你可以通过排序来是的索引或切片更为高效。就如同其他任何的索引操作一样,你可以使用 sort_index方法来实现。

In [95]: import random

In [96]: random.shuffle(tuples)

In [97]: s = pd.Series(np.random.randn(8), index=pd.MultiIndex.from_tuples(tuples))

In [98]: s
Out[98]: 
baz  one    0.206053
foo  two   -0.251905
qux  one   -2.213588
foo  one    1.063327
bar  two    1.266143
baz  two    0.299368
bar  one   -0.863838
qux  two    0.408204
dtype: float64

In [99]: s.sort_index()
Out[99]: 
bar  one   -0.863838
     two    1.266143
baz  one    0.206053
     two    0.299368
foo  one    1.063327
     two   -0.251905
qux  one   -2.213588
     two    0.408204
dtype: float64

In [100]: s.sort_index(level=0)
Out[100]: 
bar  one   -0.863838
     two    1.266143
baz  one    0.206053
     two    0.299368
foo  one    1.063327
     two   -0.251905
qux  one   -2.213588
     two    0.408204
dtype: float64

In [101]: s.sort_index(level=1)
Out[101]: 
bar  one   -0.863838
baz  one    0.206053
foo  one    1.063327
qux  one   -2.213588
bar  two    1.266143
baz  two    0.299368
foo  two   -0.251905
qux  two    0.408204
dtype: float64

如果你的多层级索引都被命名了的话,你也可以向 sort_index 传入一个层级名称。

In [102]: s.index.set_names(['L1', 'L2'], inplace=True)

In [103]: s.sort_index(level='L1')
Out[103]: 
L1   L2 
bar  one   -0.863838
     two    1.266143
baz  one    0.206053
     two    0.299368
foo  one    1.063327
     two   -0.251905
qux  one   -2.213588
     two    0.408204
dtype: float64

In [104]: s.sort_index(level='L2')
Out[104]: 
L1   L2 
bar  one   -0.863838
baz  one    0.206053
foo  one    1.063327
qux  one   -2.213588
bar  two    1.266143
baz  two    0.299368
foo  two   -0.251905
qux  two    0.408204
dtype: float64

对于多维度的对象来说,你也可以对任意的的维度来进行索引,只要他们是具有多层级索引的:

In [105]: df.T.sort_index(level=1, axis=1)
Out[105]: 
        one      zero       one      zero
          x         x         y         y
0  0.600178  2.410179  1.519970  0.132885
1  0.274230  1.450520 -0.493662 -0.023688

即便数据没有排序,你仍然可以对他们进行索引,但是索引的效率会极大降低,并且也会抛出PerformanceWarning警告。而且,这将返回一个数据的副本而非一个数据的视图:

In [106]: dfm = pd.DataFrame({'jim': [0, 0, 1, 1],
   .....:                     'joe': ['x', 'x', 'z', 'y'],
   .....:                     'jolie': np.random.rand(4)})
   .....: 

In [107]: dfm = dfm.set_index(['jim', 'joe'])

In [108]: dfm
Out[108]: 
            jolie
jim joe          
0   x    0.490671
    x    0.120248
1   z    0.537020
    y    0.110968
In [4]: dfm.loc[(1, 'z')]
PerformanceWarning: indexing past lexsort depth may impact performance.

Out[4]:
           jolie
jim joe
1   z    0.64094

另外,如果你试图索引一个没有完全lexsorted的对象,你将会碰到如下的错误:

In [5]: dfm.loc[(0, 'y'):(1, 'z')]
UnsortedIndexError: 'Key length (2) was greater than MultiIndex lexsort depth (1)'

MultiIndex上使用 is_lexsorted() (opens new window) 方法,你可以查看这个索引是否已经被排序。而使用lexsort_depth 属性则可以返回排序的深度

In [109]: dfm.index.is_lexsorted()
Out[109]: False

In [110]: dfm.index.lexsort_depth
Out[110]: 1
In [111]: dfm = dfm.sort_index()

In [112]: dfm
Out[112]: 
            jolie
jim joe          
0   x    0.490671
    x    0.120248
1   y    0.110968
    z    0.537020

In [113]: dfm.index.is_lexsorted()
Out[113]: True

In [114]: dfm.index.lexsort_depth
Out[114]: 2

现在,你的选择就可以正常工作了。

In [115]: dfm.loc[(0, 'y'):(1, 'z')]
Out[115]: 
            jolie
jim joe          
1   y    0.110968
    z    0.537020

# Take方法

NumPyndarrays相似,pandas的 IndexSeries,和DataFrame 也提供 take() (opens new window) 方法。他可以沿着某个维度,按照给定的索引取回所有的元素。这个给定的索引必须要是一个由整数组成的列表或者ndarray,用以指明在索引中的位置。take 也可以接受负整数,作为相对于结尾的相对位置。

In [116]: index = pd.Index(np.random.randint(0, 1000, 10))

In [117]: index
Out[117]: Int64Index([214, 502, 712, 567, 786, 175, 993, 133, 758, 329], dtype='int64')

In [118]: positions = [0, 9, 3]

In [119]: index[positions]
Out[119]: Int64Index([214, 329, 567], dtype='int64')

In [120]: index.take(positions)
Out[120]: Int64Index([214, 329, 567], dtype='int64')

In [121]: ser = pd.Series(np.random.randn(10))

In [122]: ser.iloc[positions]
Out[122]: 
0   -0.179666
9    1.824375
3    0.392149
dtype: float64

In [123]: ser.take(positions)
Out[123]: 
0   -0.179666
9    1.824375
3    0.392149
dtype: float64

对于DataFrames来说,这个给定的索引应当是一个一维列表或者ndarray,用于指明行或者列的位置。

In [124]: frm = pd.DataFrame(np.random.randn(5, 3))

In [125]: frm.take([1, 4, 3])
Out[125]: 
          0         1         2
1 -1.237881  0.106854 -1.276829
4  0.629675 -1.425966  1.857704
3  0.979542 -1.633678  0.615855

In [126]: frm.take([0, 2], axis=1)
Out[126]: 
          0         2
0  0.595974  0.601544
1 -1.237881 -1.276829
2 -0.767101  1.499591
3  0.979542  0.615855
4  0.629675  1.857704

需要注意的是, pandas对象的take 方法并不会正常地工作在布尔索引上,并且有可能会返回一切意外的结果。

In [127]: arr = np.random.randn(10)

In [128]: arr.take([False, False, True, True])
Out[128]: array([-1.1935, -1.1935,  0.6775,  0.6775])

In [129]: arr[[0, 1]]
Out[129]: array([-1.1935,  0.6775])

In [130]: ser = pd.Series(np.random.randn(10))

In [131]: ser.take([False, False, True, True])
Out[131]: 
0    0.233141
0    0.233141
1   -0.223540
1   -0.223540
dtype: float64

In [132]: ser.iloc[[0, 1]]
Out[132]: 
0    0.233141
1   -0.223540
dtype: float64

最后,关于性能方面的一个小建议,因为 take 方法处理的是一个范围更窄的输入,因此会比话实索引(fancy indexing)的速度快很多。

In [133]: arr = np.random.randn(10000, 5)

In [134]: indexer = np.arange(10000)

In [135]: random.shuffle(indexer)

In [136]: %timeit arr[indexer]
   .....: %timeit arr.take(indexer, axis=0)
   .....: 
152 us +- 988 ns per loop (mean +- std. dev. of 7 runs, 10000 loops each)
41.7 us +- 204 ns per loop (mean +- std. dev. of 7 runs, 10000 loops each)
In [137]: ser = pd.Series(arr[:, 0])

In [138]: %timeit ser.iloc[indexer]
   .....: %timeit ser.take(indexer)
   .....: 
120 us +- 1.05 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
110 us +- 795 ns per loop (mean +- std. dev. of 7 runs, 10000 loops each)

# 索引类型

我们在前面已经较为深入地探讨过了多层索引。你可以在 这里,可以找到关于 DatetimeIndexPeriodIndex的说明文件。在 这里,你可以找到关于TimedeltaIndex的说明。

In the following sub-sections we will highlight some other index types.

下面的一个子章节,我们将会着重探讨另外的一些索引的类型。

# 分类索引

CategoricalIndex (opens new window)分类索引 这种索引类型非常适合有重复的索引。这是一个围绕 Categorical (opens new window) 而创建的容器。这可以非常高效地存储和索引的具有大量重复元素的索引。

In [139]: from pandas.api.types import CategoricalDtype

In [140]: df = pd.DataFrame({'A': np.arange(6),
   .....:                    'B': list('aabbca')})
   .....: 

In [141]: df['B'] = df['B'].astype(CategoricalDtype(list('cab')))

In [142]: df
Out[142]: 
   A  B
0  0  a
1  1  a
2  2  b
3  3  b
4  4  c
5  5  a

In [143]: df.dtypes
Out[143]: 
A       int64
B    category
dtype: object

In [144]: df.B.cat.categories
Out[144]: Index(['c', 'a', 'b'], dtype='object')

通过设置索引将会建立一个 CategoricalIndex 分类索引.

In [145]: df2 = df.set_index('B')

In [146]: df2.index
Out[146]: CategoricalIndex(['a', 'a', 'b', 'b', 'c', 'a'], categories=['c', 'a', 'b'], ordered=False, name='B', dtype='category')

使用 __getitem__/.iloc/.loc 进行索引,在含有重复值的索引上的工作原理相似。索引值必须在一个分类中,否者将会引发KeyError错误。

In [147]: df2.loc['a']
Out[147]: 
   A
B   
a  0
a  1
a  5

CategoricalIndex 在索引之后也会被保留:

In [148]: df2.loc['a'].index
Out[148]: CategoricalIndex(['a', 'a', 'a'], categories=['c', 'a', 'b'], ordered=False, name='B', dtype='category')

索引排序将会按照类别清单中的顺序进行(我们已经基于 CategoricalDtype(list('cab'))建立了一个索引,因此排序的顺序是cab

In [149]: df2.sort_index()
Out[149]: 
   A
B   
c  4
a  0
a  1
a  5
b  2
b  3

分组操作(Groupby)也会保留索引的全部信息。

In [150]: df2.groupby(level=0).sum()
Out[150]: 
   A
B   
c  4
a  6
b  5

In [151]: df2.groupby(level=0).sum().index
Out[151]: CategoricalIndex(['c', 'a', 'b'], categories=['c', 'a', 'b'], ordered=False, name='B', dtype='category')

重设索引的操作将会根据输入的索引值返回一个索引。传入一个列表,将会返回一个最普通的Index;如果使用类别对象Categorical,则会返回一个分类索引CategoricalIndex,按照其中传入的的类别值Categorical dtype来进行索引。正如同你可以对任意pandas的索引进行重新索引一样,这将允许你随意索引任意的索引值,即便它们并不存在在你的类别对象中。

In [152]: df2.reindex(['a', 'e'])
Out[152]: 
     A
B     
a  0.0
a  1.0
a  5.0
e  NaN

In [153]: df2.reindex(['a', 'e']).index
Out[153]: Index(['a', 'a', 'a', 'e'], dtype='object', name='B')

In [154]: df2.reindex(pd.Categorical(['a', 'e'], categories=list('abcde')))
Out[154]: 
     A
B     
a  0.0
a  1.0
a  5.0
e  NaN

In [155]: df2.reindex(pd.Categorical(['a', 'e'], categories=list('abcde'))).index
Out[155]: CategoricalIndex(['a', 'a', 'a', 'e'], categories=['a', 'b', 'c', 'd', 'e'], ordered=False, name='B', dtype='category')

警告

对于一个分类索引的对象进行变形或者比较操作,一定要确保他们的索引包含相同的列别,否则将会出发类型错误TypeError

In [9]: df3 = pd.DataFrame({'A': np.arange(6), 'B': pd.Series(list('aabbca')).astype('category')})

In [11]: df3 = df3.set_index('B')

In [11]: df3.index
Out[11]: CategoricalIndex(['a', 'a', 'b', 'b', 'c', 'a'], categories=['a', 'b', 'c'], ordered=False, name='B', dtype='category')

In [12]: pd.concat([df2, df3])
TypeError: categories must match existing categories when appending

# 64位整型索引和范围索引

警告

使用浮点数进行基于数值的索引已经再0.18.0的版本中进行了声明。想查看更改的汇总,请参见 这里 (opens new window)

Int64Index (opens new window) 64位整型索引是pandas中的一种非常基本的索引操作。这是一个不可变的数组组成的一个有序的,可切片的集合。再0.18.0之前,Int64Index是会为所有NDFrame 对象提供默认的索引。

RangeIndex (opens new window) 范围索引是64位整型索引的子集,在v0.18.0版本加入。现在由范围索引来为所有的NDFrame对象提供默认索引。 RangeIndex是一个对于 Int64Index 的优化版本,能够提供一个有序且严格单调的集合。这个索引与python的 range types (opens new window)是相似的

# 64位浮点索引

默认情况下,当传入浮点数、或者浮点整型混合数的时候,一个64位浮点索引 Float64Index (opens new window)将会自动被建立。这样将能够确保一个存粹而统一的基于标签的索引切片行为,这样[],ix,loc对于标量索引和切片的工作行为将会完全一致。

In [156]: indexf = pd.Index([1.5, 2, 3, 4.5, 5])

In [157]: indexf
Out[157]: Float64Index([1.5, 2.0, 3.0, 4.5, 5.0], dtype='float64')

In [158]: sf = pd.Series(range(5), index=indexf)

In [159]: sf
Out[159]: 
1.5    0
2.0    1
3.0    2
4.5    3
5.0    4
dtype: int64

标量选择对于[],.loc永远都是基于标签的。一个整型将会自动匹配一个浮点标签(例如,3 等于 3.0

In [160]: sf[3]
Out[160]: 2

In [161]: sf[3.0]
Out[161]: 2

In [162]: sf.loc[3]
Out[162]: 2

In [163]: sf.loc[3.0]
Out[163]: 2

唯一能够通过位置进行索引的方式是通过iloc方法。

In [164]: sf.iloc[3]
Out[164]: 3

一个找不到的标量索引会触发一个KeyError错误。当使用[],ix,loc是,切片操作优先会选择索引的值,但是iloc永远都会按位置索引。唯一的例外是使用布尔索引,此时将始终按位置选择。

In [165]: sf[2:4]
Out[165]: 
2.0    1
3.0    2
dtype: int64

In [166]: sf.loc[2:4]
Out[166]: 
2.0    1
3.0    2
dtype: int64

In [167]: sf.iloc[2:4]
Out[167]: 
3.0    2
4.5    3
dtype: int64

如果你使用的是浮点数索引,那么使用浮点数切片也是可以执行的。

In [168]: sf[2.1:4.6]
Out[168]: 
3.0    2
4.5    3
dtype: int64

In [169]: sf.loc[2.1:4.6]
Out[169]: 
3.0    2
4.5    3
dtype: int64

在非浮点数中,如果使用浮点索引,将会触发TypeError错误。

In [1]: pd.Series(range(5))[3.5]
TypeError: the label [3.5] is not a proper indexer for this index type (Int64Index)

In [1]: pd.Series(range(5))[3.5:4.5]
TypeError: the slice start [3.5] is not a proper indexer for this index type (Int64Index)

警告

从0.18.0开始,.iloc将不能够使用标量浮点数进行索引,因此下列操作将触发TypeError错误。

In [3]: pd.Series(range(5)).iloc[3.0]
TypeError: cannot do positional indexing on <class 'pandas.indexes.range.RangeIndex'> with these indexers [3.0] of <type 'float'>

这里有一个典型的场景来使用这种类型的索引方式。设想你有一个不规范的类timedelta的索引方案,但是日期是按照浮点数的方式记录的。这将会导致(例如)毫秒级的延迟。

In [170]: dfir = pd.concat([pd.DataFrame(np.random.randn(5, 2),
   .....:                                index=np.arange(5) * 250.0,
   .....:                                columns=list('AB')),
   .....:                   pd.DataFrame(np.random.randn(6, 2),
   .....:                                index=np.arange(4, 10) * 250.1,
   .....:                                columns=list('AB'))])
   .....: 

In [171]: dfir
Out[171]: 
               A         B
0.0    -0.435772 -1.188928
250.0  -0.808286 -0.284634
500.0  -1.815703  1.347213
750.0  -0.243487  0.514704
1000.0  1.162969 -0.287725
1000.4 -0.179734  0.993962
1250.5 -0.212673  0.909872
1500.6 -0.733333 -0.349893
1750.7  0.456434 -0.306735
2000.8  0.553396  0.166221
2250.9 -0.101684 -0.734907

因此选择操作将总是按照值来进行所有的选择工作,

In [172]: dfir[0:1000.4]
Out[172]: 
               A         B
0.0    -0.435772 -1.188928
250.0  -0.808286 -0.284634
500.0  -1.815703  1.347213
750.0  -0.243487  0.514704
1000.0  1.162969 -0.287725
1000.4 -0.179734  0.993962

In [173]: dfir.loc[0:1001, 'A']
Out[173]: 
0.0      -0.435772
250.0    -0.808286
500.0    -1.815703
750.0    -0.243487
1000.0    1.162969
1000.4   -0.179734
Name: A, dtype: float64

In [174]: dfir.loc[1000.4]
Out[174]: 
A   -0.179734
B    0.993962
Name: 1000.4, dtype: float64

你可以返回第一秒(1000毫秒)的数据:

In [175]: dfir[0:1000]
Out[175]: 
               A         B
0.0    -0.435772 -1.188928
250.0  -0.808286 -0.284634
500.0  -1.815703  1.347213
750.0  -0.243487  0.514704
1000.0  1.162969 -0.287725

如果你想要使用基于整型的选择,你应该使用iloc:

In [176]: dfir.iloc[0:5]
Out[176]: 
               A         B
0.0    -0.435772 -1.188928
250.0  -0.808286 -0.284634
500.0  -1.815703  1.347213
750.0  -0.243487  0.514704
1000.0  1.162969 -0.287725

# 间隔索引

0.20.0中新加入 IntervalIndex (opens new window)和它自己特有的IntervalDtype以及 Interval (opens new window) 标量类型,在pandas中,间隔数据是获得头等支持的。

IntervalIndex间隔索引允许一些唯一的索引,并且也是 cut() (opens new window)qcut() (opens new window)的返回类型

# 使用间隔索引来进行数据索引

In [177]: df = pd.DataFrame({'A': [1, 2, 3, 4]},
   .....:                   index=pd.IntervalIndex.from_breaks([0, 1, 2, 3, 4]))
   .....: 

In [178]: df
Out[178]: 
        A
(0, 1]  1
(1, 2]  2
(2, 3]  3
(3, 4]  4

在间隔序列上使用基于标签的索引.loc ,正如你所预料到的,将会选择那个特定的间隔

In [179]: df.loc[2]
Out[179]: 
A    2
Name: (1, 2], dtype: int64

In [180]: df.loc[[2, 3]]
Out[180]: 
        A
(1, 2]  2
(2, 3]  3

如果你选取了一个标签,被包含在间隔当中,这个间隔也将会被选择

In [181]: df.loc[2.5]
Out[181]: 
A    3
Name: (2, 3], dtype: int64

In [182]: df.loc[[2.5, 3.5]]
Out[182]: 
        A
(2, 3]  3
(3, 4]  4

使用 Interval来选择,将只返回严格匹配(从pandas0.25.0开始)。

In [183]: df.loc[pd.Interval(1, 2)]
Out[183]: 
A    2
Name: (1, 2], dtype: int64

试图选择一个没有被严格包含在 IntervalIndex 内的区间Interval,将会出发KeyError错误。

In [7]: df.loc[pd.Interval(0.5, 2.5)]
---------------------------------------------------------------------------
KeyError: Interval(0.5, 2.5, closed='right')

可以使用overlaps() (opens new window)来创建一个布尔选择器,来选中所有与给定区间(Interval)重复的所有区间。

In [184]: idxr = df.index.overlaps(pd.Interval(0.5, 2.5))

In [185]: idxr
Out[185]: array([ True,  True,  True, False])

In [186]: df[idxr]
Out[186]: 
        A
(0, 1]  1
(1, 2]  2
(2, 3]  3

# 使用 cutqcut来为数据分块

cut() (opens new window)qcut() (opens new window) 都将返回一个分类Categorical 对象,并且每个分块区域都会以 分类索引IntervalIndex的方式被创建并保存在它的.categories属性中。

In [187]: c = pd.cut(range(4), bins=2)

In [188]: c
Out[188]: 
[(-0.003, 1.5], (-0.003, 1.5], (1.5, 3.0], (1.5, 3.0]]
Categories (2, interval[float64]): [(-0.003, 1.5] < (1.5, 3.0]]

In [189]: c.categories
Out[189]: 
IntervalIndex([(-0.003, 1.5], (1.5, 3.0]],
              closed='right',
              dtype='interval[float64]')

cut() (opens new window) 也可以接受一个 IntervalIndex 作为他的 bins 参数,这样可以使用一个非常有用的pandas的写法。 首先,我们调用 cut() (opens new window) 在一些数据上面,并且将 bins设置为某一个固定的数 ,从而生成bins。

随后,我们可以在其他的数据上调用 cut() (opens new window),并传入.categories 的值,作为 bins参数。这样新的数据就也将会被分配到同样的bins里面

In [190]: pd.cut([0, 3, 5, 1], bins=c.categories)
Out[190]: 
[(-0.003, 1.5], (1.5, 3.0], NaN, (-0.003, 1.5]]
Categories (2, interval[float64]): [(-0.003, 1.5] < (1.5, 3.0]]

任何落在bins之外的数据都将会被设为 NaN

# 生成一定区间内的间隔

如果我们需要经常地使用步进区间,我们可以使用 interval_range() (opens new window) 函数,结合 start, end, 和 periods来建立一个 IntervalIndex 对于数值型的间隔,默认的 interval_range间隔频率是1,对于datetime类型的间隔则是日历日。

In [191]: pd.interval_range(start=0, end=5)
Out[191]: 
IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]],
              closed='right',
              dtype='interval[int64]')

In [192]: pd.interval_range(start=pd.Timestamp('2017-01-01'), periods=4)
Out[192]: 
IntervalIndex([(2017-01-01, 2017-01-02], (2017-01-02, 2017-01-03], (2017-01-03, 2017-01-04], (2017-01-04, 2017-01-05]],
              closed='right',
              dtype='interval[datetime64[ns]]')

In [193]: pd.interval_range(end=pd.Timedelta('3 days'), periods=3)
Out[193]: 
IntervalIndex([(0 days 00:00:00, 1 days 00:00:00], (1 days 00:00:00, 2 days 00:00:00], (2 days 00:00:00, 3 days 00:00:00]],
              closed='right',
              dtype='interval[timedelta64[ns]]')

freq 参数可以被用来明确非默认的频率,并且可以充分地利用各种各样的 frequency aliases datetime类型的时间间隔。

In [194]: pd.interval_range(start=0, periods=5, freq=1.5)
Out[194]: 
IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0], (6.0, 7.5]],
              closed='right',
              dtype='interval[float64]')

In [195]: pd.interval_range(start=pd.Timestamp('2017-01-01'), periods=4, freq='W')
Out[195]: 
IntervalIndex([(2017-01-01, 2017-01-08], (2017-01-08, 2017-01-15], (2017-01-15, 2017-01-22], (2017-01-22, 2017-01-29]],
              closed='right',
              dtype='interval[datetime64[ns]]')

In [196]: pd.interval_range(start=pd.Timedelta('0 days'), periods=3, freq='9H')
Out[196]: 
IntervalIndex([(0 days 00:00:00, 0 days 09:00:00], (0 days 09:00:00, 0 days 18:00:00], (0 days 18:00:00, 1 days 03:00:00]],
              closed='right',
              dtype='interval[timedelta64[ns]]')

此外, closed 参数可以用来声明哪个边界是包含的。默认情况下,间隔的右界是包含的。

In [197]: pd.interval_range(start=0, end=4, closed='both')
Out[197]: 
IntervalIndex([[0, 1], [1, 2], [2, 3], [3, 4]],
              closed='both',
              dtype='interval[int64]')

In [198]: pd.interval_range(start=0, end=4, closed='neither')
Out[198]: 
IntervalIndex([(0, 1), (1, 2), (2, 3), (3, 4)],
              closed='neither',
              dtype='interval[int64]')

v0.23.0新加入

使用start, end, 和 periods可以从 startend(包含)生成一个平均分配的间隔,在返回IntervalIndex中生成periods这么多的元素(译者:区间)。

In [199]: pd.interval_range(start=0, end=6, periods=4)
Out[199]: 
IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]],
              closed='right',
              dtype='interval[float64]')

In [200]: pd.interval_range(pd.Timestamp('2018-01-01'),
   .....:                   pd.Timestamp('2018-02-28'), periods=3)
   .....: 
Out[200]: 
IntervalIndex([(2018-01-01, 2018-01-20 08:00:00], (2018-01-20 08:00:00, 2018-02-08 16:00:00], (2018-02-08 16:00:00, 2018-02-28]],
              closed='right',
              dtype='interval[datetime64[ns]]')

# 其他索引常见问题

# 数值索引

使用数值作为各维度的标签,再基于标签进行索引是一个非常痛苦的话题。在Scientific Python社区的邮件列表中,进行着剧烈的争论。在Pandas中,我们一般性的观点是,标签比实际的(用数值表示的)位置更为重要。因此,对于使用数值作为标签的的对象来说,只有基于标签的索引才可以在标准工具,例如.loc方法,中正常使用。下面的代码将引发错误:

In [201]: s = pd.Series(range(5))

In [202]: s[-1]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-202-76c3dce40054> in <module>
----> 1 s[-1]

/pandas/pandas/core/series.py in __getitem__(self, key)
   1062         key = com.apply_if_callable(key, self)
   1063         try:
-> 1064             result = self.index.get_value(self, key)
   1065 
   1066             if not is_scalar(result):

/pandas/pandas/core/indexes/base.py in get_value(self, series, key)
   4721         k = self._convert_scalar_indexer(k, kind="getitem")
   4722         try:
-> 4723             return self._engine.get_value(s, k, tz=getattr(series.dtype, "tz", None))
   4724         except KeyError as e1:
   4725             if len(self) > 0 and (self.holds_integer() or self.is_boolean()):

/pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()

/pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()

/pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

/pandas/pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()

/pandas/pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()

KeyError: -1

In [203]: df = pd.DataFrame(np.random.randn(5, 4))

In [204]: df
Out[204]: 
          0         1         2         3
0 -0.130121 -0.476046  0.759104  0.213379
1 -0.082641  0.448008  0.656420 -1.051443
2  0.594956 -0.151360 -0.069303  1.221431
3 -0.182832  0.791235  0.042745  2.069775
4  1.446552  0.019814 -1.389212 -0.702312

In [205]: df.loc[-2:]
Out[205]: 
          0         1         2         3
0 -0.130121 -0.476046  0.759104  0.213379
1 -0.082641  0.448008  0.656420 -1.051443
2  0.594956 -0.151360 -0.069303  1.221431
3 -0.182832  0.791235  0.042745  2.069775
4  1.446552  0.019814 -1.389212 -0.702312

我们特意地做了这样的设计,是为了阻止歧义性以及一些难以避免的小bug(当我们修改了函数,从而阻止了“滚回”到基于位置的索引方式以后,许多用户报告说,他们发现了bug)。

# 非单调索引要求严格匹配

如果一个 序列 或者 数据表是单调递增或递减的,那么基于标签的切片行为的边界是可以超出索引的,这与普通的python列表的索引切片非常相似。索引的单调性可以使用 is_monotonic_increasing() (opens new window)is_monotonic_decreasing() (opens new window) 属性来检查

In [206]: df = pd.DataFrame(index=[2, 3, 3, 4, 5], columns=['data'], data=list(range(5)))

In [207]: df.index.is_monotonic_increasing
Out[207]: True

# no rows 0 or 1, but still returns rows 2, 3 (both of them), and 4:
In [208]: df.loc[0:4, :]
Out[208]: 
   data
2     0
3     1
3     2
4     3

# slice is are outside the index, so empty DataFrame is returned
In [209]: df.loc[13:15, :]
Out[209]: 
Empty DataFrame
Columns: [data]
Index: []

另一方面,如果索引不是单调的,那么切片的两侧边界都必须是索引值中的唯一值。

In [210]: df = pd.DataFrame(index=[2, 3, 1, 4, 3, 5],
   .....:                   columns=['data'], data=list(range(6)))
   .....: 

In [211]: df.index.is_monotonic_increasing
Out[211]: False

# OK because 2 and 4 are in the index
In [212]: df.loc[2:4, :]
Out[212]: 
   data
2     0
3     1
1     2
4     3
# 0 is not in the index
In [9]: df.loc[0:4, :]
KeyError: 0

# 3 is not a unique label
In [11]: df.loc[2:3, :]
KeyError: 'Cannot get right slice bound for non-unique label: 3'

Index.is_monotonic_increasingIndex.is_monotonic_decreasing方法只能进行弱单调性的检查。要进行严格的单调性检查,你可以配合 is_unique() (opens new window) 方法一起使用。

In [213]: weakly_monotonic = pd.Index(['a', 'b', 'c', 'c'])

In [214]: weakly_monotonic
Out[214]: Index(['a', 'b', 'c', 'c'], dtype='object')

In [215]: weakly_monotonic.is_monotonic_increasing
Out[215]: True

In [216]: weakly_monotonic.is_monotonic_increasing & weakly_monotonic.is_unique
Out[216]: False

# 终止点被包含在内

与表中的python序列切片中,终止点不被包含不同,基于标签的切片在Pandas中,终止点是被包含在内的。最主要的原因是因为,我们很难准确地确定在索引中的“下一个”标签“到底是什么。例如下面这个序列

In [217]: s = pd.Series(np.random.randn(6), index=list('abcdef'))

In [218]: s
Out[218]: 
a    0.301379
b    1.240445
c   -0.846068
d   -0.043312
e   -1.658747
f   -0.819549
dtype: float64

如果我们希望从c选取到e,如果我们使用基于数值的索引,那将会由如下操作:

In [219]: s[2:5]
Out[219]: 
c   -0.846068
d   -0.043312
e   -1.658747
dtype: float64

然而,如果你只有ce,确定下一个索引中的元素将会是比较困难的。例如,下面的这种方法完全是行不通的:

s.loc['c':'e' + 1]

一个非常常见的用例是限制一个时间序列的起始和终止日期。为了能够便于操作,我们决定在基于标签的切片行为中包含两个端点:

In [220]: s.loc['c':'e']
Out[220]: 
c   -0.846068
d   -0.043312
e   -1.658747
dtype: float64

这是一个非常典型的“显示战胜理想”的情况,但是如果你仅仅是想当然的认为基于标签的索引应该会和标准python中的整数型索引有着相同的行为时,你也确实需要多加留意。

# 索引会潜在地改变序列的dtype

不同的索引操作有可能会潜在地改变一个序列的dtypes

In [221]: series1 = pd.Series([1, 2, 3])

In [222]: series1.dtype
Out[222]: dtype('int64')

In [223]: res = series1.reindex([0, 4])

In [224]: res.dtype
Out[224]: dtype('float64')

In [225]: res
Out[225]: 
0    1.0
4    NaN
dtype: float64
In [226]: series2 = pd.Series([True])

In [227]: series2.dtype
Out[227]: dtype('bool')

In [228]: res = series2.reindex_like(series1)

In [229]: res.dtype
Out[229]: dtype('O')

In [230]: res
Out[230]: 
0    True
1     NaN
2     NaN
dtype: object

这是因为上述(重新)索引的操作悄悄地插入了 NaNs ,因此dtype也就随之发生改变了。如果你在使用一些numpyufuncs,如 numpy.logical_and时,将会导致一些问题。

参见 this old issue (opens new window)了解更详细的讨论过程