Mathew K Analytics

Lesson 30 · Mastering Pandas

Understanding Multi-Level Grouping and Custom Aggregations in Pandas for Data Analysis

Welcome! Today we will master advanced grouping and aggregation in pandas. Learn why grouping by more than one column is so powerful. See how custom…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Multi-level Grouping and Custom Aggregations in Pandas#

Welcome! Today we will master advanced grouping and aggregation in pandas.

  • Learn why grouping by more than one column is so powerful.
  • See how custom functions reveal new insights from your data.

We'll practice with the Titanic dataset, exploring survival rates by gender and passenger class. Ready? Let's dive in!

# Suppress warnings for a clean experience
import warnings
import numpy as np
np.random.seed(42)
warnings.filterwarnings("ignore")
# Data setup (Titanic Dataset)
import pandas as pd
url = 'https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv'
df = pd.read_csv(url)
print(df.shape)
print(df.head(3))
(891, 12)
   PassengerId  Survived  Pclass  \
0            1         0       3   
1            2         1       1   
2            3         1       3   

                                                Name     Sex   Age  SibSp  \
0                            Braund, Mr. Owen Harris    male  22.0      1   
1  Cumings, Mrs. John Bradley (Florence Briggs Th...  female  38.0      1   
2                             Heikkinen, Miss. Laina  female  26.0      0   

   Parch            Ticket     Fare Cabin Embarked  
0      0         A/5 21171   7.2500   NaN        S  
1      0          PC 17599  71.2833   C85        C  
2      0  STON/O2. 3101282   7.9250   NaN        S  

Why Group By Multiple Columns?#

Grouping by more than one column lets us break down patterns within subgroups.

For example:

  • How likely were women in First Class to survive?
  • Did survival differ by 'Sex' across all passenger classes?

Pandas lets us answer these questions in just a few lines!

# Preview value counts for Sex and Pclass
print(df['Sex'].value_counts())
print(df['Pclass'].value_counts())
Sex
male      577
female    314
Name: count, dtype: int64
Pclass
3    491
1    216
2    184
Name: count, dtype: int64
# Group by Sex and Pclass, then count survivors
grouped = df.groupby(['Sex', 'Pclass'])['Survived']
print(grouped.sum())
Sex     Pclass
female  1         91
        2         70
        3         72
male    1         45
        2         17
        3         47
Name: Survived, dtype: int64
# Group by Sex and Pclass, then calculate survival rate
survival_rates = grouped.mean()
print(survival_rates)
Sex     Pclass
female  1         0.968085
        2         0.921053
        3         0.500000
male    1         0.368852
        2         0.157407
        3         0.135447
Name: Survived, dtype: float64

What is a MultiIndex?#

Grouping by multiple columns creates a MultiIndex for the rows.

A MultiIndex lets pandas identify groups based on several columns.

This structure allows us to drill deep into subgroup details.

We can reset it back to columns using .reset_index().

# Reset index to turn MultiIndex into columns
survival_rates = survival_rates.reset_index()
print(survival_rates.head())
      Sex  Pclass  Survived
0  female       1  0.968085
1  female       2  0.921053
2  female       3  0.500000
3    male       1  0.368852
4    male       2  0.157407
# Custom aggregation: count, mean, and median simultaneously
agg_results = df.groupby(['Sex', 'Pclass'])['Fare'].agg(['count', 'mean', 'median'])
print(agg_results)
               count        mean    median
Sex    Pclass                             
female 1          94  106.125798  82.66455
       2          76   21.970121  22.00000
       3         144   16.118810  12.47500
male   1         122   67.226127  41.26250
       2         108   19.741782  13.00000
       3         347   12.661633   7.92500
# Using a custom aggregation function: percent missing age
def percent_missing(series):
    return series.isnull().mean() * 100

missing_age = df.groupby(['Sex', 'Pclass'])['Age'].agg(percent_missing)
print(missing_age)
Sex     Pclass
female  1          9.574468
        2          2.631579
        3         29.166667
male    1         17.213115
        2          8.333333
        3         27.089337
Name: Age, dtype: float64
# Aggregating different functions per column using a dictionary
agg_dict = {
    'Fare': ['mean', 'std'],
    'Age': ['median', percent_missing]
}
multiagg = df.groupby(['Sex', 'Pclass']).agg(agg_dict)
print(multiagg.head())
                     Fare               Age                
                     mean        std median percent_missing
Sex    Pclass                                              
female 1       106.125798  74.259988   35.0        9.574468
       2        21.970121  10.891796   28.0        2.631579
       3        16.118810  11.690314   21.5       29.166667
male   1        67.226127  77.548021   40.0       17.213115
       2        19.741782  14.922235   30.0        8.333333
# Sorting aggregation results by mean Fare
sorted_multiagg = multiagg.sort_values(('Fare', 'mean'), ascending=False)
print(sorted_multiagg)
                     Fare               Age                
                     mean        std median percent_missing
Sex    Pclass                                              
female 1       106.125798  74.259988   35.0        9.574468
male   1        67.226127  77.548021   40.0       17.213115
female 2        21.970121  10.891796   28.0        2.631579
male   2        19.741782  14.922235   30.0        8.333333
female 3        16.118810  11.690314   21.5       29.166667
male   3        12.661633  11.681696   25.0       27.089337
# Applying a lambda: survival rates for minors versus adults by class
df['IsMinor'] = df['Age'] < 18
minor_rates = df.groupby(['Pclass', 'IsMinor'])['Survived'].mean()
print(minor_rates)
Pclass  IsMinor
1       False      0.612745
        True       0.916667
2       False      0.409938
        True       0.913043
3       False      0.217918
        True       0.371795
Name: Survived, dtype: float64
# Unstack to make the aggregation easier to read as a table
table_rates = minor_rates.unstack()
print(table_rates)
IsMinor     False     True 
Pclass                     
1        0.612745  0.916667
2        0.409938  0.913043
3        0.217918  0.371795
# Mini-project: Find median fares and average ages per embark port and class
results = df.groupby(['Embarked', 'Pclass']).agg({'Fare': 'median', 'Age': 'mean'})
print(results)
                    Fare        Age
Embarked Pclass                    
C        1       78.2667  38.027027
         2       24.0000  22.766667
         3        7.8958  20.741951
Q        1       90.0000  38.500000
         2       12.3500  43.500000
         3        7.7500  25.937500
S        1       52.0000  38.152037
         2       13.5000  30.386731
         3        8.0500  25.696552
# Practice: Use input() to explore custom groupings
col1 = input("First group-by column (Sex/Pclass/Embarked): ")
col2 = input("Second group-by column (Sex/Pclass/Embarked): ")
stat = input("Choose stat (mean/count/median): ")
col = input("Target numeric column (Fare/Age/SibSp): ")
custom = df.groupby([col1, col2])[col].agg(stat)
print(custom)
Sex     Pclass
female  1         106.125798
        2          21.970121
        3          16.118810
male    1          67.226127
        2          19.741782
        3          12.661633
Name: Fare, dtype: float64

Best Practices and Performance Tips#

  • Group only by columns you need: Less memory, faster code.
  • Try .agg() with built-in and custom functions together.
  • For big data, watch for NaN and missing values.
  • Use .sort_values() to help spot most important groups.
  • Reset unwanted multi-level indexes for tidy tables.

Troubleshooting:

  • Errors about missing columns usually mean a typo or missing reset_index().
  • For KeyError: check all groupby columns existed in your DataFrame.
  • Use df.info() to check your columns' types if grouping fails.
# Challenge: Calculate total family members on board and average survival by family size and class
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
fam_group = df.groupby(['FamilySize', 'Pclass'])['Survived'].mean()
print(fam_group.head(10))
FamilySize  Pclass
1           1         0.532110
            2         0.346154
            3         0.212963
2           1         0.728571
            2         0.529412
            3         0.350877
3           1         0.750000
            2         0.677419
            3         0.425532
4           1         0.714286
Name: Survived, dtype: float64
# Extra: Visualize survival rates by class and gender
import matplotlib.pyplot as plt
rates = df.groupby(['Sex', 'Pclass'])['Survived'].mean().unstack()
rates.plot(kind='bar')
plt.title('Survival Rate by Gender and Class')
plt.ylabel('Survival Rate')
plt.show()
No description has been provided for this image

Recap: Key Concepts for Multi-level Grouping and Custom Aggregation#

  • Use groupby with lists for multiple columns.
  • Custom aggregation functions help answer unique questions.
  • Chain with .agg() for clear, powerful group summaries.
  • Use .reset_index() and .unstack() for easier reading and plotting.

The next time you need detailed breakdowns or hidden patterns, you will be ready!

Thank you for joining! If this lesson was helpful, hit Like and Subscribe on YouTube.#

Share your own groupby ideas in the comments!

Happy coding with pandas!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.