多层级索引和高级索引

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

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

::: danger 警告

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

:::

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

分层索引(多层级索引)

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

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

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

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

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

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

  1. In [1]: arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
  2. ...: ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
  3. ...:
  4. In [2]: tuples = list(zip(*arrays))
  5. In [3]: tuples
  6. Out[3]:
  7. [('bar', 'one'),
  8. ('bar', 'two'),
  9. ('baz', 'one'),
  10. ('baz', 'two'),
  11. ('foo', 'one'),
  12. ('foo', 'two'),
  13. ('qux', 'one'),
  14. ('qux', 'two')]
  15. In [4]: index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
  16. In [5]: index
  17. Out[5]:
  18. MultiIndex([('bar', 'one'),
  19. ('bar', 'two'),
  20. ('baz', 'one'),
  21. ('baz', 'two'),
  22. ('foo', 'one'),
  23. ('foo', 'two'),
  24. ('qux', 'one'),
  25. ('qux', 'two')],
  26. names=['first', 'second'])
  27. In [6]: s = pd.Series(np.random.randn(8), index=index)
  28. In [7]: s
  29. Out[7]:
  30. first second
  31. bar one 0.469112
  32. two -0.282863
  33. baz one -1.509059
  34. two -1.135632
  35. foo one 1.212112
  36. two -0.173215
  37. qux one 0.119209
  38. two -1.044236
  39. dtype: float64

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

  1. In [8]: iterables = [['bar', 'baz', 'foo', 'qux'], ['one', 'two']]
  2. In [9]: pd.MultiIndex.from_product(iterables, names=['first', 'second'])
  3. Out[9]:
  4. MultiIndex([('bar', 'one'),
  5. ('bar', 'two'),
  6. ('baz', 'one'),
  7. ('baz', 'two'),
  8. ('foo', 'one'),
  9. ('foo', 'two'),
  10. ('qux', 'one'),
  11. ('qux', 'two')],
  12. names=['first', 'second'])

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

