Pandas:对多级索引数据框应用掩码

4
我有一个 Pandas 数据框,它具有三层的多级列索引:
import itertools
import numpy as np

def mklbl(prefix, n):
    return ["%s%s" % (prefix, i) for i in range(n)]


miindex = pd.MultiIndex.from_product([mklbl('A', 4)])

micolumns = pd.MultiIndex.from_tuples(list(itertools.product(['A', 'B'], ['a', 'b', 'c'], ['foo', 'bar'])),
                                      names=['lvl0', 'lvl1', 'lvl2'])

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

lvl0   A                       B                    
lvl1   a       b       c       a       b       c    
lvl2 bar foo bar foo bar foo bar foo bar foo bar foo
A0     1   0   3   2   5   4   7   6   9   8  11  10
A1    13  12  15  14  17  16  19  18  21  20  23  22
A2    25  24  27  26  29  28  31  30  33  32  35  34
A3    37  36  39  38  41  40  43  42  45  44  47  46

我希望对于这个数据框进行遮蔽,基于另一个数据框,该数据框具有索引的最后两个级别:
cols = micolumns.droplevel(0).unique()
a_mask = pd.DataFrame(np.random.randn(len(dfmi.index), len(cols)), index=dfmi.index, columns=cols)
a_mask = (np.sign(a_mask) > 0).astype(bool)

        a             b             c       
      foo    bar    foo    bar    foo    bar
A0  False  False  False   True   True  False
A1   True  False   True  False   True   True
A2   True   True   True   True  False  False
A3   True  False  False   True   True  False

我希望做的是根据a_mask来掩盖原始数据框。例如,当a_mask为真时,我想将原始条目设置为零。
我尝试使用pd.IndexSlice,但它静默失败(即我可以运行以下代码,但没有效果:
dfmi.loc[:, pd.IndexSlice[:, a_mask]] = 0  #dfmi is unchanged

有什么建议可以实现这个目标吗?

编辑 在我的使用情况下,标签是通过笛卡尔积构建的,因此将存在所有组合的(lev0,lev1,lev2)。 但是有一种情况,即lev0可以假定2个值{A,B},而lev1可以假定3个值{a,b,c}。


那么,dfmi.Adfmi.B的形状是相同的,对吗? - Divakar
是的,在我的情况下这是真的。 - FLab
4个回答

3
我认为使用这种方式更加安全。
dfmi.where(a_mask.loc[:,dfmi.columns.droplevel(0)].values,0)
Out[191]: 
lvl0   A               B            
lvl1   a       b       a       b    
lvl2 bar foo bar foo bar foo bar foo
A0     0   0   0   2   0   0   0   6
A1     9   8  11   0  13  12  15   0
A2     0  16  19  18   0  20  23  22
A3    25   0   0   0  29   0   0   0

啊,droplevel 的使用很好。我之前一直在尝试使用一个空的 slicer,但是没有进展。 - Brad Solomon
谢谢,非常好的答案。 - FLab
@FLab yw~ 开心编程 - BENY

2
我会按照以下方式进行操作:
mask = pd.concat({k: a_mask for k in dfmi.columns.levels[0]}, axis=1)
dfmi.where(~mask, 0)

谢谢,这个解决方案可行,不过在接受之前我想等待其他建议。我建议进行以下改进,使其稍微更加通用。mask = pd.concat(dict.fromkeys(dfmi.columns.get_level_values(0), a_mask), axis=1) dfmi = dfmi.mask(mask, 0) - FLab
这是一个聪明的解决方案,但在您更新的问题中,标签仍然只是“重复”(a、b、c / a、b、c)...如果这种情况不再存在,则连接解决方案将无法工作。而且我仍然认为.loc应该能够在这里以某种方式使用。 - Brad Solomon
例如,如果您删除了 dfmi[('A', 'a', 'foo')],则会导致数据不均匀。 - Brad Solomon
在答案中进行了澄清。 - FLab
@BradSolomon 添加了 .loc 方法。 :-) - BENY
如果数据不均匀,where方法将根据dfmi重新索引mask,因此应该处理它。您可以通过执行mask = mask.reindex_like(dfmi, fill_value=<True or False>)来获得更多控制。 - JoeCondron

