熊猫的MultiIndex有哪些好处?

43

我发现即使没有使用MultiIndex,也可以使用DataFrame.groupby进行子采样/横截面。

另一方面,当DataFrame具有MultiIndex时,仍需使用DataFrame.groupby进行子采样/横截面。

那么除了在打印时显示层次结构的帮助和美观之外,MultiIndex还有哪些好处呢?


4
除了效益之外,我很想了解使用MultiIndex的计算成本或其他成本类型。 - Sarah
4个回答

70

“分层索引”(也称为“多级”索引)是在pandas 0.4版本中引入的。这为进行高维数据处理和操作打开了一些相当复杂的可能性。本质上,它使您能够在二维表格结构(DataFrame)中有效地存储和操作任意高维数据,例如。例如,可以使用MultiIndex构建一个数据帧,如下所示:

import pandas as pd
import numpy as np

np.arrays = [['one','one','one','two','two','two'],[1,2,3,1,2,3]]

df = pd.DataFrame(np.random.randn(6,2),index=pd.MultiIndex.from_tuples(list(zip(*np.arrays))),columns=['A','B'])

df  # This is the dataframe we have generated

          A         B
one 1 -0.732470 -0.313871
    2 -0.031109 -2.068794
    3  1.520652  0.471764
two 1 -0.101713 -1.204458
    2  0.958008 -0.455419
    3 -0.191702 -0.915983

这个 df 只是一个由两个维度构成的数据结构

df.ndim

2

但我们可以将它想象成一个三维数据结构,通过输出来观察。

  • one 是一行数据,包含 1 和数据 -0.732470 -0.313871
  • one 是另一行数据,包含 2 和数据 -0.031109 -2.068794
  • one 是第三行数据,包含 3 和数据 1.520652 0.471764

也称为:"在二维表格结构中有效地存储和操作任意高维数据"。

这不仅是一个“漂亮的展示”,而且还有易于检索数据的好处,因为我们现在拥有了一个分层索引。

例如:

In [44]: df.ix["one"]
Out[44]: 
          A         B
1 -0.732470 -0.313871
2 -0.031109 -2.068794
3  1.520652  0.471764

将为我们提供一个仅包含属于“one”组的数据的新数据框。

我们可以通过执行以下操作进一步缩小数据选择范围:

In [45]: df.ix["one"].ix[1]
Out[45]: 
A   -0.732470
B   -0.313871
Name: 1

当然,如果我们想要一个特定的值,这里有一个例子:

In [46]: df.ix["one"].ix[1]["A"]
Out[46]: -0.73247029752040727
所以如果我们有更多的索引(除了上面示例中显示的2个索引),我们实际上可以深入挖掘并选择我们真正感兴趣的数据集,而无需使用groupby。我们甚至可以从我们的数据框中获取交叉部分(行或列)... 通过行:-
In [47]: df.xs('one')
Out[47]: 
          A         B
1 -0.732470 -0.313871
2 -0.031109 -2.068794
3  1.520652  0.471764

按列排序:

In [48]: df.xs('B', axis=1)
Out[48]: 
one  1   -0.313871
     2   -2.068794
     3    0.471764
two  1   -1.204458
     2   -0.455419
     3   -0.915983
Name: B

6
在你的第一个三维数据结构描述中,那三组数据(one with 1 with data -0.790620 0.229276...)似乎与你实际列出示例中的任何数据都不对应。请问是否需要更正? - Gerrat
使用 MultiIndex.from_tuples(list(zip(*np.arrays))) 的特定原因是什么,当 from_arrays 方法可用时呢? df = pd.DataFrame(np.random.randn(6,2), index=pd.MultiIndex.from_arrays(np.arrays),columns=['A','B']) - Luca P.
4
一篇不错的文章,但我认为它没有很好地回答为什么多索引很有用。这里提到的每个操作都可以在带有列“A”和“B”的数据框上执行,而且额外的工作量也很小。 - Ben

16

Calvin Cheng写了一篇很好的文章,但我也想试着解释一下。

何时使用MultiIndex:

  1. 当单个列的值不足以唯一标识一行时。
  2. 当数据在逻辑上是分层的,即具有多个维度或“级别”。

为什么(你的核心问题)- 至少这些是我认为最大的优点:

  1. 通过stack()和unstack()轻松操作
  2. 当存在多个列级别时,容易进行数学计算
  3. 用于切片/过滤的语法糖

例子:

                                                       Dollars  Units
