在 MultiIndex pandas 数据帧上使用多维索引?

Use a multidimensional index on a MultiIndex pandas dataframe?

我有一个多索引 pandas 数据框,看起来像这样(称为 p_z):

                     p_z
entry subentry
0     0         0.338738
      1         0.636035
      2        -0.307365
      3        -0.167779
      4         0.243284
...                  ...
26692 891      -0.459227
      892       0.055993
      893      -0.469857
      894       0.192554
      895       0.155738

[11742280 rows x 1 columns]

我希望能够 select 基于另一个多维数据框(或 numpy 数组)的某些行。它看起来像一个 pandas 数据框(称为 tofpid):

                tofpid
entry subentry
0     0              0
      1              2
      2              4
      3              5
      4              7
...                ...
26692 193          649
      194          670
      195          690
      196          725
      197          737

[2006548 rows x 1 columns]

我也把它当作一个笨拙的数组,它是一个 (26692, ) 数组(每个条目都有一个非标准数量的子条目)。这是一个 selection df/array 告诉 p_z df 要保留哪些行。所以在 p_z 的条目 0 中,它应该保留子条目 0、2、4、5、7 等

我在 pandas 中找不到完成此操作的方法。我是 pandas 的新手,甚至是 multiindex 的新手;但我觉得应该有办法做到这一点。如果它能够更好地广播,我将在大约 1500 个类似大小的数据帧上进行广播。如果有帮助,这些数据帧来自使用 uproot 导入的 *.root 文件(如果没有 pandas 有另一种方法可以做到这一点,我会接受;但我很想使用 pandas 来保持事情井井有条)。

编辑:这是一个可重现的例子(由 Jim Pavinski 的回答提供;谢谢!)。

import awkward as ak
import pandas as pd

>>> p_z = ak.Array([[ 0.338738, 0.636035, -0.307365, -0.167779, 0.243284,  
                      0.338738, 0.636035],
                    [-0.459227, 0.055993, -0.469857,  0.192554, 0.155738, 
                     -0.459227]])
>>> p_z = ak.to_pandas(p_z)
>>> tofpid = ak.Array([[0, 2, 4, 5], [1, 2, 4]])
>>> tofpid = ak.to_pandas(tofpid)

这两个数据帧都是在 uproot 中本地生成的,但这将重现与 uproot 相同的数据帧(使用笨拙的库)。

IIUC:

输入数据:

>>> p_z
                     p_z
entry subentry
0     0         0.338738
      1         0.636035
      2        -0.307365
      3        -0.167779
      4         0.243284

>>> tofpid
                tofpid
entry subentry
0     0              0
      1              2
      2              4
      3              5
      4              7

从第二个数据框的列(条目、tofpid)创建一个新的多索引:

mi = pd.MultiIndex.from_frame(tofpid.reset_index(level='subentry', drop=True)
                                    .reset_index())

输出结果:

>>> p_z.loc[mi.intersection(p_z.index)]
              p_z
entry
0     0  0.338738
      2 -0.307365
      4  0.243284

这是一个可重现的示例,具有足够的结构来表示问题(使用 awkward 库):

>>> import awkward as ak
>>> 
>>> p_z = ak.Array([
...     [ 0.338738, 0.636035, -0.307365, -0.167779, 0.243284,  0.338738, 0.636035],
...     [-0.459227, 0.055993, -0.469857,  0.192554, 0.155738, -0.459227],
... ])
>>> p_z
<Array [[0.339, 0.636, ... 0.156, -0.459]] type='2 * var * float64'>
>>> 
>>> tofpid = ak.Array([[0, 2, 4, 5], [1, 2, 4]])
>>> tofpid
<Array [[0, 2, 4, 5], [1, 2, 4]] type='2 * var * int64'>

在Pandas形式中,这是:

>>> df_p_z = ak.to_pandas(p_z)
>>> df_p_z
                  values
entry subentry          
0     0         0.338738
      1         0.636035
      2        -0.307365
      3        -0.167779
      4         0.243284
      5         0.338738
      6         0.636035
1     0        -0.459227
      1         0.055993
      2        -0.469857
      3         0.192554
      4         0.155738
      5        -0.459227
>>> df_tofpid = ak.to_pandas(tofpid)
>>> df_tofpid
                values
entry subentry        
0     0              0
      1              2
      2              4
      3              5
1     0              1
      1              2
      2              4

作为一个Awkward Array,你要做的是slice the first array by the second。也就是说,你想要 p_z[tofpid]:

>>> p_z[tofpid]
<Array [[0.339, -0.307, ... -0.47, 0.156]] type='2 * var * float64'>
>>> p_z[tofpid].tolist()
[[0.338738, -0.307365, 0.243284, 0.338738], [0.055993, -0.469857, 0.155738]]

使用 Pandas,我设法做到了:

>>> df_p_z.loc[df_tofpid.reset_index(level=0).apply(lambda x: tuple(x.values), axis=1).tolist()]
                  values
entry subentry          
0     0         0.338738
      2        -0.307365
      4         0.243284
      5         0.338738
1     1         0.055993
      2        -0.469857
      4         0.155738

这里发生的事情是 df_tofpid.reset_index(level=0) 将 MultiIndex 的 "entry" 部分变成一列,然后 apply 在每一行上执行一个 Python 函数,如果 axis=1,每一行都是x.valuestolist()把结果变成一个元组列表,比如

>>> df_tofpid.reset_index(level=0).apply(lambda x: tuple(x.values), axis=1).tolist()
[(0, 0), (0, 2), (0, 4), (0, 5), (1, 1), (1, 2), (1, 4)]

这就是 loc 从其 MultiIndex 中 select entry/subentry 对所需要的。

我的 Pandas 解决方案有两个缺点:它很复杂,而且它要经过 Python 迭代和对象,它不像数组那样可缩放。 很有可能 Pandas 专家会找到比我更好的解决方案。 关于 Pandas 我不知道的很多。