0.24.0版本新增。

  1. In [10]: df = pd.DataFrame([['bar', 'one'], ['bar', 'two'],
  2. ....: ['foo', 'one'], ['foo', 'two']],
  3. ....: columns=['first', 'second'])
  4. ....:
  5. In [11]: pd.MultiIndex.from_frame(df)
  6. Out[11]:
  7. MultiIndex([('bar', 'one'),
  8. ('bar', 'two'),
  9. ('foo', 'one'),
  10. ('foo', 'two')],
  11. names=['first', 'second'])

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

  1. In [12]: arrays = [np.array(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux']),
  2. ....: np.array(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'])]
  3. ....:
  4. In [13]: s = pd.Series(np.random.randn(8), index=arrays)
  5. In [14]: s
  6. Out[14]:
  7. bar one -0.861849
  8. two -2.104569
  9. baz one -0.494929
  10. two 1.071804
  11. foo one 0.721555
  12. two -0.706771
  13. qux one -1.039575
  14. two 0.271860
  15. dtype: float64
  16. In [15]: df = pd.DataFrame(np.random.randn(8, 4), index=arrays)
  17. In [16]: df
  18. Out[16]:
  19. 0 1 2 3
  20. bar one -0.424972 0.567020 0.276232 -1.087401
  21. two -0.673690 0.113648 -1.478427 0.524988
  22. baz one 0.404705 0.577046 -1.715002 -1.039268
  23. two -0.370647 -1.157892 -1.344312 0.844885
  24. foo one 1.075770 -0.109050 1.643563 -1.469388
  25. two 0.357021 -0.674600 -1.776904 -0.968914
  26. qux one -1.294524 0.413738 0.276662 -0.472035
  27. two -0.013960 -0.362543 -0.006154 -0.923061

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

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

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

  1. In [18]: df = pd.DataFrame(np.random.randn(3, 8), index=['A', 'B', 'C'], columns=index)
  2. In [19]: df
  3. Out[19]:
  4. first bar baz foo qux
  5. second one two one two one two one two
  6. A 0.895717 0.805244 -1.206412 2.565646 1.431256 1.340309 -1.170299 -0.226169
  7. B 0.410835 0.813850 0.132003 -0.827317 -0.076467 -1.187678 1.130127 -1.436737
  8. C -1.413681 1.607920 1.024180 0.569605 0.875906 -2.211372 0.974466 -2.006747
  9. In [20]: pd.DataFrame(np.random.randn(6, 6), index=index[:6], columns=index[:6])
  10. Out[20]:
  11. first bar baz foo
  12. second one two one two one two
  13. first second
  14. bar one -0.410001 -0.078638 0.545952 -1.219217 -1.226825 0.769804
  15. two -1.281247 -0.727707 -0.121306 -0.097883 0.695775 0.341734
  16. baz one 0.959726 -1.110336 -0.619976 0.149748 -0.732339 0.687738
  17. two 0.176444 0.403310 -0.154951 0.301624 -2.179861 -1.369849
  18. foo one -0.954208 1.462696 -1.743161 -0.826591 -0.345352 1.314232
  19. two 0.690579 0.995761 2.396780 0.014871 3.357427 -0.317441

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

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

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

  1. In [22]: pd.Series(np.random.randn(8), index=tuples)
  2. Out[22]:
  3. (bar, one) -1.236269
  4. (bar, two) 0.896171
  5. (baz, one) -0.487602
  6. (baz, two) -0.082240
  7. (foo, one) -2.182937
  8. (foo, two) 0.380396
  9. (qux, one) 0.084844
  10. (qux, two) 0.432390
  11. dtype: float64

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

重构层次标签

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

  1. In [23]: index.get_level_values(0)
  2. Out[23]: Index(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], dtype='object', name='first')
  3. In [24]: index.get_level_values('second')
  4. Out[24]: Index(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], dtype='object', name='second')

基本索引轴上的多索引

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

  1. In [25]: df['bar']
  2. Out[25]:
  3. second one two
  4. A 0.895717 0.805244
  5. B 0.410835 0.813850
  6. C -1.413681 1.607920
  7. In [26]: df['bar', 'one']
  8. Out[26]:
  9. A 0.895717
  10. B 0.410835
  11. C -1.413681
  12. Name: (bar, one), dtype: float64
  13. In [27]: df['bar']['one']
  14. Out[27]:
  15. A 0.895717
  16. B 0.410835
  17. C -1.413681
  18. Name: one, dtype: float64
  19. In [28]: s['qux']
  20. Out[28]:
  21. one -1.039575
  22. two 0.271860
  23. dtype: float64

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

定义不同层次索引

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

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

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

  1. In [31]: df[['foo', 'qux']].columns.to_numpy()
  2. Out[31]:
  3. array([('foo', 'one'), ('foo', 'two'), ('qux', 'one'), ('qux', 'two')],
  4. dtype=object)
  5. # for a specific level
  6. In [32]: df[['foo', 'qux']].columns.get_level_values(0)
  7. Out[32]: Index(['foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

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

0.20.0.中更新

  1. In [33]: new_mi = df[['foo', 'qux']].columns.remove_unused_levels()
  2. In [34]: new_mi.levels
  3. Out[34]: FrozenList([['foo', 'qux'], ['one', 'two']])

数据对齐和使用 reindex

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

  1. In [35]: s + s[:-2]
  2. Out[35]:
  3. bar one -1.723698
  4. two -4.209138
  5. baz one -0.989859
  6. two 2.143608
  7. foo one 1.443110
  8. two -1.413542
  9. qux one NaN
  10. two NaN
  11. dtype: float64
  12. In [36]: s + s[::2]
  13. Out[36]:
  14. bar one -1.723698
  15. two NaN
  16. baz one -0.989859
  17. two NaN
  18. foo one 1.443110
  19. two NaN
  20. qux one -2.079150
  21. two NaN
  22. dtype: float64

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

  1. In [37]: s.reindex(index[:3])
  2. Out[37]:
  3. first second
  4. bar one -0.861849
  5. two -2.104569
  6. baz one -0.494929
  7. dtype: float64
  8. In [38]: s.reindex([('foo', 'two'), ('bar', 'one'), ('qux', 'one'), ('baz', 'one')])
  9. Out[38]:
  10. foo two -0.706771
  11. bar one -0.861849
  12. qux one -1.039575
  13. baz one -0.494929
  14. dtype: float64

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

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

  1. In [39]: df = df.T
  2. In [40]: df
  3. Out[40]:
  4. A B C
  5. first second
  6. bar one 0.895717 0.410835 -1.413681
  7. two 0.805244 0.813850 1.607920
  8. baz one -1.206412 0.132003 1.024180
  9. two 2.565646 -0.827317 0.569605
  10. foo one 1.431256 -0.076467 0.875906
  11. two 1.340309 -1.187678 -2.211372
  12. qux one -1.170299 1.130127 0.974466
  13. two -0.226169 -1.436737 -2.006747
  14. In [41]: df.loc[('bar', 'two')]
  15. Out[41]:
  16. A 0.805244
  17. B 0.813850
  18. C 1.607920
  19. Name: (bar, two), dtype: float64

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

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

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

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

  1. df.loc[‘bar’]

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

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

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

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

  1. In [44]: df.loc[('baz', 'two'):('qux', 'one')]
  2. Out[44]:
  3. A B C
  4. first second
  5. baz two 2.565646 -0.827317 0.569605
  6. foo one 1.431256 -0.076467 0.875906
  7. two 1.340309 -1.187678 -2.211372
  8. qux one -1.170299 1.130127 0.974466
  9. In [45]: df.loc[('baz', 'two'):'foo']
  10. Out[45]:
  11. A B C
  12. first second
  13. baz two 2.565646 -0.827317 0.569605
  14. foo one 1.431256 -0.076467 0.875906
  15. two 1.340309 -1.187678 -2.211372

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

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

::: tip 小技巧

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

:::

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

  1. In [47]: s = pd.Series([1, 2, 3, 4, 5, 6],
  2. ....: index=pd.MultiIndex.from_product([["A", "B"], ["c", "d", "e"]]))
  3. ....:
  4. In [48]: s.loc[[("A", "c"), ("B", "d")]] # list of tuples
  5. Out[48]:
  6. A c 1
  7. B d 5
  8. dtype: int64
  9. In [49]: s.loc[(["A", "B"], ["c", "d"])] # tuple of lists
  10. Out[49]:
  11. A c 1
  12. d 2
  13. B c 4
  14. d 5
  15. dtype: int64

使用切片器

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

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

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

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

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

建议使用下列的方式:

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

不建议使用下列的方式:

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

:::

  1. In [50]: def mklbl(prefix, n):
  2. ....: return ["%s%s" % (prefix, i) for i in range(n)]
  3. ....:
  4. In [51]: miindex = pd.MultiIndex.from_product([mklbl('A', 4),
  5. ....: mklbl('B', 2),
  6. ....: mklbl('C', 4),
  7. ....: mklbl('D', 2)])
  8. ....:
  9. In [52]: micolumns = pd.MultiIndex.from_tuples([('a', 'foo'), ('a', 'bar'),
  10. ....: ('b', 'foo'), ('b', 'bah')],
  11. ....: names=['lvl0', 'lvl1'])
  12. ....:
  13. In [53]: dfmi = pd.DataFrame(np.arange(len(miindex) * len(micolumns))
  14. ....: .reshape((len(miindex), len(micolumns))),
  15. ....: index=miindex,
  16. ....: columns=micolumns).sort_index().sort_index(axis=1)
  17. ....:
  18. In [54]: dfmi
  19. Out[54]:
  20. lvl0 a b
  21. lvl1 bar foo bah foo
  22. A0 B0 C0 D0 1 0 3 2
  23. D1 5 4 7 6
  24. C1 D0 9 8 11 10
  25. D1 13 12 15 14
  26. C2 D0 17 16 19 18
  27. ... ... ... ... ...
  28. A3 B1 C1 D1 237 236 239 238
  29. C2 D0 241 240 243 242
  30. D1 245 244 247 246
  31. C3 D0 249 248 251 250
  32. D1 253 252 255 254
  33. [64 rows x 4 columns]

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

  1. In [55]: dfmi.loc[(slice('A1', 'A3'), slice(None), ['C1', 'C3']), :]
  2. Out[55]:
  3. lvl0 a b
  4. lvl1 bar foo bah foo
  5. A1 B0 C1 D0 73 72 75 74
  6. D1 77 76 79 78
  7. C3 D0 89 88 91 90
  8. D1 93 92 95 94
  9. B1 C1 D0 105 104 107 106
  10. ... ... ... ... ...
  11. A3 B0 C3 D1 221 220 223 222
  12. B1 C1 D0 233 232 235 234
  13. D1 237 236 239 238
  14. C3 D0 249 248 251 250
  15. D1 253 252 255 254
  16. [24 rows x 4 columns]

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

  1. In [56]: idx = pd.IndexSlice
  2. In [57]: dfmi.loc[idx[:, :, ['C1', 'C3']], idx[:, 'foo']]
  3. Out[57]:
  4. lvl0 a b
  5. lvl1 foo foo
  6. A0 B0 C1 D0 8 10
  7. D1 12 14
  8. C3 D0 24 26
  9. D1 28 30
  10. B1 C1 D0 40 42
  11. ... ... ...
  12. A3 B0 C3 D1 220 222
  13. B1 C1 D0 232 234
  14. D1 236 238
  15. C3 D0 248 250
  16. D1 252 254
  17. [32 rows x 2 columns]

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

  1. In [58]: dfmi.loc['A1', (slice(None), 'foo')]
  2. Out[58]:
  3. lvl0 a b
  4. lvl1 foo foo
  5. B0 C0 D0 64 66
  6. D1 68 70
  7. C1 D0 72 74
  8. D1 76 78
  9. C2 D0 80 82
  10. ... ... ...
  11. B1 C1 D1 108 110
  12. C2 D0 112 114
  13. D1 116 118
  14. C3 D0 120 122
  15. D1 124 126
  16. [16 rows x 2 columns]
  17. In [59]: dfmi.loc[idx[:, :, ['C1', 'C3']], idx[:, 'foo']]
  18. Out[59]:
  19. lvl0 a b
  20. lvl1 foo foo
  21. A0 B0 C1 D0 8 10
  22. D1 12 14
  23. C3 D0 24 26
  24. D1 28 30
  25. B1 C1 D0 40 42
  26. ... ... ...
  27. A3 B0 C3 D1 220 222
  28. B1 C1 D0 232 234
  29. D1 236 238
  30. C3 D0 248 250
  31. D1 252 254
  32. [32 rows x 2 columns]

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

  1. In [60]: mask = dfmi[('a', 'foo')] > 200
  2. In [61]: dfmi.loc[idx[mask, :, ['C1', 'C3']], idx[:, 'foo']]
  3. Out[61]:
  4. lvl0 a b
  5. lvl1 foo foo
  6. A3 B0 C1 D1 204 206
  7. C3 D0 216 218
  8. D1 220 222
  9. B1 C1 D0 232 234
  10. D1 236 238
  11. C3 D0 248 250
  12. D1 252 254

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

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

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

  1. In [63]: df2 = dfmi.copy()
  2. In [64]: df2.loc(axis=0)[:, :, ['C1', 'C3']] = -10
  3. In [65]: df2
  4. Out[65]:
  5. lvl0 a b
  6. lvl1 bar foo bah foo
  7. A0 B0 C0 D0 1 0 3 2
  8. D1 5 4 7 6
  9. C1 D0 -10 -10 -10 -10
  10. D1 -10 -10 -10 -10
  11. C2 D0 17 16 19 18
  12. ... ... ... ... ...
  13. A3 B1 C1 D1 -10 -10 -10 -10
  14. C2 D0 241 240 243 242
  15. D1 245 244 247 246
  16. C3 D0 -10 -10 -10 -10
  17. D1 -10 -10 -10 -10
  18. [64 rows x 4 columns]

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

  1. In [66]: df2 = dfmi.copy()
  2. In [67]: df2.loc[idx[:, :, ['C1', 'C3']], :] = df2 * 1000
  3. In [68]: df2
  4. Out[68]:
  5. lvl0 a b
  6. lvl1 bar foo bah foo
  7. A0 B0 C0 D0 1 0 3 2
  8. D1 5 4 7 6
  9. C1 D0 9000 8000 11000 10000
  10. D1 13000 12000 15000 14000
  11. C2 D0 17 16 19 18
  12. ... ... ... ... ...
  13. A3 B1 C1 D1 237000 236000 239000 238000
  14. C2 D0 241 240 243 242
  15. D1 245 244 247 246
  16. C3 D0 249000 248000 251000 250000
  17. D1 253000 252000 255000 254000
  18. [64 rows x 4 columns]

交叉选择

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

  1. In [69]: df
  2. Out[69]:
  3. A B C
  4. first second
  5. bar one 0.895717 0.410835 -1.413681
  6. two 0.805244 0.813850 1.607920
  7. baz one -1.206412 0.132003 1.024180
  8. two 2.565646 -0.827317 0.569605
  9. foo one 1.431256 -0.076467 0.875906
  10. two 1.340309 -1.187678 -2.211372
  11. qux one -1.170299 1.130127 0.974466
  12. two -0.226169 -1.436737 -2.006747
  13. In [70]: df.xs('one', level='second')
  14. Out[70]:
  15. A B C
  16. first
  17. bar 0.895717 0.410835 -1.413681
  18. baz -1.206412 0.132003 1.024180
  19. foo 1.431256 -0.076467 0.875906
  20. qux -1.170299 1.130127 0.974466
  1. # using the slicers
  2. In [71]: df.loc[(slice(None), 'one'), :]
  3. Out[71]:
  4. A B C
  5. first second
  6. bar one 0.895717 0.410835 -1.413681
  7. baz one -1.206412 0.132003 1.024180
  8. foo one 1.431256 -0.076467 0.875906
  9. qux one -1.170299 1.130127 0.974466

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

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

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

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

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

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

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

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

高级重命名索引及对齐

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

  1. In [79]: midx = pd.MultiIndex(levels=[['zero', 'one'], ['x', 'y']],
  2. ....: codes=[[1, 1, 0, 0], [1, 0, 1, 0]])
  3. ....:
  4. In [80]: df = pd.DataFrame(np.random.randn(4, 2), index=midx)
  5. In [81]: df
  6. Out[81]:
  7. 0 1
  8. one y 1.519970 -0.493662
  9. x 0.600178 0.274230
  10. zero y 0.132885 -0.023688
  11. x 2.410179 1.450520
  12. In [82]: df2 = df.mean(level=0)
  13. In [83]: df2
  14. Out[83]:
  15. 0 1
  16. one 1.060074 -0.109716
  17. zero 1.271532 0.713416
  18. In [84]: df2.reindex(df.index, level=0)
  19. Out[84]:
  20. 0 1
  21. one y 1.060074 -0.109716
  22. x 1.060074 -0.109716
  23. zero y 1.271532 0.713416
  24. x 1.271532 0.713416
  25. # aligning
  26. In [85]: df_aligned, df2_aligned = df.align(df2, level=0)
  27. In [86]: df_aligned
  28. Out[86]:
  29. 0 1
  30. one y 1.519970 -0.493662
  31. x 0.600178 0.274230
  32. zero y 0.132885 -0.023688
  33. x 2.410179 1.450520
  34. In [87]: df2_aligned
  35. Out[87]:
  36. 0 1
  37. one y 1.060074 -0.109716
  38. x 1.060074 -0.109716
  39. zero y 1.271532 0.713416
  40. x 1.271532 0.713416

使用swaplevel来交换层级

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

  1. In [88]: df[:5]
  2. Out[88]:
  3. 0 1
  4. one y 1.519970 -0.493662
  5. x 0.600178 0.274230
  6. zero y 0.132885 -0.023688
  7. x 2.410179 1.450520
  8. In [89]: df[:5].swaplevel(0, 1, axis=0)
  9. Out[89]:
  10. 0 1
  11. y one 1.519970 -0.493662
  12. x one 0.600178 0.274230
  13. y zero 0.132885 -0.023688
  14. x zero 2.410179 1.450520

使用reorder_levels来进行层级重排序

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

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

索引多层索引进行重命名

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

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

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

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

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

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

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

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

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

多索引进行排序

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

  1. In [95]: import random
  2. In [96]: random.shuffle(tuples)
  3. In [97]: s = pd.Series(np.random.randn(8), index=pd.MultiIndex.from_tuples(tuples))
  4. In [98]: s
  5. Out[98]:
  6. baz one 0.206053
  7. foo two -0.251905
  8. qux one -2.213588
  9. foo one 1.063327
  10. bar two 1.266143
  11. baz two 0.299368
  12. bar one -0.863838
  13. qux two 0.408204
  14. dtype: float64
  15. In [99]: s.sort_index()
  16. Out[99]:
  17. bar one -0.863838
  18. two 1.266143
  19. baz one 0.206053
  20. two 0.299368
  21. foo one 1.063327
  22. two -0.251905
  23. qux one -2.213588
  24. two 0.408204
  25. dtype: float64
  26. In [100]: s.sort_index(level=0)
  27. Out[100]:
  28. bar one -0.863838
  29. two 1.266143
  30. baz one 0.206053
  31. two 0.299368
  32. foo one 1.063327
  33. two -0.251905
  34. qux one -2.213588
  35. two 0.408204
  36. dtype: float64
  37. In [101]: s.sort_index(level=1)
  38. Out[101]:
  39. bar one -0.863838
  40. baz one 0.206053
  41. foo one 1.063327
  42. qux one -2.213588
  43. bar two 1.266143
  44. baz two 0.299368
  45. foo two -0.251905
  46. qux two 0.408204
  47. dtype: float64

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

  1. In [102]: s.index.set_names(['L1', 'L2'], inplace=True)
  2. In [103]: s.sort_index(level='L1')
  3. Out[103]:
  4. L1 L2
  5. bar one -0.863838
  6. two 1.266143
  7. baz one 0.206053
  8. two 0.299368
  9. foo one 1.063327
  10. two -0.251905
  11. qux one -2.213588
  12. two 0.408204
  13. dtype: float64
  14. In [104]: s.sort_index(level='L2')
  15. Out[104]:
  16. L1 L2
  17. bar one -0.863838
  18. baz one 0.206053
  19. foo one 1.063327
  20. qux one -2.213588
  21. bar two 1.266143
  22. baz two 0.299368
  23. foo two -0.251905
  24. qux two 0.408204
  25. dtype: float64

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

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

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

  1. In [106]: dfm = pd.DataFrame({'jim': [0, 0, 1, 1],
  2. .....: 'joe': ['x', 'x', 'z', 'y'],
  3. .....: 'jolie': np.random.rand(4)})
  4. .....:
  5. In [107]: dfm = dfm.set_index(['jim', 'joe'])
  6. In [108]: dfm
  7. Out[108]:
  8. jolie
  9. jim joe
  10. 0 x 0.490671
  11. x 0.120248
  12. 1 z 0.537020
  13. y 0.110968
  1. In [4]: dfm.loc[(1, 'z')]
  2. PerformanceWarning: indexing past lexsort depth may impact performance.
  3. Out[4]:
  4. jolie
  5. jim joe
  6. 1 z 0.64094

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

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

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

  1. In [109]: dfm.index.is_lexsorted()
  2. Out[109]: False
  3. In [110]: dfm.index.lexsort_depth
  4. Out[110]: 1
  1. In [111]: dfm = dfm.sort_index()
  2. In [112]: dfm
  3. Out[112]:
  4. jolie
  5. jim joe
  6. 0 x 0.490671
  7. x 0.120248
  8. 1 y 0.110968
  9. z 0.537020
  10. In [113]: dfm.index.is_lexsorted()
  11. Out[113]: True
  12. In [114]: dfm.index.lexsort_depth
  13. Out[114]: 2

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

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

Take方法

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

  1. In [116]: index = pd.Index(np.random.randint(0, 1000, 10))
  2. In [117]: index
  3. Out[117]: Int64Index([214, 502, 712, 567, 786, 175, 993, 133, 758, 329], dtype='int64')
  4. In [118]: positions = [0, 9, 3]
  5. In [119]: index[positions]
  6. Out[119]: Int64Index([214, 329, 567], dtype='int64')
  7. In [120]: index.take(positions)
  8. Out[120]: Int64Index([214, 329, 567], dtype='int64')
  9. In [121]: ser = pd.Series(np.random.randn(10))
  10. In [122]: ser.iloc[positions]
  11. Out[122]:
  12. 0 -0.179666
  13. 9 1.824375
  14. 3 0.392149
  15. dtype: float64
  16. In [123]: ser.take(positions)
  17. Out[123]:
  18. 0 -0.179666
  19. 9 1.824375
  20. 3 0.392149
  21. dtype: float64

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

  1. In [124]: frm = pd.DataFrame(np.random.randn(5, 3))
  2. In [125]: frm.take([1, 4, 3])
  3. Out[125]:
  4. 0 1 2
  5. 1 -1.237881 0.106854 -1.276829
  6. 4 0.629675 -1.425966 1.857704
  7. 3 0.979542 -1.633678 0.615855
  8. In [126]: frm.take([0, 2], axis=1)
  9. Out[126]:
  10. 0 2
  11. 0 0.595974 0.601544
  12. 1 -1.237881 -1.276829
  13. 2 -0.767101 1.499591
  14. 3 0.979542 0.615855
  15. 4 0.629675 1.857704

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

  1. In [127]: arr = np.random.randn(10)
  2. In [128]: arr.take([False, False, True, True])
  3. Out[128]: array([-1.1935, -1.1935, 0.6775, 0.6775])
  4. In [129]: arr[[0, 1]]
  5. Out[129]: array([-1.1935, 0.6775])
  6. In [130]: ser = pd.Series(np.random.randn(10))
  7. In [131]: ser.take([False, False, True, True])
  8. Out[131]:
  9. 0 0.233141
  10. 0 0.233141
  11. 1 -0.223540
  12. 1 -0.223540
  13. dtype: float64
  14. In [132]: ser.iloc[[0, 1]]
  15. Out[132]:
  16. 0 0.233141
  17. 1 -0.223540
  18. dtype: float64

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

  1. In [133]: arr = np.random.randn(10000, 5)
  2. In [134]: indexer = np.arange(10000)
  3. In [135]: random.shuffle(indexer)
  4. In [136]: %timeit arr[indexer]
  5. .....: %timeit arr.take(indexer, axis=0)
  6. .....:
  7. 152 us +- 988 ns per loop (mean +- std. dev. of 7 runs, 10000 loops each)
  8. 41.7 us +- 204 ns per loop (mean +- std. dev. of 7 runs, 10000 loops each)
  1. In [137]: ser = pd.Series(arr[:, 0])
  2. In [138]: %timeit ser.iloc[indexer]
  3. .....: %timeit ser.take(indexer)
  4. .....:
  5. 120 us +- 1.05 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
  6. 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分类索引 这种索引类型非常适合有重复的索引。这是一个围绕 Categorical 而创建的容器。这可以非常高效地存储和索引的具有大量重复元素的索引。

  1. In [139]: from pandas.api.types import CategoricalDtype
  2. In [140]: df = pd.DataFrame({'A': np.arange(6),
  3. .....: 'B': list('aabbca')})
  4. .....:
  5. In [141]: df['B'] = df['B'].astype(CategoricalDtype(list('cab')))
  6. In [142]: df
  7. Out[142]:
  8. A B
  9. 0 0 a
  10. 1 1 a
  11. 2 2 b
  12. 3 3 b
  13. 4 4 c
  14. 5 5 a
  15. In [143]: df.dtypes
  16. Out[143]:
  17. A int64
  18. B category
  19. dtype: object
  20. In [144]: df.B.cat.categories
  21. Out[144]: Index(['c', 'a', 'b'], dtype='object')

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

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

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

  1. In [147]: df2.loc['a']
  2. Out[147]:
  3. A
  4. B
  5. a 0
  6. a 1
  7. a 5

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

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

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

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

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

  1. In [150]: df2.groupby(level=0).sum()
  2. Out[150]:
  3. A
  4. B
  5. c 4
  6. a 6
  7. b 5
  8. In [151]: df2.groupby(level=0).sum().index
  9. Out[151]: CategoricalIndex(['c', 'a', 'b'], categories=['c', 'a', 'b'], ordered=False, name='B', dtype='category')

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

  1. In [152]: df2.reindex(['a', 'e'])
  2. Out[152]:
  3. A
  4. B
  5. a 0.0
  6. a 1.0
  7. a 5.0
  8. e NaN
  9. In [153]: df2.reindex(['a', 'e']).index
  10. Out[153]: Index(['a', 'a', 'a', 'e'], dtype='object', name='B')
  11. In [154]: df2.reindex(pd.Categorical(['a', 'e'], categories=list('abcde')))
  12. Out[154]:
  13. A
  14. B
  15. a 0.0
  16. a 1.0
  17. a 5.0
  18. e NaN
  19. In [155]: df2.reindex(pd.Categorical(['a', 'e'], categories=list('abcde'))).index
  20. Out[155]: CategoricalIndex(['a', 'a', 'a', 'e'], categories=['a', 'b', 'c', 'd', 'e'], ordered=False, name='B', dtype='category')

::: danger 警告

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

  1. In [9]: df3 = pd.DataFrame({'A': np.arange(6), 'B': pd.Series(list('aabbca')).astype('category')})
  2. In [11]: df3 = df3.set_index('B')
  3. In [11]: df3.index
  4. Out[11]: CategoricalIndex(['a', 'a', 'b', 'b', 'c', 'a'], categories=['a', 'b', 'c'], ordered=False, name='B', dtype='category')
  5. In [12]: pd.concat([df2, df3])
  6. TypeError: categories must match existing categories when appending

:::

64位整型索引和范围索引

::: danger 警告

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

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

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

64位浮点索引

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

  1. In [156]: indexf = pd.Index([1.5, 2, 3, 4.5, 5])
  2. In [157]: indexf
  3. Out[157]: Float64Index([1.5, 2.0, 3.0, 4.5, 5.0], dtype='float64')
  4. In [158]: sf = pd.Series(range(5), index=indexf)
  5. In [159]: sf
  6. Out[159]:
  7. 1.5 0
  8. 2.0 1
  9. 3.0 2
  10. 4.5 3
  11. 5.0 4
  12. dtype: int64

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

  1. In [160]: sf[3]
  2. Out[160]: 2
  3. In [161]: sf[3.0]
  4. Out[161]: 2
  5. In [162]: sf.loc[3]
  6. Out[162]: 2
  7. In [163]: sf.loc[3.0]
  8. Out[163]: 2

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

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

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

  1. In [165]: sf[2:4]
  2. Out[165]:
  3. 2.0 1
  4. 3.0 2
  5. dtype: int64
  6. In [166]: sf.loc[2:4]
  7. Out[166]:
  8. 2.0 1
  9. 3.0 2
  10. dtype: int64
  11. In [167]: sf.iloc[2:4]
  12. Out[167]:
  13. 3.0 2
  14. 4.5 3
  15. dtype: int64

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

  1. In [168]: sf[2.1:4.6]
  2. Out[168]:
  3. 3.0 2
  4. 4.5 3
  5. dtype: int64
  6. In [169]: sf.loc[2.1:4.6]
  7. Out[169]:
  8. 3.0 2
  9. 4.5 3
  10. dtype: int64

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

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

::: danger 警告

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

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

:::

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

  1. In [170]: dfir = pd.concat([pd.DataFrame(np.random.randn(5, 2),
  2. .....: index=np.arange(5) * 250.0,
  3. .....: columns=list('AB')),
  4. .....: pd.DataFrame(np.random.randn(6, 2),
  5. .....: index=np.arange(4, 10) * 250.1,
  6. .....: columns=list('AB'))])
  7. .....:
  8. In [171]: dfir
  9. Out[171]:
  10. A B
  11. 0.0 -0.435772 -1.188928
  12. 250.0 -0.808286 -0.284634
  13. 500.0 -1.815703 1.347213
  14. 750.0 -0.243487 0.514704
  15. 1000.0 1.162969 -0.287725
  16. 1000.4 -0.179734 0.993962
  17. 1250.5 -0.212673 0.909872
  18. 1500.6 -0.733333 -0.349893
  19. 1750.7 0.456434 -0.306735
  20. 2000.8 0.553396 0.166221
  21. 2250.9 -0.101684 -0.734907

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

  1. In [172]: dfir[0:1000.4]
  2. Out[172]:
  3. A B
  4. 0.0 -0.435772 -1.188928
  5. 250.0 -0.808286 -0.284634
  6. 500.0 -1.815703 1.347213
  7. 750.0 -0.243487 0.514704
  8. 1000.0 1.162969 -0.287725
  9. 1000.4 -0.179734 0.993962
  10. In [173]: dfir.loc[0:1001, 'A']
  11. Out[173]:
  12. 0.0 -0.435772
  13. 250.0 -0.808286
  14. 500.0 -1.815703
  15. 750.0 -0.243487
  16. 1000.0 1.162969
  17. 1000.4 -0.179734
  18. Name: A, dtype: float64
  19. In [174]: dfir.loc[1000.4]
  20. Out[174]:
  21. A -0.179734
  22. B 0.993962
  23. Name: 1000.4, dtype: float64

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

  1. In [175]: dfir[0:1000]
  2. Out[175]:
  3. A B
  4. 0.0 -0.435772 -1.188928
  5. 250.0 -0.808286 -0.284634
  6. 500.0 -1.815703 1.347213
  7. 750.0 -0.243487 0.514704
  8. 1000.0 1.162969 -0.287725

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

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

间隔索引

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

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

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

  1. In [177]: df = pd.DataFrame({'A': [1, 2, 3, 4]},
  2. .....: index=pd.IntervalIndex.from_breaks([0, 1, 2, 3, 4]))
  3. .....:
  4. In [178]: df
  5. Out[178]:
  6. A
  7. (0, 1] 1
  8. (1, 2] 2
  9. (2, 3] 3
  10. (3, 4] 4

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

  1. In [179]: df.loc[2]
  2. Out[179]:
  3. A 2
  4. Name: (1, 2], dtype: int64
  5. In [180]: df.loc[[2, 3]]
  6. Out[180]:
  7. A
  8. (1, 2] 2
  9. (2, 3] 3

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

  1. In [181]: df.loc[2.5]
  2. Out[181]:
  3. A 3
  4. Name: (2, 3], dtype: int64
  5. In [182]: df.loc[[2.5, 3.5]]
  6. Out[182]:
  7. A
  8. (2, 3] 3
  9. (3, 4] 4

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

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

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

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

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

  1. In [184]: idxr = df.index.overlaps(pd.Interval(0.5, 2.5))
  2. In [185]: idxr
  3. Out[185]: array([ True, True, True, False])
  4. In [186]: df[idxr]
  5. Out[186]:
  6. A
  7. (0, 1] 1
  8. (1, 2] 2
  9. (2, 3] 3

使用 cutqcut来为数据分块

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

  1. In [187]: c = pd.cut(range(4), bins=2)
  2. In [188]: c
  3. Out[188]:
  4. [(-0.003, 1.5], (-0.003, 1.5], (1.5, 3.0], (1.5, 3.0]]
  5. Categories (2, interval[float64]): [(-0.003, 1.5] < (1.5, 3.0]]
  6. In [189]: c.categories
  7. Out[189]:
  8. IntervalIndex([(-0.003, 1.5], (1.5, 3.0]],
  9. closed='right',
  10. dtype='interval[float64]')

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

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

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

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

生成一定区间内的间隔

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

  1. In [191]: pd.interval_range(start=0, end=5)
  2. Out[191]:
  3. IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]],
  4. closed='right',
  5. dtype='interval[int64]')
  6. In [192]: pd.interval_range(start=pd.Timestamp('2017-01-01'), periods=4)
  7. Out[192]:
  8. 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]],
  9. closed='right',
  10. dtype='interval[datetime64[ns]]')
  11. In [193]: pd.interval_range(end=pd.Timedelta('3 days'), periods=3)
  12. Out[193]:
  13. 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]],
  14. closed='right',
  15. dtype='interval[timedelta64[ns]]')

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

  1. In [194]: pd.interval_range(start=0, periods=5, freq=1.5)
  2. Out[194]:
  3. IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0], (6.0, 7.5]],
  4. closed='right',
  5. dtype='interval[float64]')
  6. In [195]: pd.interval_range(start=pd.Timestamp('2017-01-01'), periods=4, freq='W')
  7. Out[195]:
  8. 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]],
  9. closed='right',
  10. dtype='interval[datetime64[ns]]')
  11. In [196]: pd.interval_range(start=pd.Timedelta('0 days'), periods=3, freq='9H')
  12. Out[196]:
  13. 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]],
  14. closed='right',
  15. dtype='interval[timedelta64[ns]]')

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

  1. In [197]: pd.interval_range(start=0, end=4, closed='both')
  2. Out[197]:
  3. IntervalIndex([[0, 1], [1, 2], [2, 3], [3, 4]],
  4. closed='both',
  5. dtype='interval[int64]')
  6. In [198]: pd.interval_range(start=0, end=4, closed='neither')
  7. Out[198]:
  8. IntervalIndex([(0, 1), (1, 2), (2, 3), (3, 4)],
  9. closed='neither',
  10. dtype='interval[int64]')

