Split a stacked table into one group per ticker, run a calculation inside each group, and get the answers back.
df.groupby("Ticker") splits a table into one group per ticker, runs a calculation inside each group, and puts the answers back together. The calculation never reaches across from one ticker into the next.
In this lesson I read a stacked file of three tickers, take a few per-ticker summaries, then compute daily returns inside each ticker and show what happens at the row where one ticker ends and the next begins.
The prices are simulated, not real market data.
Step 1. Split, apply, combine
prices.csv holds 40 business days for three tickers, stacked on top of each other in one table.
Row 39 is AAPL’s last day. Row 40 is MSFT’s first day. Now compute returns twice: once on the whole column, once per ticker.
df["flat"] = df["Close"].pct_change() # one column, top to bottomdf["ret"] = df.groupby("Ticker")["Close"].pct_change() # restarts inside each tickerprint(df["flat"].isna().sum()) # -> 1print(df["ret"].isna().sum()) # -> 3
1
3
One NaN against three. ret starts fresh for every ticker, so each of the three loses its first day. flat only loses row 0.
The rows where the ticker changes show what flat put there instead.
edge = df["Ticker"] != df["Ticker"].shift(1) # True on each ticker's first rowprint(df.loc[edge, ["Date", "Ticker", "Close", "flat", "ret"]])# -> Date Ticker Close flat ret# -> 0 2026-01-02 AAPL 186.66 NaN NaN# -> 40 2026-01-02 MSFT 416.98 1.461221 NaN# -> 80 2026-01-02 NVDA 125.18 -0.712850 NaN
Date Ticker Close flat ret
0 2026-01-02 AAPL 186.66 NaN NaN
40 2026-01-02 MSFT 416.98 1.461221 NaN
80 2026-01-02 NVDA 125.18 -0.712850 NaN
flat reports a 146% gain on MSFT’s first day and a 71% loss on NVDA’s first day. Those are the prices of two different companies divided by each other:
print(416.98/169.42-1) # -> 1.461220635108016
1.461220635108016
AAPL closed at 169.42 on its last row, MSFT’s first row closed at 416.98, and pct_change divided one by the other. groupby puts NaN there because MSFT has no day before its first day.
Date Ticker Close flat ret
38 2026-02-25 AAPL 171.42 -0.007067 -0.007067
39 2026-02-26 AAPL 169.42 -0.011667 -0.011667
40 2026-01-02 MSFT 416.98 1.461221 NaN
41 2026-01-05 MSFT 423.19 0.014893 0.014893
Inside a ticker the two columns agree. They differ only on the row where the ticker changes.
Step 3. transform keeps every row
g["Close"].mean() gives one number per ticker: three rows out of 120. transform("mean") computes the same number and then copies it onto every row of that ticker, so it fits back into the table.
An average daily return is not the return over the whole stretch. Simple returns compound, they do not add.
NoteDoes the sort matter?
groupby finds the rows belonging to each ticker wherever they sit, so g["Close"].mean() gives the same answer on the unsorted file. pct_change is different: it takes each row against the row above it inside the group. If the dates are shuffled, the groups are still correct, but each return is then taken against whatever row happens to sit above it. Sort by ticker and date before anything that reads a neighbouring row.