Home > Net >  How to aggregate a subset of rows in and append to a MultiIndexed Pandas DataFrame?
How to aggregate a subset of rows in and append to a MultiIndexed Pandas DataFrame?

Time:08-04

Problem Setup & Goal

I have a Multindexed Pandas DataFrame that looks like this:

import pandas as pd

df = pd.DataFrame({
    'Values':[1, 3, 4, 8, 5, 2, 9, 0, 2],
    'A':['A1', 'A1', 'A1', 'A1', 'A2', 'A2', 'A3', 'A3', 'A3'],
    'B':['foo', 'bar', 'fab', 'baz', 'foo', 'baz', 'qux', 'baz', 'bar']
})
df.set_index(['A','B'], inplace=True)
print(df.to_string())

        Values
A  B          
A1 foo       1
   bar       3
   fab       4
   baz       8
A2 foo       5
   baz       2
A3 qux       9
   baz       0
   bar       2

My ultimate goal is to replace all the "bar" and "baz" rows in the B column with a summed row called "other" (see below) in the simplest, most canonical Pandas way.

       Values
A  B          
A1 foo       1
   fab       4
   other    11
A2 foo       5
   other     2
A3 qux       9
   other     2

Current Work

I managed to figure out how to create a mask for a MultiIndex DataFrame from a similar problem to highlight the rows we want to eventually aggregate, which are in an agg_list.

agg_list = ['bar', 'baz']
# Create a mask that highlights the rows in B that are in agg_list
filterFunc = lambda x: x.index.get_level_values('B') in agg_list
mask = df.groupby(level=['A','B']).apply(filterFunc)

This produces the expected mask:

print(mask.to_string())

A   B  
A1  bar     True
    baz     True
    fab    False
    foo    False
A2  baz     True
    foo    False
A3  bar     True
    baz     True
    qux    False

And I know how to remove the rows I no longer need:

# Remove rows in B col that are in agg_list using mask
df_masked = df[[~mask.loc[i1, i2] for i1,i2 in df.index]]
print(df_masked.to_string())

    Values
A  B          
A1 foo       1
   fab       4
A2 foo       5
A3 qux       9

But I don't know how to do the actual aggregation/sum on these rows and append it to each Multindexed row.

Similar Problems/Solutions

Similar problems I've seen didn't involve a Multindex DataFrame, so I can't quite use some of the solutions like this one, which has the same general idea of creating a mask and then append a summed row:

threshold = 6
m = df['value'] < threshold
df1 = df[~m].copy()
df1.loc['Z'] = df.loc[m, 'value'].sum()

or

m = df['value'] < threshold
df1 = df[~m].append(df.loc[m, ['value']].sum().rename('Z'))

CodePudding user response:

Here is one way which resets the index for just B, performs a replace and aggregates the values.

agg_list = ['bar', 'baz']
(df.reset_index(level=1)
.replace({'B':{'|'.join(agg_list):'other'}},regex=True)
.groupby(['A','B']).sum())

Another way is to create a new MultiIndex with bar and baz being replaced with other.

(df.set_axis(pd.MultiIndex.from_arrays([df.index.get_level_values(0),
df.index.get_level_values(1).str.replace('|'.join(agg_list),'other')]))
.groupby(level=[0,1]).sum())

Output:

          Values
A  B            
A1 fab         4
   foo         1
   other      11
A2 foo         5
   other       2
A3 other       2
   qux         9

CodePudding user response:

You can do something as simple as below

b_others = df.B.replace({'bar': 'other', 'baz': 'other'})
df.groupby(['A', b_others]).sum()

Create the variable with replaced 'bar' and 'baz' values. Then, just use it to group.

Output

          Values
A  B            
A1 fab         4
   foo         1
   other      11
A2 foo         5
   other       2
A3 other       2
   qux         9

CodePudding user response:

One option would be to rename axis=0 and level=1 so that bar and baz become other then do a standard groupby sum:

df = df.rename(
    axis=0,
    level=1,
    mapper={'bar': 'other', 'baz': 'other'}
).groupby(level=['A', 'B']).sum()

A bit more generic solution could look like:

from typing import List, Dict


def map_to_value(
        values_to_map: List[str],
        default_value: str
) -> Dict[str, str]:
    return {k: default_value for k in values_to_map}


df = df.rename(
    axis=0,
    level=1,
    mapper=map_to_value(['bar', 'baz'], 'other')
).groupby(level=['A', 'B']).sum()

Either approach results in:

          Values
A  B            
A1 fab         4
   foo         1
   other      11
A2 foo         5
   other       2
A3 other       2
   qux         9

Alternatively, with a bit more work, we can instead keep a set of known values:

from typing import List, Callable


def keep_and_default(
        keep_values: List[str], default_value: str
) -> Callable[[str], str]:
    mapping = {k: k for k in keep_values}

    def mapper(c: str) -> str:
        return mapping.get(c, default_value)

    return mapper


df = df.rename(
    axis=0,
    level=1,
    mapper=keep_and_default(['foo', 'fab', 'qux'], 'other')
).groupby(level=['A', 'B']).sum()

In this case, this also results in:

          Values
A  B            
A1 fab         4
   foo         1
   other      11
A2 foo         5
   other       2
A3 other       2
   qux         9
  • Related