Date       Store   Category Subcategory UPC EAN
2018-07-10 Store 1 Alcohol  Liqour      80480280024    154.77      7
           Store 2 Alcohol  Liqour      80480280024     82.08      4
           Store 3 Alcohol  Liqour      80480280024    259.38      9
           Store 1 Alcohol  Liquor      80432400630    477.68     14
                                        674545000001   139.68      4
           Store 2 Alcohol  Liquor      80432400630    203.88      6
                                        674545000001   377.13     13
           Store 3 Alcohol  Liquor      80432400630    239.19      7
                                        674545000001   432.32     14
           Store 1 Beer     Ales        94922755711     65.17      7
                                        702770082018   174.44     14
                                        736920111112    50.70      5
           Store 2 Beer     Ales        94922755711    129.60     12
                                        702770082018   107.40     10
                                        736920111112    59.65      5
           Store 3 Beer     Ales        94922755711    154.00     14
                                        702770082018   137.40     10
                                        736920111112   107.88     12
           Store 1 Beer     Lagers      702770081011   156.24     12
           Store 2 Beer     Lagers      702770081011   137.06     11
           Store 3 Beer     Lagers      702770081011   119.52      8    

1) 如果我们想要轻松比较各个商店的销售情况,可以使用 df.unstack('Store') 将所有内容排列在一起:

                                             Dollars                   Units
Store                                        Store 1 Store 2 Store 3 Store 1 Store 2 Store 3
Date       Category Subcategory UPC EAN
2018-07-10 Alcohol  Liqour      80480280024   154.77   82.08  259.38       7       4       9
                    Liquor      80432400630   477.68  203.88  239.19      14       6       7
                                674545000001  139.68  377.13  432.32       4      13      14
           Beer     Ales        94922755711    65.17  129.60  154.00       7      12      14
                                702770082018  174.44  107.40  137.40      14      10      10
                                736920111112   50.70   59.65  107.88       5       5      12
                    Lagers      702770081011  156.24  137.06  119.52      12      11       8

2) 我们还可以轻松地对多列进行数学运算。例如,df ['Dollars'] / df ['Units'] 将为每个商店分别将其美元除以其单位,对于没有多个操作的每个商店:

Store                                         Store 1  Store 2  Store 3
Date       Category Subcategory UPC EAN
2018-07-10 Alcohol  Liqour      80480280024     22.11    20.52    28.82
                    Liquor      80432400630     34.12    33.98    34.17
                                674545000001    34.92    29.01    30.88
           Beer     Ales        94922755711      9.31    10.80    11.00
                                702770082018    12.46    10.74    13.74
                                736920111112    10.14    11.93     8.99
                    Lagers      702770081011    13.02    12.46    14.94

3) 如果我们想要过滤特定的行,而不是使用 WHERE 子句,可以使用
df[(df[col1] == val1) and (df[col2] == val2) and (df[col3] == val3)]

使用 .xs 或 .query(是的,这些都适用于常规数据框,但并不是特别有用),我们可以更改格式。语法如下:

df.xs((val1, val2, val3), level=(col1, col2, col3))

更多示例可以在我整理的教程笔记本中找到。


2

使用Dataframe的多列存储数据是一个替代使用MultiIndex的方法。通常情况下,人们会期待使用MultiIndex可以提供比朴素的列存储更好的性能表现,但是在Pandas v1.1.4中似乎并非如此。

时间

import numpy as np
import pandas as pd

np.random.seed(2020)
inv = pd.DataFrame({
    'store_id': np.random.choice(10000, size=10**7),
    'product_id': np.random.choice(1000, size=10**7),
    'stock': np.random.choice(100, size=10**7),
})

# Create a DataFrame with a multiindex
inv_multi = inv.groupby(['store_id', 'product_id'])[['stock']].agg('sum')
print(inv_multi)
                     stock
store_id product_id       
0        2              48
         4              18
         5              58
         7             149
         8             158
...                    ...
9999     992           132
         995           121
         996           105
         998            99
         999            16

[6321869 rows x 1 columns]

# Create a DataFrame without a multiindex
inv_cols = inv_multi.reset_index()
print(inv_cols)
         store_id  product_id  stock
0               0           2     48
1               0           4     18
2               0           5     58
3               0           7    149
4               0           8    158
...           ...         ...    ...
6321864      9999         992    132
6321865      9999         995    121
6321866      9999         996    105
6321867      9999         998     99
6321868      9999         999     16

[6321869 rows x 3 columns]

%%timeit
inv_multi.xs(key=100, level='store_id')
10 loops, best of 3: 20.2 ms per loop

%%timeit
inv_cols.loc[inv_cols.store_id == 100]
The slowest run took 8.79 times longer than the fastest. This could mean that an intermediate result is being cached.
100 loops, best of 3: 11.5 ms per loop

%%timeit
inv_multi.xs(key=100, level='product_id')
100 loops, best of 3: 9.08 ms per loop