v0.23.0新加入

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

  1. In [199]: pd.interval_range(start=0, end=6, periods=4)
  2. Out[199]:
  3. IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]],
  4. closed='right',
  5. dtype='interval[float64]')
  6. In [200]: pd.interval_range(pd.Timestamp('2018-01-01'),
  7. .....: pd.Timestamp('2018-02-28'), periods=3)
  8. .....:
  9. Out[200]:
  10. 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]],
  11. closed='right',
  12. dtype='interval[datetime64[ns]]')

其他索引常见问题

数值索引

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

  1. In [201]: s = pd.Series(range(5))
  2. In [202]: s[-1]
  3. ---------------------------------------------------------------------------
  4. KeyError Traceback (most recent call last)
  5. <ipython-input-202-76c3dce40054> in <module>
  6. ----> 1 s[-1]
  7. /pandas/pandas/core/series.py in __getitem__(self, key)
  8. 1062 key = com.apply_if_callable(key, self)
  9. 1063 try:
  10. -> 1064 result = self.index.get_value(self, key)
  11. 1065
  12. 1066 if not is_scalar(result):
  13. /pandas/pandas/core/indexes/base.py in get_value(self, series, key)
  14. 4721 k = self._convert_scalar_indexer(k, kind="getitem")
  15. 4722 try:
  16. -> 4723 return self._engine.get_value(s, k, tz=getattr(series.dtype, "tz", None))
  17. 4724 except KeyError as e1:
  18. 4725 if len(self) > 0 and (self.holds_integer() or self.is_boolean()):
  19. /pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()
  20. /pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()
  21. /pandas/pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()
  22. /pandas/pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()
  23. /pandas/pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()
  24. KeyError: -1
  25. In [203]: df = pd.DataFrame(np.random.randn(5, 4))
  26. In [204]: df
  27. Out[204]:
  28. 0 1 2 3
  29. 0 -0.130121 -0.476046 0.759104 0.213379
  30. 1 -0.082641 0.448008 0.656420 -1.051443
  31. 2 0.594956 -0.151360 -0.069303 1.221431
  32. 3 -0.182832 0.791235 0.042745 2.069775
  33. 4 1.446552 0.019814 -1.389212 -0.702312
  34. In [205]: df.loc[-2:]
  35. Out[205]:
  36. 0 1 2 3
  37. 0 -0.130121 -0.476046 0.759104 0.213379
  38. 1 -0.082641 0.448008 0.656420 -1.051443
  39. 2 0.594956 -0.151360 -0.069303 1.221431
  40. 3 -0.182832 0.791235 0.042745 2.069775
  41. 4 1.446552 0.019814 -1.389212 -0.702312

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

