Q&A library Python

How do I group consecutive rows that share a value in pandas?

Python Advanced cumsumgroupbysessions

Short answer

Detect where the value changes, take a cumulative sum of those changes to build a run identifier, then group by it. This is how you find streaks, sessions and consecutive blocks.

The short version

Compare each row to the previous one, take the cumulative sum of the changes, and you get an identifier that increments at each new run. Group by that identifier to treat consecutive blocks as groups. This is the standard trick for streaks, sessions and any "consecutive run" question.

Building the run identifier

import pandas as pd

df = pd.DataFrame({"status": ["on", "on", "off", "on", "on"]})

changed = df["status"] != df["status"].shift(1)
df["run"] = changed.cumsum()
print(df)
  status  run
0     on    1
1     on    1
2    off    2
3     on    3
4     on    3

Every time the value differs from the row above, changed is True, which adds one to the running total. Rows that continue a run keep the same identifier. Note that the two on blocks get different identifiers (1 and 3), which is exactly right: they are separate runs, not one group.

Why cumsum works here

changed is a boolean column, and True counts as one in a sum. So cumsum() increments only at each boundary and holds steady within a run. It is a neat use of the fact that booleans are numbers.

Using it

Once you have the run identifier, everything is an ordinary groupby.

df.groupby("run").size()                    # length of each run
df.groupby("run").agg(
    status=("status", "first"),
    length=("status", "size"),
)

This gives you the length of each streak, which is the usual question.

The longest streak

runs = df.groupby("run").agg(status=("status", "first"), length=("status", "size"))
runs[runs["status"] == "on"]["length"].max()   # longest run of 'on'

Sessions from timestamps

The same pattern splits events into sessions when a gap exceeds a threshold, which is one of its most valuable uses.

df = df.sort_values("timestamp")
gap = df["timestamp"].diff() > pd.Timedelta(minutes=30)
df["session"] = gap.cumsum()

Any gap of more than thirty minutes starts a new session. Group by session and you can count events per session, measure session length, and so on.

Within groups

If the runs must be detected separately per user or per city, do the comparison within the group so a run never spans two groups.

df = df.sort_values(["city", "timestamp"])
changed = (df["status"] != df.groupby("city")["status"].shift(1))
df["run"] = changed.cumsum()

Because changed is True at the first row of every city (the comparison with NaN is True), each city's first row correctly begins a new run.

Sort first

As with everything based on row order, this only means anything if the rows are sorted. Sorting by the group key and then the sequence column is the necessary first step, and forgetting it produces runs that are pure noise.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project