Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Python

Ranking and Window Calculations in pandas: Running Totals, Cumulative Averages, and Percent Rank with groupby and transform

Learn how to compute running totals, cumulative averages, rolling windows, and within-group rankings in pandas using groupby and transform. Practical examples using realistic sales data, with full copy-paste code and a complete weekly performance report you can adapt immediately.

⚡ Practitioner19 min readSep 22, 2026Updated Sep 22, 2026
Ranking and Window Calculations in pandas: Running Totals, Cumulative Averages, and Percent Rank with groupby and transform
On this page
  • Introduction
  • Prerequisites
  • The Dataset: Quarterly Sales Records
  • Understanding `transform`: The Key That Unlocks Group-Aware Window Functions
  • Running Totals: `cumsum` Within Groups
  • Cumulative Max, Min, and Product
  • Cumulative Averages with `expanding`
  • Rolling Windows Within Groups
  • Ranking Within Groups: `rank` and `pct_rank`
  • Basic Rank
  • Tie-Breaking Methods
  • Ranking Across All Weeks
Percent Rank: Normalizing Position to 0–1
  • Combining Everything: Building a Real Weekly Performance Report
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Forgetting to sort before cumulative calculations
  • Mistake 2: Using `agg` when you meant `transform`
  • Mistake 3: Grouping incorrectly for weekly rankings
  • Mistake 4: Integer dtypes breaking `rank` output
  • Mistake 5: `expanding()` not resetting between groups
  • Summary & Next Steps
  • Ranking and Window Calculations in pandas: Running Totals, Cumulative Averages, and Percent Rank with groupby and transform

    Introduction

    Picture this: your sales director drops a spreadsheet on your desk and asks for three things before the end-of-day meeting. First, she wants a running total of revenue by region so she can see how each territory is tracking toward its quarterly goal. Second, she wants a rolling average to smooth out the noise in weekly numbers. Third, she wants every salesperson ranked within their region — not globally, but within their peer group — so the team can see where they stand relative to colleagues selling in similar markets.

    In Excel, you'd cobble this together with a mix of SUMIF, RANK, and maybe a pivot table. In SQL, you'd reach for window functions like SUM() OVER (PARTITION BY ...). In pandas, there's an elegant toolkit for exactly this kind of work: cumsum, cumprod, expanding, rank, and transform — often combined with groupby to do the partitioning that makes window calculations actually useful. This lesson teaches you to use all of them fluently, in realistic scenarios that mirror what you'd encounter in a production reporting environment.

    By the end of this lesson, you'll have moved beyond simple aggregation and into the territory of "per-row analytics" — the calculations that require knowing both a row's individual value and its relationship to the group it belongs to.

    What you'll learn:

    • How cumsum, cummax, and cumprod work row-by-row, and how to apply them within groups using groupby + transform
    • How to compute cumulative averages and expanding-window statistics that update with each new row
    • How rank and pct_rank work, including how to handle ties and partition rankings by group
    • How transform differs from agg and apply, and why it's the right tool for adding back-calculated columns to your original DataFrame
    • How to combine multiple window calculations into a coherent analytical report

    Prerequisites

    You should be comfortable with the following before diving in:

    • Loading and exploring DataFrames (covered in Your First pandas DataFrame: Loading CSV and Excel Files and Exploring Data)
    • Filtering and selecting data with loc and boolean masks (Selecting and Filtering Data in pandas: loc, iloc, and Boolean Masks)
    • Basic groupby and aggregation (Grouping and Aggregating in pandas: groupby as the PivotTable Replacement)

    You don't need to know anything about SQL window functions, but if you do, you'll find the parallels helpful and we'll call them out explicitly.


    The Dataset: Quarterly Sales Records

    Let's build a realistic dataset we'll use throughout the lesson. This represents daily sales transactions across four regions and a handful of reps:

    import pandas as pd
    import numpy as np
    
    np.random.seed(42)
    
    regions = ['Northeast', 'Southeast', 'Midwest', 'West']
    reps = {
        'Northeast': ['Alice', 'Bob', 'Carol'],
        'Southeast': ['David', 'Elena'],
        'Midwest':   ['Frank', 'Grace', 'Hank'],
        'West':      ['Iris', 'Jake'],
    }
    
    rows = []
    dates = pd.date_range('2024-01-01', periods=13, freq='W-MON')  # 13 weekly periods
    
    for week in dates:
        for region, people in reps.items():
            for rep in people:
                revenue = np.random.randint(8_000, 45_000)
                units   = np.random.randint(10, 120)
                rows.append({
                    'week':    week,
                    'region':  region,
                    'rep':     rep,
                    'revenue': revenue,
                    'units':   units,
                })
    
    df = pd.DataFrame(rows).sort_values(['region', 'rep', 'week']).reset_index(drop=True)
    print(df.head(10))
    
            week     region   rep  revenue  units
    0 2024-01-01  Midwest   Frank    32145     87
    1 2024-01-08  Midwest   Frank    18932     44
    2 2024-01-15  Midwest   Frank    41203     99
    ...
    

    This gives us 130 rows (13 weeks × 10 reps), sorted by region, rep, and week — the natural order for running calculations.

    Note

    Sorting before cumulative calculations is critical. pandas processes rows in the order they appear; if your DataFrame isn't sorted by the time dimension first, your running totals will be nonsense. Always call .sort_values() before applying cumulative functions to time-series data.


    Understanding `transform`: The Key That Unlocks Group-Aware Window Functions

    Before touching cumulative calculations, you need to understand transform, because it's the mechanism that makes group-aware window functions possible.

    When you use groupby + agg, the result is a smaller DataFrame — one row per group. That's useful for summaries, but useless when you want to add a new column back to your original data.

    transform solves this by returning a same-sized Series — every row gets a value, computed within its group context. Think of it as broadcasting group-level calculations back down to the row level.

    Here's the contrast:

    # agg: produces a summary table (fewer rows)
    df.groupby('region')['revenue'].agg('sum')
    # Region
    # Midwest      ...
    # Northeast    ...
    # Southeast    ...
    # West         ...
    
    # transform: produces a same-length Series you can assign back
    df['region_total'] = df.groupby('region')['revenue'].transform('sum')
    df[['region', 'rep', 'week', 'revenue', 'region_total']].head(8)
    
         region   rep       week  revenue  region_total
    0   Midwest  Frank 2024-01-01    32145       3287654
    1   Midwest  Frank 2024-01-08    18932       3287654
    2   Midwest  Frank 2024-01-15    41203       3287654
    ...
    

    Every Midwest row gets the same region_total. That's the "broadcast" behavior — and it's what makes the more interesting calculations below possible. You can pass any aggregation string ('sum', 'mean', 'max', 'std') or a custom function to transform.

    Key insight

    In SQL terms, transform is the pandas equivalent of a window function with a PARTITION BY clause but no ORDER BY. It computes a value per group and joins it back to every row in that group. The cumulative functions we'll cover next add the ORDER BY dimension.


    Running Totals: `cumsum` Within Groups

    cumsum is the simplest window function: each row gets the sum of all values up to and including that row. Globally, it's trivial:

    df['running_revenue_global'] = df['revenue'].cumsum()
    

    But that's almost never what you want. You want running totals per rep — resetting at the start of each rep's record, not carrying over from one rep to the next.

    This is where groupby + transform comes in:

    # Running total of revenue per rep (resets for each rep)
    df['running_rev_per_rep'] = (
        df
        .groupby('rep')['revenue']
        .transform('cumsum')
    )
    
    # Running total per region (resets for each region)
    df['running_rev_per_region'] = (
        df
        .groupby('region')['revenue']
        .transform('cumsum')
    )
    
    # Check one rep's progression
    alice = df[df['rep'] == 'Alice'][['week', 'revenue', 'running_rev_per_rep']]
    print(alice)
    
              week  revenue  running_rev_per_rep
    ...  2024-01-01    22341                22341
    ...  2024-01-08    31204                53545
    ...  2024-01-15    14876                68421
    ...
    

    Each week builds on the previous. The running total resets to zero at the start of each rep's record, exactly as you'd want.

    Adding a percentage-of-goal column is a natural next step. Suppose the quarterly revenue target per rep is $400,000:

    QUARTERLY_TARGET = 400_000
    
    df['pct_of_target'] = (df['running_rev_per_rep'] / QUARTERLY_TARGET * 100).round(1)
    

    Now every row shows the rep's cumulative progress toward their quarterly goal — a genuinely useful number for a mid-quarter report.

    Tip

    cumsum also works on boolean columns. If you have an is_enterprise_deal flag, cumsum() on it gives you a running count of enterprise deals per rep, which is often more readable than a raw count. Combine it with groupby + transform the same way.


    Cumulative Max, Min, and Product

    The cumsum pattern extends to other cumulative operations:

    # Best single-week revenue achieved so far (running peak)
    df['peak_revenue_to_date'] = (
        df
        .groupby('rep')['revenue']
        .transform('cummax')
    )
    
    # Worst week so far (useful for flagging reps who are declining)
    df['trough_revenue_to_date'] = (
        df
        .groupby('rep')['revenue']
        .transform('cummin')
    )
    

    The cummax column answers a question like "what's the best this rep has ever done up to this point in time?" It's a useful benchmark column — any week where revenue < peak_revenue_to_date is a week the rep underperformed their personal best.

    df['below_personal_best'] = df['revenue'] < df['peak_revenue_to_date']
    

    Warning

    cumprod exists but is rarely useful for revenue-type data — multiplying dollars together produces numbers with no business meaning. It's genuinely useful for things like cumulative growth rates (multiplying 1.03 × 1.05 × 0.98 gives compound growth), but don't reach for it by default.


    Cumulative Averages with `expanding`

    A running average — the mean of all observations up to the current row — smooths out week-to-week noise and shows you the underlying trend more clearly than raw values. pandas calls this an expanding window because the window size grows with each row.

    For a global expanding mean, you'd write df['revenue'].expanding().mean(). For a per-group expanding mean, you need a custom function inside transform:

    # Expanding (cumulative) mean per rep
    df['cumulative_avg_rev'] = (
        df
        .groupby('rep')['revenue']
        .transform(lambda x: x.expanding().mean())
    )
    
    # Expanding mean per region (all reps combined)
    df['cumulative_avg_rev_region'] = (
        df
        .groupby('region')['revenue']
        .transform(lambda x: x.expanding().mean())
    )
    
    # Round for readability
    df['cumulative_avg_rev'] = df['cumulative_avg_rev'].round(0)
    

    The expanding mean is a much more stable signal than a single week's revenue. After 10 weeks, the cumulative average is based on 10 data points and won't swing dramatically from a single strong or weak week.

    Comparing weekly revenue to the cumulative average is a powerful diagnostic:

    df['vs_cumulative_avg'] = (df['revenue'] - df['cumulative_avg_rev']).round(0)
    df['above_avg'] = df['vs_cumulative_avg'] > 0
    

    Reps who are consistently above_avg=True in the last few weeks are on an upward trend. Those who've been above_avg=False for several weeks are worth a closer look.

    Key insight

    The expanding mean is related to, but distinct from, the rolling mean. A rolling mean uses a fixed-size window (e.g., the last 4 weeks). An expanding mean uses all history from the start. For early observations, they behave similarly; as data accumulates, the expanding mean becomes more stable while the rolling mean stays reactive. Both are covered in depth for time-series work in Working with Dates and Time Series in pandas: Parsing, Resampling, and Rolling Windows.


    Rolling Windows Within Groups

    While we're here, let's cover rolling calculations within groups — they follow the same pattern as expanding:

    # 4-week rolling average revenue per rep
    df['rolling_4w_avg'] = (
        df
        .groupby('rep')['revenue']
        .transform(lambda x: x.rolling(window=4, min_periods=1).mean())
    )
    

    The min_periods=1 argument means the rolling window will compute a mean even when fewer than 4 observations are available (i.e., for the first three weeks of each rep's record). Without it, those early rows would be NaN.

    # 4-week rolling sum (useful for "last month's total")
    df['rolling_4w_total'] = (
        df
        .groupby('rep')['revenue']
        .transform(lambda x: x.rolling(window=4, min_periods=1).sum())
    )
    

    Rolling calculations within groups are particularly useful for:

    • Trailing 12-month revenue totals in monthly data
    • 7-day moving averages in daily data
    • Any "recent trend" calculation that should ignore data from too far back

    Ranking Within Groups: `rank` and `pct_rank`

    Ranking is where the "window function" concept becomes especially powerful for people-analytics and leaderboard-style reporting. You almost never want a global rank — you want a rank within a peer group.

    Basic Rank

    # Within-region rank by total revenue (for the most recent week only)
    latest_week = df['week'].max()
    latest = df[df['week'] == latest_week].copy()
    
    latest['region_rank'] = (
        latest
        .groupby('region')['revenue']
        .rank(method='dense', ascending=False)
    )
    
    print(latest[['region', 'rep', 'revenue', 'region_rank']].sort_values(['region', 'region_rank']))
    
            region    rep  revenue  region_rank
    ...   Midwest   Grace    43210          1.0
    ...   Midwest    Hank    38742          2.0
    ...   Midwest   Frank    22108          3.0
    ...  Northeast   Carol    41876          1.0
    ...  Northeast   Alice    34521          2.0
    ...  Northeast     Bob    19043          3.0
    

    Each rep is ranked 1st, 2nd, or 3rd within their own region, not against the entire company. A rep who would be #7 globally might be #1 in their region — both pieces of information are meaningful.

    Tie-Breaking Methods

    The method parameter controls what happens when two reps have the same revenue:

    Method Behavior SQL equivalent
    'average' Tied values get the average of their ranks (default) AVG
    'min' Tied values both get the lower rank RANK()
    'max' Tied values both get the higher rank —
    'dense' Tied values get the same rank; next rank is consecutive DENSE_RANK()
    'first' Tied values ranked by order of appearance ROW_NUMBER()

    For most leaderboard reports, 'dense' is the most intuitive — if two reps tie for first, both get rank 1 and the next rep gets rank 2 (not rank 3).

    Tip

    If you're coming from SQL, dense maps to DENSE_RANK(), min maps to RANK(), and first maps to ROW_NUMBER(). The pandas default (average) has no direct SQL equivalent and is more common in statistical contexts.

    Ranking Across All Weeks

    To rank every row — not just the latest week — for a "ranking over time" view:

    # Rank each rep within their region for every week
    df['weekly_region_rank'] = (
        df
        .groupby(['week', 'region'])['revenue']
        .rank(method='dense', ascending=False)
    )
    

    Note the grouping here: ['week', 'region']. For each combination of week and region, we independently rank the reps. This gives you a longitudinal view of how rankings shift week to week.

    # How often has each rep been ranked #1 in their region?
    times_top = (
        df[df['weekly_region_rank'] == 1]
        .groupby(['region', 'rep'])
        .size()
        .rename('weeks_at_top')
        .reset_index()
    )
    print(times_top)
    

    That's a genuinely interesting metric: not just "who's winning now" but "who's been most consistently at the top."


    Percent Rank: Normalizing Position to 0–1

    pct_rank (called with rank(pct=True)) converts ranks to percentiles: 0.0 means lowest, 1.0 means highest. It answers the question "what fraction of the group is this rep outperforming?"

    # Percentile rank within region for total-period revenue
    total_rev = (
        df
        .groupby(['region', 'rep'])['revenue']
        .sum()
        .reset_index()
        .rename(columns={'revenue': 'total_revenue'})
    )
    
    total_rev['pct_rank_in_region'] = (
        total_rev
        .groupby('region')['total_revenue']
        .rank(pct=True)
        .round(2)
    )
    
    print(total_rev.sort_values(['region', 'pct_rank_in_region'], ascending=[True, False]))
    
          region    rep  total_revenue  pct_rank_in_region
    ...  Midwest   Grace        412340                1.00
    ...  Midwest   Frank        389201                0.67
    ...  Midwest    Hank        314820                0.33
    ...
    

    A rep with pct_rank_in_region = 1.0 is at the 100th percentile within their region — they outperformed everyone in their peer group over the period.

    Percent rank is useful when you want a normalized score that lets you compare across groups of different sizes. A rep in a 3-person region and a rep in a 5-person region can both have a pct_rank of 0.8 — meaning they're in the top 20% of their respective groups — even though the absolute ranks would look different.

    Warning

    pct_rank is sensitive to small group sizes. With only 2 reps in a group, the values are always 0.0 and 1.0 — there's no nuance. Don't over-interpret percentile ranks when the peer group is fewer than ~5 people. Flag this caveat in any report you share.


    Combining Everything: Building a Real Weekly Performance Report

    Let's pull all these techniques together into a single, coherent analytical output that you could actually send to a sales director.

    import pandas as pd
    import numpy as np
    
    # --- Rebuild dataset (same as top of lesson) ---
    np.random.seed(42)
    regions = ['Northeast', 'Southeast', 'Midwest', 'West']
    reps = {
        'Northeast': ['Alice', 'Bob', 'Carol'],
        'Southeast': ['David', 'Elena'],
        'Midwest':   ['Frank', 'Grace', 'Hank'],
        'West':      ['Iris', 'Jake'],
    }
    rows = []
    dates = pd.date_range('2024-01-01', periods=13, freq='W-MON')
    for week in dates:
        for region, people in reps.items():
            for rep in people:
                revenue = np.random.randint(8_000, 45_000)
                units   = np.random.randint(10, 120)
                rows.append({'week': week, 'region': region, 'rep': rep,
                             'revenue': revenue, 'units': units})
    
    df = pd.DataFrame(rows).sort_values(['region', 'rep', 'week']).reset_index(drop=True)
    
    # -----------------------------------------------
    # 1. Cumulative revenue per rep
    # -----------------------------------------------
    df['cum_rev_rep'] = (
        df.groupby('rep')['revenue'].transform('cumsum')
    )
    
    # 2. 4-week rolling average per rep (smoothed trend)
    df['roll4_avg_rep'] = (
        df.groupby('rep')['revenue']
          .transform(lambda x: x.rolling(4, min_periods=1).mean())
          .round(0)
    )
    
    # 3. Expanding (cumulative) average per rep
    df['expanding_avg_rep'] = (
        df.groupby('rep')['revenue']
          .transform(lambda x: x.expanding().mean())
          .round(0)
    )
    
    # 4. Week-over-week change per rep
    df['wow_change'] = (
        df.groupby('rep')['revenue']
          .transform(lambda x: x.diff())
    )
    
    # 5. Weekly rank within region (1 = top earner that week)
    df['rank_in_region'] = (
        df.groupby(['week', 'region'])['revenue']
          .rank(method='dense', ascending=False)
          .astype(int)
    )
    
    # 6. Percentile rank within region, per week
    df['pct_rank_in_region'] = (
        df.groupby(['week', 'region'])['revenue']
          .rank(pct=True)
          .round(2)
    )
    
    # 7. Flag: is this week's revenue above the rep's cumulative average?
    df['above_own_avg'] = df['revenue'] > df['expanding_avg_rep']
    
    # -----------------------------------------------
    # Build the summary snapshot: latest week only
    # -----------------------------------------------
    latest = df[df['week'] == df['week'].max()].copy()
    
    # Add cumulative-period rank by total revenue within region
    period_totals = (
        df.groupby(['region', 'rep'])['revenue']
          .sum()
          .reset_index()
          .rename(columns={'revenue': 'period_total'})
    )
    period_totals['period_rank_in_region'] = (
        period_totals
        .groupby('region')['period_total']
        .rank(method='dense', ascending=False)
        .astype(int)
    )
    
    report = latest.merge(period_totals, on=['region', 'rep'])
    
    report = report[[
        'region', 'rep', 'week',
        'revenue', 'wow_change',
        'roll4_avg_rep', 'expanding_avg_rep',
        'above_own_avg',
        'cum_rev_rep', 'period_total',
        'rank_in_region', 'pct_rank_in_region',
        'period_rank_in_region'
    ]].sort_values(['region', 'period_rank_in_region'])
    
    print(report.to_string(index=False))
    

    This report gives the sales director everything she asked for — and more:

    • cum_rev_rep: Running total toward quarterly goal
    • roll4_avg_rep: Smoothed trend for the last month
    • rank_in_region and period_rank_in_region: Where each rep stands in their peer group, both for this week and the full period
    • above_own_avg: A simple flag that shows whether a rep is trending up or coasting

    This is the kind of output you can pipe directly into Building Summary Reports with pandas pivot_table and to_excel: Turning Aggregated Data into a Formatted, Multi-Sheet Workbook to produce a polished, formatted Excel file.


    Hands-On Exercise

    Take the dataset from this lesson and complete the following tasks. All of them require combining techniques from across the lesson.

    Setup: Use the same df DataFrame built at the beginning. Make sure it's sorted by ['region', 'rep', 'week'] before starting.

    Task 1: Units leaderboard Add a column called units_rank_in_region that ranks each rep by units sold within their region, for every week. A rank of 1 should mean the most units sold. Print the rows for week 2024-01-29 only.

    Task 2: Momentum flag Add a column called momentum that is 'rising' if the rep's current week revenue is above their 4-week rolling average, and 'falling' otherwise. (Hint: use np.where or a conditional column — see Conditional Columns and Bucketing in pandas: Creating New Fields with np.where, cut, and map if you need a refresher.)

    Task 3: Running share of region For each row, calculate what percentage of the region's cumulative revenue to that point was contributed by this rep. Call it rep_share_of_region_cumrev. (Hint: you'll need both the per-rep cumulative sum and the per-region cumulative sum.)

    Task 4: Full-period summary Collapse the DataFrame to one row per rep. Include: total revenue, total units, overall percentile rank within their region by revenue, number of weeks spent at rank #1 in region, and number of weeks with above_own_avg == True. Sort by region, then by total revenue descending.


    Common Mistakes & Troubleshooting

    Mistake 1: Forgetting to sort before cumulative calculations

    # WRONG: cumulative sum on unsorted data
    df['bad_cumsum'] = df.groupby('rep')['revenue'].transform('cumsum')
    
    # CORRECT: sort first
    df = df.sort_values(['rep', 'week']).reset_index(drop=True)
    df['good_cumsum'] = df.groupby('rep')['revenue'].transform('cumsum')
    

    The result of cumsum depends entirely on row order. If your data was loaded from a database or shuffled somewhere in your pipeline, the cumulative values will be wrong and the problem won't be obvious from inspection.

    Mistake 2: Using `agg` when you meant `transform`

    # This returns a smaller DataFrame — can't assign back
    result = df.groupby('rep')['revenue'].agg('cumsum')  # raises an error or misaligns
    
    # This returns a same-length Series
    result = df.groupby('rep')['revenue'].transform('cumsum')  # correct
    

    If you ever get an alignment error when trying to assign a grouped result back to your DataFrame, the first thing to check is whether you accidentally used agg instead of transform.

    Mistake 3: Grouping incorrectly for weekly rankings

    # WRONG: this ranks globally across all weeks
    df['bad_rank'] = df.groupby('region')['revenue'].rank()
    
    # CORRECT: group by both week AND region to rank within each week's cohort
    df['good_rank'] = df.groupby(['week', 'region'])['revenue'].rank()
    

    This is easy to miss. If you want "who's #1 in the Northeast this week", you need to group on ['week', 'region'], not just ['region']. Grouping on region alone ranks all rows across all weeks, which is meaningless as a weekly leaderboard.

    Mistake 4: Integer dtypes breaking `rank` output

    # rank returns floats by default (because of the 'average' method for ties)
    df['rank_col'] = df.groupby(['week', 'region'])['revenue'].rank(method='dense')
    # dtype: float64
    
    # Cast to int only when you're confident there are no NaN values
    df['rank_col'] = df['rank_col'].astype(int)
    

    If your source column has NaN values, rank will propagate them and you'll get a float column even with method='dense'. Fix missing values first (see Cleaning Messy Data with pandas: Missing Values, Duplicates, and Data Types) before ranking.

    Mistake 5: `expanding()` not resetting between groups

    # WRONG: expanding mean globally — doesn't reset per rep
    df['wrong_exp_avg'] = df['revenue'].expanding().mean()
    
    # CORRECT: expanding mean per rep using transform + lambda
    df['correct_exp_avg'] = (
        df.groupby('rep')['revenue']
          .transform(lambda x: x.expanding().mean())
    )
    

    The expanding() method on a plain Series has no concept of groups. It sees one long sequence of values across all reps. To get per-group expanding windows, you must use groupby + transform with a lambda.

    Warning

    Performance can be a concern when using lambda functions inside transform on large DataFrames, because pandas can't apply the same optimizations it uses for named aggregation functions like 'sum' or 'mean'. For DataFrames with hundreds of thousands of rows, consider computing the expanding/rolling calculation directly after sorting and grouping, or look at vectorized alternatives discussed in Writing Fast pandas Code: Vectorization Instead of apply and Loops.


    Summary & Next Steps

    You've now got a solid command of the "per-row analytics" layer of pandas — the calculations that live between raw data and summary tables. Let's recap the key ideas:

    • cumsum, cummax, cummin applied via groupby + transform give you running totals and tracking statistics that reset cleanly per group
    • expanding() inside a transform lambda produces cumulative averages that grow more stable with each new data point
    • rolling(window, min_periods=1) inside a transform lambda gives you smoothed trends that stay reactive to recent changes
    • rank(method='dense', ascending=False) applied to groupby(['week', 'region']) gives you within-group leaderboards at every point in time
    • rank(pct=True) normalizes rankings to 0–1, enabling fair comparisons across peer groups of different sizes
    • transform is the key operator: it does the grouping, applies any function, and broadcasts results back to the original DataFrame's shape

    These calculations power a huge share of real-world business reporting — anything involving "tracking toward a goal," "trending up or down," or "where does this person/product/region rank among peers."

    Where to go next:

    • If your data has complex group structures with multiple levels of hierarchy, Reshaping and Analyzing Multi-Level Data in pandas: Working with MultiIndex Columns and Rows After groupby and pivot_table will teach you how to work with MultiIndex outputs from these operations.
    • For more advanced custom aggregations — weighted averages, percent of total, and custom functions — see Weighted Averages, Percent of Total, and Custom Aggregations in pandas: Going Beyond sum and mean in groupby.
    • To turn the report you built in this lesson into a formatted, stakeholder-ready Excel file, head to Automating Excel Reports with pandas and openpyxl: Formatted Workbooks Without Manual Work.
    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Python for Data Analysis

    Previous

    Ranking and Comparing Groups in pandas: percent_rank, cumsum, and Window Functions for Running Totals and Leaderboards

    Next

    Calculating Month-over-Month and Year-over-Year Changes in pandas: pct_change, shift, and Period Comparisons for Business Reporting

    Related Insights

    PythonExpert

    Segmenting Customers with RFM Analysis in pandas: Calculating Recency, Frequency, and Monetary Scores from Transaction Data

    28 min
    PythonExpert

    Building a Self-Updating Sales Dashboard in pandas and openpyxl: Automated Charts, Conditional Formatting, and Named Ranges from a Live Data Source

    24 min
    PythonExpert

    Automating pandas Data Cleaning with Custom Validation Rules, Error Logs, and a Corrected Output File

    26 min

    On this page

    • Introduction
    • Prerequisites
    • The Dataset: Quarterly Sales Records
    • Understanding `transform`: The Key That Unlocks Group-Aware Window Functions
    • Running Totals: `cumsum` Within Groups
    • Cumulative Max, Min, and Product
    • Cumulative Averages with `expanding`
    • Rolling Windows Within Groups
    • Ranking Within Groups: `rank` and `pct_rank`
    • Basic Rank
    • Tie-Breaking Methods
    • Ranking Across All Weeks
    • Percent Rank: Normalizing Position to 0–1
    • Combining Everything: Building a Real Weekly Performance Report
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Mistake 1: Forgetting to sort before cumulative calculations
    • Mistake 2: Using `agg` when you meant `transform`
    • Mistake 3: Grouping incorrectly for weekly rankings
    • Mistake 4: Integer dtypes breaking `rank` output
    • Mistake 5: `expanding()` not resetting between groups
    • Summary & Next Steps