非单调索引要求严格匹配

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

  1. In [206]: df = pd.DataFrame(index=[2, 3, 3, 4, 5], columns=['data'], data=list(range(5)))
  2. In [207]: df.index.is_monotonic_increasing
  3. Out[207]: True
  4. # no rows 0 or 1, but still returns rows 2, 3 (both of them), and 4:
  5. In [208]: df.loc[0:4, :]
  6. Out[208]:
  7. data
  8. 2 0
  9. 3 1
  10. 3 2
  11. 4 3
  12. # slice is are outside the index, so empty DataFrame is returned
  13. In [209]: df.loc[13:15, :]
  14. Out[209]:
  15. Empty DataFrame
  16. Columns: [data]
  17. Index: []

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

  1. In [210]: df = pd.DataFrame(index=[2, 3, 1, 4, 3, 5],
  2. .....: columns=['data'], data=list(range(6)))
  3. .....:
  4. In [211]: df.index.is_monotonic_increasing
  5. Out[211]: False
  6. # OK because 2 and 4 are in the index
  7. In [212]: df.loc[2:4, :]
  8. Out[212]:
  9. data
  10. 2 0
  11. 3 1
  12. 1 2
  13. 4 3
  1. # 0 is not in the index
  2. In [9]: df.loc[0:4, :]
  3. KeyError: 0
  4. # 3 is not a unique label
  5. In [11]: df.loc[2:3, :]
  6. KeyError: 'Cannot get right slice bound for non-unique label: 3'

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

  1. In [213]: weakly_monotonic = pd.Index(['a', 'b', 'c', 'c'])
  2. In [214]: weakly_monotonic
  3. Out[214]: Index(['a', 'b', 'c', 'c'], dtype='object')
  4. In [215]: weakly_monotonic.is_monotonic_increasing
  5. Out[215]: True
  6. In [216]: weakly_monotonic.is_monotonic_increasing & weakly_monotonic.is_unique
  7. Out[216]: False