%%timeit
inv_cols.loc[inv_cols.product_id == 100]
100 loops, best of 3: 12.2 ms per loop

%%timeit
inv_multi.xs(key=(100, 100), level=('store_id', 'product_id'))
10 loops, best of 3: 29.8 ms per loop

%%timeit
inv_cols.loc[(inv_cols.store_id == 100) & (inv_cols.product_id == 100)]
10 loops, best of 3: 28.8 ms per loop

结论

使用MultiIndex的好处在于语法糖、自我记录数据以及像@ZaxR所提到的unstack()函数中的小便利;性能不是一个好处,这似乎是一个真正的错过机会。


4
观察速度方面,熊猫索引(单个或多个)的优点只有在索引是唯一的时才能看到;在你的测试中,你选择了一个不唯一的单级别... 如果你跨越索引进行选择,比如选择产品ID为100和店铺ID为100,你将看到额外的速度优势。这个SO链接可以提供更多信息。查看熊猫源代码也可以提供更多信息。 - sammywemmy

1

根据这个答案的评论,似乎实验存在缺陷。下面是我尝试进行正确实验的结果。

时间

import pandas as pd
import numpy as np
from timeit import timeit


random_data = np.random.randn(16, 4)

multiindex_lists = [["A", "B", "C", "D"], [1, 2, 3, 4]]
multiindex = pd.MultiIndex.from_product(multiindex_lists)

dfm = pd.DataFrame(random_data, multiindex)
df = dfm.reset_index()
print("dfm:\n", dfm, "\n")
print("df\n", df, "\n")

dfm_selection = dfm.loc[("B", 4), 3]
print("dfm_selection:", dfm_selection, type(dfm_selection))

df_selection = df[(df["level_0"] == "B") & (df["level_1"] == 4)][3].iat[0]
print("df_selection: ", df_selection, type(df_selection), "\n")

print("dfm_selection timeit:",
      timeit(lambda: dfm.loc[("B", 4), 3], number=int(1e6)))
print("df_selection timeit: ",
      timeit(
          lambda: df[(df["level_0"] == "B") & (df["level_1"] == 4)][3].iat[0],
          number=int(1e6)))

dfm:
             0         1         2         3
A 1 -1.055128 -0.845019 -2.853027  0.521738
  2  0.397804  0.385045 -0.121294 -0.696215
  3 -0.551836 -0.666953 -0.956578  1.929732
  4 -0.154780  1.778150  0.183104 -0.013989
B 1 -0.315476  0.564419  0.492496 -1.052432
  2 -0.695300  0.085265  0.701724 -0.974168
  3 -0.879915 -0.206499  1.597701  1.294885
  4  0.653261  0.279641 -0.800613  1.050241
C 1  1.004199 -1.377520 -0.672913  1.491793
  2 -0.453452  0.367264 -0.002362  0.411193
  3  2.271958  0.240864 -0.923934 -0.572957
  4  0.737893 -0.523488  0.485497 -2.371977
D 1  1.133661 -0.584973 -0.713320 -0.656315
  2 -1.173231 -0.490667  0.634677  1.711015
  3 -0.050371 -0.175644  0.124797  0.703672
  4  1.349595  0.122202 -1.498178  0.013391

df
    level_0  level_1         0         1         2         3
0        A        1 -1.055128 -0.845019 -2.853027  0.521738
1        A        2  0.397804  0.385045 -0.121294 -0.696215
2        A        3 -0.551836 -0.666953 -0.956578  1.929732
3        A        4 -0.154780  1.778150  0.183104 -0.013989
4        B        1 -0.315476  0.564419  0.492496 -1.052432
5        B        2 -0.695300  0.085265  0.701724 -0.974168
6        B        3 -0.879915 -0.206499  1.597701  1.294885
7        B        4  0.653261  0.279641 -0.800613  1.050241
8        C        1  1.004199 -1.377520 -0.672913  1.491793
9        C        2 -0.453452  0.367264 -0.002362  0.411193
10       C        3  2.271958  0.240864 -0.923934 -0.572957
11       C        4  0.737893 -0.523488  0.485497 -2.371977
12       D        1  1.133661 -0.584973 -0.713320 -0.656315
13       D        2 -1.173231 -0.490667  0.634677  1.711015
14       D        3 -0.050371 -0.175644  0.124797  0.703672
15       D        4  1.349595  0.122202 -1.498178  0.013391 

dfm_selection: 1.0502406808918188 <class 'numpy.float64'>
df_selection:  1.0502406808918188 <class 'numpy.float64'> 

dfm_selection timeit: 63.92458086000079
df_selection timeit:  450.4555013199997

结论

使用MultiIndex进行单值检索比传统的数据框单值检索快7倍以上。

MultiIndex检索的语法更加简洁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接