How do I number the rows within each group in pandas?
Short answer
Use groupby with cumcount to give each row a position within its group, starting at zero. Sort first so the numbering follows a meaningful order, and add one if you want it to start at one.
The short version
Use groupby(key).cumcount() to number rows within each group, starting from zero and restarting at each new group. It is the pandas equivalent of the SQL ROW_NUMBER window function. Sort the data first so the numbering follows the order you intend, and add 1 if you prefer numbering from one.
A worked example
import pandas as pd
df = pd.DataFrame({"city": ["Leeds", "Leeds", "Bath", "Leeds"]})
df["n"] = df.groupby("city").cumcount()
print(df)
city n
0 Leeds 0
1 Leeds 1
2 Bath 0
3 Leeds 2
Each city gets its own counter: Leeds runs 0, 1, 2, while Bath starts again at 0.
Numbering from one
cumcount starts at zero, matching Python indexing. For human-facing "first, second, third" numbering, add one.
df["position"] = df.groupby("city").cumcount() + 1
Sort so the order means something
cumcount numbers rows in their current order, so sort first if the position should reflect time or size. Numbering unsorted rows gives positions that mean nothing.
df = df.sort_values(["city", "date"])
df["visit"] = df.groupby("city").cumcount() + 1
cumcount versus rank
These answer different questions. cumcount gives a strict sequence with no ties: every row in a group gets a distinct number in order. rank looks at the values and can give tied rows the same rank. Use cumcount for "which row is this within the group" and rank for "where does this value stand among the group's values".
A practical use: keeping the first row per group
Row numbering makes "keep only the first record per group" a filter.
first_only = df[df.groupby("city").cumcount() == 0]
This keeps the first row of each city, which is a common deduplication step. It is equivalent to drop_duplicates(subset="city") but lets you sort deliberately first, so you control which row counts as "first".
Counting group size instead
If you want each row tagged with the total size of its group rather than a position, use transform("size"), which broadcasts the count back to every row. That answers "how big is this group", where cumcount answers "where in the group is this row".