终止点被包含在内

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

  1. In [217]: s = pd.Series(np.random.randn(6), index=list('abcdef'))
  2. In [218]: s
  3. Out[218]:
  4. a 0.301379
  5. b 1.240445
  6. c -0.846068
  7. d -0.043312
  8. e -1.658747
  9. f -0.819549
  10. dtype: float64

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

  1. In [219]: s[2:5]
  2. Out[219]:
  3. c -0.846068
  4. d -0.043312
  5. e -1.658747
  6. dtype: float64

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

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

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

  1. In [220]: s.loc['c':'e']
  2. Out[220]:
  3. c -0.846068
  4. d -0.043312
  5. e -1.658747
  6. dtype: float64

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

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

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

  1. In [221]: series1 = pd.Series([1, 2, 3])
  2. In [222]: series1.dtype
  3. Out[222]: dtype('int64')
  4. In [223]: res = series1.reindex([0, 4])
  5. In [224]: res.dtype
  6. Out[224]: dtype('float64')
  7. In [225]: res
  8. Out[225]:
  9. 0 1.0
  10. 4 NaN
  11. dtype: float64
  1. In [226]: series2 = pd.Series([True])
  2. In [227]: series2.dtype
  3. Out[227]: dtype('bool')
  4. In [228]: res = series2.reindex_like(series1)
  5. In [229]: res.dtype
  6. Out[229]: dtype('O')
  7. In [230]: res
  8. Out[230]:
  9. 0 True
  10. 1 NaN
  11. 2 NaN
  12. dtype: object

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

参见 this old issue了解更详细的讨论过程