What you're actually building
Econometrics is statistics with an economic question attached. The core tool, and the one that gets you 80% of the way to understanding everything built on top of it, is ordinary least squares (OLS) regression: fitting a line (or hyperplane, with multiple variables) through your data that minimizes the squared distance between your predictions and reality. Everything else — fixed effects, instrumental variables, panel models — is a variation built to fix a specific way OLS can go wrong.
This guide builds one real model end to end: predicting how much an early-stage startup raises based on observable traits at the time of the raise. The dataset is synthetic (generated in the code below, so you can run every line yourself), but the workflow — fit, check assumptions, fix what's broken, interpret honestly — is identical to what you'd do with real data.
Setup
Three libraries do essentially everything: pandas for data handling, numpy for the maths, statsmodels for the actual regression and its diagnostics (it reports far more statistical detail than scikit-learn, which is built for prediction rather than inference).
pip install pandas numpy statsmodels matplotlib
Step 1 — Get data into a DataFrame
Real data comes from a CSV, an API, or a database query. To make this guide fully reproducible, here's a synthetic dataset with a funding round size that depends on four founder/company traits, plus some realistic random noise:
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 300
df = pd.DataFrame({
"months_since_founding": rng.uniform(3, 36, n),
"monthly_revenue_k": rng.gamma(2, 8, n), # £k/month, right-skewed
"team_size": rng.integers(2, 25, n),
"prior_funding_k": rng.gamma(1.5, 40, n), # £k raised previously
})
# True relationship (unknown in real life -- this is what we're trying to recover)
noise = rng.normal(0, 60, n)
df["raise_size_k"] = (
30
+ 4.5 * df["months_since_founding"]
+ 8.0 * df["monthly_revenue_k"]
+ 12.0 * df["team_size"]
+ 0.6 * df["prior_funding_k"]
+ noise
).clip(lower=10) # can't raise a negative amount
print(df.describe())
Step 2 — Fit the model
statsmodels wants an explicit intercept, so you add a constant column before fitting — easy to forget, and it silently changes what your coefficients mean if you do.
import statsmodels.api as sm
X = df[["months_since_founding", "monthly_revenue_k", "team_size", "prior_funding_k"]]
X = sm.add_constant(X)
y = df["raise_size_k"]
model = sm.OLS(y, X).fit()
print(model.summary())
The output you actually care about, reading top to bottom: R-squared (what fraction of the variation in raise size this model explains), each coefficient's estimate (the effect of a one-unit change in that variable, holding the others fixed), and its p-value (roughly, how likely you'd see an effect this large by chance alone if the true effect were zero — under 0.05 is the conventional, if arbitrary, bar).
Step 3 — Check the assumptions before you trust any of it
A regression that runs without errors will still hand you a summary table full of numbers even when its core assumptions are badly violated. Python won't warn you. Three checks catch most real-world problems.
Multicollinearity — when your predictor variables are highly correlated with each other, the model can't cleanly separate their individual effects, and your coefficients become unstable (small changes in the data swing them wildly). The variance inflation factor (VIF) catches this; a common rule of thumb flags anything above 10.
from statsmodels.stats.outliers_influence import variance_inflation_factor
vif_data = pd.DataFrame()
vif_data["variable"] = X.columns
vif_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
print(vif_data)
Heteroscedasticity — when the size of your prediction errors changes systematically across the range of your data (e.g. the model is far less accurate for large raises than small ones). It doesn't bias your coefficients, but it does make your p-values and confidence intervals wrong. The Breusch-Pagan test gives you a formal check:
from statsmodels.stats.diagnostic import het_breuschpagan
bp_test = het_breuschpagan(model.resid, model.model.exog)
labels = ["LM stat", "LM p-value", "F stat", "F p-value"]
print(dict(zip(labels, bp_test)))
Normality of residuals — OLS's confidence intervals and p-values rely on the errors being roughly normally distributed. A quick visual check beats a formal test here, because it also shows you why it fails, not just that it does:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax[0].hist(model.resid, bins=30)
ax[0].set_title("Residual distribution")
sm.qqplot(model.resid, line="45", ax=ax[1])
ax[1].set_title("Q-Q plot")
plt.tight_layout()
plt.show()
Step 4 — When a check fails
Each of these has a standard fix, and none of them require starting over:
- High VIF: drop or combine the correlated variables, or accept that you can only interpret the group's combined effect, not each one individually.
- Heteroscedasticity: refit with heteroscedasticity-robust standard errors — one extra argument, no change to your data:
model = sm.OLS(y, X).fit(cov_type="HC3") - Skewed residuals from a skewed target: log-transform the dependent variable (
np.log(df["raise_size_k"])) and re-fit — very common for anything money-related, since financial quantities are almost never normally distributed in levels.
Reading the result honestly
A statistically significant coefficient tells you the relationship is unlikely to be pure noise in this dataset, under the model's assumptions. It does not tell you the relationship is causal, that it will hold in a different market or time period, or that the variables you left out don't matter more than the ones you included. The single most common misuse of a regression like this one is presenting an association as if it were a mechanism — "more team members causes bigger raises" — when the honest reading is closer to "in this sample, bigger raises are associated with larger teams, for reasons this model can't distinguish."
That caveat isn't a reason to skip the analysis. A well-specified, honestly-interpreted regression is still one of the sharpest tools available for turning a pile of numbers into a testable claim about how something works — which is exactly why it's worth learning to do properly rather than pasting into a black-box AutoML tool and trusting whatever comes out.