1

通过使用底层数组数据进行原地编辑以实现内存效率(不创建任何其他数据帧) -

d = len(dfmi.columns.levels[0])
n = dfmi.shape[1]//d
for i in range(0,d*n,n):
    dfmi.values[:,i:i+n][a_mask] = 0

样例运行 -

In [833]: dfmi
Out[833]: 
lvl0   A                       B                    
lvl1   a       b       c       a       b       c    
lvl2 bar foo bar foo bar foo bar foo bar foo bar foo
A0     1   0   3   2   5   4   7   6   9   8  11  10
A1    13  12  15  14  17  16  19  18  21  20  23  22
A2    25  24  27  26  29  28  31  30  33  32  35  34
A3    37  36  39  38  41  40  43  42  45  44  47  46

In [834]: a_mask
Out[834]: 
        a             b             c       
      foo    bar    foo    bar    foo    bar
A0   True   True   True  False  False  False
A1  False   True  False  False   True  False
A2  False   True   True   True  False  False
A3  False  False  False  False  False   True

In [835]: d = len(dfmi.columns.levels[0])
     ...: n = dfmi.shape[1]//d
     ...: for i in range(0,d*n,n):
     ...:     dfmi.values[:,i:i+n][a_mask] = 0

In [836]: dfmi
Out[836]: 
lvl0   A                       B                    
lvl1   a       b       c       a       b       c    
lvl2 bar foo bar foo bar foo bar foo bar foo bar foo
A0     0   0   0   2   5   4   0   0   0   8  11  10
A1    13   0  15  14   0  16  19   0  21  20   0  22
A2    25   0   0   0  29  28  31   0   0   0  35  34
A3    37  36  39  38  41   0  43  42  45  44  47   0

谢谢您的回答。您的解决方案是否依赖于每个列级别具有相同数量的标签(2、2、2)这一事实? - FLab
@FLab 它确实如此! - Divakar
哎呀,不幸的是,在我的使用情况下并非如此(我的错误是示例不够通用,我将对其进行更新)。我真的很喜欢你的这个答案:https://dev59.com/BVgR5IYBdhLWcg3wT7xL我想知道它是否可以修改/概括为这种情况? - FLab
@FLab 只要 dfmi.A 和 dfmi.B 的形状相同,无论它们的形状如何,这个应该可以工作。这是唯一的要求。 - Divakar
@FLab 不再需要了。请查看编辑内容。无论外层标签的数量是多少 - A、B、C等,都应该可以工作。 - Divakar
显示剩余2条评论

0

更新的解决方案更加健壮,不使用硬编码来设置级别值:

lvl0_values = dfmi.columns.get_level_values(0).unique()
pd.concat([dfmi[i].mask(a_mask.rename_axis(['lvl1','lvl2'],axis=1),0) for i in lvl0_values],
          keys=lvl0_values, axis=1)

输出:

lvl0   A               B            
lvl1   a       b       a       b    
lvl2 bar foo bar foo bar foo bar foo
A0     1   0   0   0   5   0   0   0
A1     9   0  11   0  13   0  15   0
A2    17  16  19   0  21  20  23   0
A3     0  24   0  26   0  28   0  30

你可以这样做:

pd.concat([dfmi['A'].mask(a_mask.rename_axis(['lvl1','lvl2'],axis=1),0),
           dfmi['B'].mask(a_mask.rename_axis(['lvl1','lvl2'],axis=1),0)],
           keys=['A','B'], axis=1)

print(a_mask)

lvl1      a             b       
lvl2    foo    bar    foo    bar
A0     True  False   True   True
A1     True  False   True  False
A2    False  False   True  False
A3    False   True  False   True

输出:

       A               B            
lvl1   a       b       a       b    
lvl2 bar foo bar foo bar foo bar foo
A0     1   0   0   0   5   0   0   0
A1     9   0  11   0  13   0  15   0
A2    17  16  19   0  21  20  23   0
A3     0  24   0  26   0  28   0  30

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