Insights
How to Calculate Abnormal Returns in Python
Alphanume Team · June 9, 2026
Mean-adjusted, market-adjusted, and market-model, compared in code.
An abnormal return is the gap between what a security actually earned and what it was expected to earn. The concept is simple; the discipline is in the expected-return model. Get that wrong and you are measuring noise instead of the signal you care about. This guide walks through abnormal returns python implementations of the three models that appear in nearly every event study: mean-adjusted, market-adjusted, and the market model — each written as a small, testable function. For a broader view of how these pieces fit together, see our guide on how to compute abnormal returns.
What an abnormal return actually measures
Define the abnormal return on day t as:
AR_t = R_t - E[R_t]
where R_t is the observed return and E[R_t] is what some model said the return should be, absent any event. Everything turns on how you build that expected-return model. The three standard approaches differ in how much information they use and what assumptions they make about the underlying return process.
Mean-adjusted says the stock's expected return equals whatever it averaged over a quiet estimation window before the event. No market exposure, no covariance — just the historical mean. It is the most assumption-free model and works well for short windows when you have a clean estimation period.
Market-adjusted subtracts the market return directly, implicitly setting alpha to zero and beta to one for every name. It asks no estimation at all, which is both its strength and its weakness: stocks with betas far from one will systematically over- or under-attribute returns to the event.
Market model runs OLS of the stock's returns on the market's returns over the estimation window, producing a fitted alpha and beta. The expected return on each event-window day is then alpha + beta * market_return. This is the standard in academic event studies because it accounts for systematic risk without requiring an asset-pricing model.
The estimation-window discipline
All three models share one non-negotiable rule: the estimation window must end before the event window begins. Using data from inside the event window to calibrate the expected-return model is look-ahead contamination — the "normal" behavior you are fitting already reflects the event you are trying to detect. A common convention is an estimation window of 120 to 250 trading days ending 10 to 30 days before the event date, with a gap of at least a few days between them.
The estimation and event windows should be passed as index slices on the same aligned DataFrame — never as separate objects that might drift out of sync. Build one joint Series, slice it, and both windows reference the same underlying data. The running an event study in Python guide covers the full pipeline if you need the surrounding scaffolding.
Setting up the aligned return series
The functions below assume you already have two pd.Series of daily returns — one for the stock, one for the market index — sharing the same DatetimeIndex. You can pull aligned return data from the Alphanume API; see the API documentation for the endpoint details. For this tutorial, we generate synthetic data so every number is reproducible.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n_days = 300
dates = pd.bdate_range("2022-01-03", periods=n_days)
# Synthetic market returns: mild drift, realistic vol
mkt_returns = pd.Series(
rng.normal(0.0004, 0.010, n_days), index=dates, name="market"
)
# Synthetic stock: beta ~1.2, alpha ~0.0002, idiosyncratic noise
true_alpha = 0.0002
true_beta = 1.2
stock_returns = pd.Series(
true_alpha + true_beta * mkt_returns.values
+ rng.normal(0, 0.012, n_days),
index=dates,
name="stock",
)
# Combine into a single aligned DataFrame
returns = pd.concat([stock_returns, mkt_returns], axis=1)
# Define window boundaries by integer position
EST_START, EST_END = 0, 249 # 250-day estimation window
EVENT_START, EVENT_END = 260, 279 # 20-day event window
Implementing the three models
Each function takes the full aligned DataFrame plus the window boundaries. All three return a pd.Series of abnormal returns over the event window, making it easy to compare them side by side.
def mean_adjusted_ar(returns, est_start, est_end, ev_start, ev_end):
"""Abnormal return = actual - mean return from estimation window."""
est = returns["stock"].iloc[est_start:est_end + 1]
expected = est.mean()
event = returns["stock"].iloc[ev_start:ev_end + 1]
return (event - expected).rename("mean_adj")
def market_adjusted_ar(returns, ev_start, ev_end):
"""Abnormal return = stock return - market return (beta assumed = 1)."""
event = returns.iloc[ev_start:ev_end + 1]
return (event["stock"] - event["market"]).rename("mkt_adj")
def market_model_ar(returns, est_start, est_end, ev_start, ev_end):
"""
Fit OLS on estimation window, then compute abnormal returns
over the event window using fitted alpha and beta.
Uses numpy.polyfit for a zero-dependency implementation.
"""
est = returns.iloc[est_start:est_end + 1]
x = est["market"].values
y = est["stock"].values
# polyfit returns [slope, intercept] for degree-1 polynomial
beta, alpha = np.polyfit(x, y, 1)
event = returns.iloc[ev_start:ev_end + 1]
expected = alpha + beta * event["market"]
return (event["stock"] - expected).rename("mkt_model")
The numpy.polyfit approach is concise for a single regressor. If you need heteroskedasticity-robust standard errors on the beta estimate — useful when you want to test whether alpha is statistically different from zero — reach for statsmodels:
import statsmodels.api as sm
def market_model_ar_ols(returns, est_start, est_end, ev_start, ev_end):
"""Market-model AR using statsmodels OLS for richer diagnostics."""
est = returns.iloc[est_start:est_end + 1]
X = sm.add_constant(est["market"])
res = sm.OLS(est["stock"], X).fit(cov_type="HC3")
print(res.summary()) # inspect R², beta t-stat, etc.
event = returns.iloc[ev_start:ev_end + 1]
X_ev = sm.add_constant(event["market"])
expected = res.predict(X_ev)
return (event["stock"].values - expected).rename("mkt_model_ols")
The HC3 covariance estimator is heteroskedasticity-robust and appropriate for daily return data. The summary() output lets you verify the beta makes economic sense before trusting the abnormal returns downstream.
Comparing the three series
Run all three and print them side by side. You should rarely see large systematic differences — if you do, check whether the stock has an unusual beta or whether the estimation window overlaps a structural break.
ar_mean = mean_adjusted_ar(
returns, EST_START, EST_END, EVENT_START, EVENT_END
)
ar_mkt = market_adjusted_ar(returns, EVENT_START, EVENT_END)
ar_model = market_model_ar(
returns, EST_START, EST_END, EVENT_START, EVENT_END
)
comparison = pd.concat([ar_mean, ar_mkt, ar_model], axis=1)
comparison.index = comparison.index.strftime("%Y-%m-%d")
pd.set_option("display.float_format", "{:.4%}".format)
print(comparison.to_string())
# Cumulative abnormal returns over the event window
car = comparison.sum()
print("\nCumulative Abnormal Returns:")
print(car.to_string())
Because our synthetic data has a true beta of 1.2, the mean-adjusted and market-adjusted series will differ from the market-model series on days when the market moves sharply — the first two methods are not controlling for that extra 20% of market sensitivity. That gap narrows as the event-window market return averages to zero, but in a volatile market environment it can be material.
When to use each model
The mean-adjusted model is appropriate when you distrust the market proxy — for instance, when studying securities in thin markets where the index is a poor factor — or when the estimation window is so long that a stable mean is plausible. Its weakness is that it ignores all covariance with market-wide shocks, so event-window abnormal returns inherit full market volatility.
The market-adjusted model is a reasonable default for large-cap equities where beta is close to one. It requires no estimation window at all, which matters when clean pre-event data is scarce — for example, studying IPOs or newly listed instruments. The cost is systematic mis-specification for any name that deviates from beta equal to one.
The market model is the standard choice when you have 120 or more days of clean estimation data, the stock is liquid enough for OLS to produce a stable beta, and you want to maximize power to detect small abnormal returns. It is the most widely published approach and the right one to use when your work will be compared against the academic literature. The additional implementation complexity is minimal — it is a one-line regression — and the improvement in precision is usually worth it.
One practical note: winsorize or trim extreme returns in the estimation window before fitting. A single circuit-breaker day with a 20% move will distort the OLS beta enough to contaminate every event-window expected return downstream. A clean estimation window matters more than a long one.