How do I group consecutive rows that share a value in pandas?
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.