Coming from statsmodels and linearmodels¶
Most Python econometrics teaching code is written for statsmodels,
linearmodels, scikit-learn and arch. This page maps those calls to
StatsPAI and lists the places where the same request returns a different
number, with the reason. It was written by rerunning every example of
Dogan's Introduction to Econometrics with Python, a companion to Stock &
Watson, on the book's data.
The formulas carry over as they are. I(x**2), np.log(x), C(g),
a*b, a:b and x.shift(1) mean what they mean in statsmodels.
The map¶
| You wrote | Write |
|---|---|
smf.ols(f, df).fit() |
sp.regress(f, df) |
.fit(cov_type="HC1") |
sp.regress(f, df, robust="hc1") |
.fit(cov_type="cluster", cov_kwds={"groups": df["g"]}) |
sp.regress(f, df, cluster="g") |
.fit(cov_type="HAC", cov_kwds={"maxlags": 7}) |
sp.regress(f, df, robust="hac", hac_lags=7) |
res.bse, res.rsquared, res.resid |
res.std_errors, res.r2, res.residuals() |
res.f_test("x1 = 0, x2 = 0") |
sp.test(res, "x1 = 0, x2 = 0") |
res.get_prediction(new).summary_frame() |
res.predict(new, what="confidence") |
smf.probit(f, df).fit(), smf.logit |
sp.probit(f, df), sp.logit(f, df) |
res.get_margeff(at="overall") |
sp.margins(res, df, method="ame") |
res.get_margeff(at="mean") |
sp.margins(res, df, method="mem") |
PanelOLS.from_formula("y ~ x + EntityEffects", df) |
sp.panel(df, "y ~ x", entity="id", time="t", method="fe") |
... + EntityEffects + TimeEffects |
sp.panel(..., method="twoway") |
RandomEffects, PooledOLS |
sp.panel(..., method="re"), method="pooled" |
IV2SLS.from_formula("y ~ 1 + w + [x ~ z]", df) |
sp.ivreg("y ~ w + [x ~ z]", df) |
rdrobust(y=Y, x=X, c=c) |
sp.rdrobust(df, y="y", x="x", c=c) |
ARIMA(y, order=(2, 0, 0)).fit() |
sp.arima(y, order=(2, 0, 0)) |
adfuller(y, regression="ct") |
sp.unitroot(y, trend="ct") |
VAR(df).fit(2), .irf(10), .test_causality |
sp.var(df, lags=2), sp.irf, sp.granger_causality |
coint_johansen(df, 0, 3) |
sp.johansen(df, lags=3) |
VECM(df, k_ar_diff=3, coint_rank=1, deterministic="ci").fit() |
sp.vec(df, lags=3, rank=1, trend="rc") |
VECM(...).fit().predict(steps=8), .irf(10) |
fit.forecast(8), fit.irf(10), fit.fevd(10) |
coint(y, x, maxlag=2, autolag=None) |
sp.engle_granger(df, ["y", "x"], lags=2) |
AutoReg(y, lags=3).fit(), ar_select_order(y, 8) |
sp.ardl(df, "y", lags=3), lags="bic" |
ARDL(y, 2, X, {"x": 1}).fit() |
sp.ardl(df, "y", ["x"], lags=2, x_lags=1, contemporaneous=True) |
ardl_select_order(y, 4, X, 4) |
sp.ardl(df, "y", xs, lags="bic", x_lags="bic", max_lags=4, contemporaneous=True) |
res.predict(start, end, exog_oos=new) |
fit.forecast(steps=h, exog=new) |
UECM.from_ardl(m).fit().ci_params |
fit.long_run() |
pmdarima.auto_arima(y, seasonal=False) |
sp.arima(y, auto=True) |
acorr_ljungbox(e, lags=10, boxpierce=True, model_df=2) |
sp.corrgram(e, lags=10, boxpierce=True, model_df=2) |
RollingOLS(y, X, window=36).fit() |
sp.rolling("y ~ x", df, window=36) |
breaks_cusumolsresid(res.resid, ddof=k) |
sp.cusum_test(df, "y", xs, method="ols") |
bds(res.resid, max_dim=3) |
sp.bds(res, max_dim=3) |
het_breuschpagan, het_white, het_arch, acorr_breusch_godfrey |
sp.estat(res, "hettest" / "white" / "archlm" / "bgodfrey") |
reset_ramsey(res, degree=5) |
sp.estat(res, "reset", powers=5) |
res.get_influence().cooks_distance |
sp.influence_measures(res)["cooksd"] |
stats.shapiro(res.resid), stats.ttest_1samp(x, 0.5) |
sp.swilk(res.residuals()), sp.ttest(x, mu=0.5) |
arch_model(r, vol="Garch", p=1, q=1).fit() |
sp.garch(r, p=1, q=1, vce="robust") |
RidgeCV, LassoCV, PCA + LinearRegression |
sp.shrinkage(df, y, x, method="ridge" / "lasso" / "pcr") |
summary_col([r1, r2]), Stargazer([r1, r2]) |
sp.regtable(r1, r2) |
A name from one of those libraries that is not a StatsPAI name says where
to go. Asking the package for vecm raises "it is sp.vec here", and the
same holds for IV2SLS, adfuller, arch_model and for the result
attributes bse, rsquared and f_test.
Where the numbers differ, and why¶
Coefficients agree to machine precision in every row above. These are the differences in what is reported around them.
Confidence intervals and p-values after robust errors. statsmodels
switches to the normal distribution when cov_type is not "nonrobust".
StatsPAI keeps the t distribution with the residual degrees of freedom, as
Stata does. With 420 observations the interval is wider by 0.3%.
cov_type="HC1" on probit and logit. statsmodels accepts the
argument and returns the sandwich without the N / (N - K) factor, which
is HC0. sp.probit(..., robust="hc0") reproduces it to 1e-14.
robust="hc1" applies the factor the name promises, and
robust="robust" gives Stata's N / (N - 1).
Clustered errors in a fixed-effects panel. The usual linearmodels
formula spells the intercept, y ~ 1 + x + EntityEffects, and counts it
as a regressor in the small-sample factor. sp.panel has no intercept to
count. The standard error differs in the fourth digit (0.28923 against
0.28880 on the traffic-fatality panel). Without the 1 + the two agree,
and with entity and time effects they agree either way. ssc="stata"
gives the factor of xtreg, fe.
cov_type="robust" in IV2SLS. It is HC0. Use robust="hc0" to
match it; robust="robust" is HC1, as in Stata's ivregress ..., small.
VAR. statsmodels divides the residual covariance by T - Kp - 1.
sp.var follows Stata's var and divides by T, so standard errors,
orthogonalised impulse responses and information criteria differ by that
factor. Coefficients and forecasts are identical. The information
criteria are also on Stata's scale (they include the constant of the
Gaussian likelihood), and the lag they select is the same.
Johansen critical values. statsmodels uses the MacKinnon, Haug and
Michelis values (15.49 and 3.84 for two series). sp.johansen prints the
Osterwald-Lenum values Stata's vecrank uses (15.41 and 3.76). The trace
statistics are identical.
GARCH. arch starts the variance recursion from an exponentially
weighted backcast and StatsPAI from the sample variance, as Stata does.
On 7,055 daily returns the estimates differ in the third or fourth
digit. arch
reports Bollerslev-Wooldridge standard errors by default, which is
vce="robust" here.
Cross-validation. Textbook code often standardises the predictors
once and then splits into folds. sp.shrinkage standardises inside each
training fold. Its penalty multiplies the sum of squared residuals, so
ridge is Ridge(alpha=penalty) and the lasso is
Lasso(alpha=penalty / (2 * n)).
fit(vcov="HC1"). statsmodels ignores keyword arguments it does
not know, so this misspelling of cov_type returns classical standard
errors without a word (the book's chapter 12 has five such calls).
sp.regress(f, df, vcov="HC1") computes HC1, and an option StatsPAI does
not know raises.
ARIMA on a differenced series. statsmodels starts the level of an
integrated series from a normal prior with variance 1e6. That is diffuse
for a series whose innovations have a variance of 1, and informative for
one measured in thousands. On quarterly U.S. GDP in billions of dollars
ARIMA(gdp, order=(1, 1, 0)) gives an AR coefficient of 0.17, 0.22 with
GDP in trillions and 0.02 in millions. sp.arima uses the exact diffuse
initialisation and returns 0.216 in every unit, with the log-likelihood
of R's arima(method="ML") and Stata's arima. AIC and BIC use the
number of observations left after differencing.
Automatic ARIMA. sp.arima(auto=True) runs the exhaustive search of
R's forecast::auto.arima(stepwise=FALSE, approximation=FALSE): d by
KPSS tests, then every (p, q) with and without a constant or drift,
ranked by AICc, dropping fits with a root on the unit circle. pmdarima
defaults to the stepwise search, which can stop at a different model.
AutoReg and ARDL standard errors. Both divide the residual sum of
squares by n. sp.ardl is an OLS regression and divides by n - k, so
its classical standard errors and forecast intervals are wider by
sqrt(n / (n - k)). Coefficients and point forecasts are identical.
Lag selected by adfuller. statsmodels searches up to
ceil(12 (T / 100) ** 0.25) lags and StatsPAI up to the floor of the same
number, which is Schwert's rule. When the two differ and AIC picks the
extra lag, pass max_lags= to reproduce adfuller; the statistic and the
p-value then agree exactly.
coint. It chooses the lag of the residual regression by AIC.
sp.engle_granger uses a fixed rule unless lags= is given. With the
same lag the statistic and the MacKinnon p-value are identical.
breaks_cusumolsresid. Its default divides the residual sum of
squares by n. sp.cusum_test(method="ols") divides by n - k, as R's
strucchange and Stata's estat sbcusum, ols do; ddof=k in
statsmodels gives the same number.
Quantile regression. quantreg iterates reweighted least squares and
stops slightly above the minimum of the check function; its coefficients
differ from the exact solution in the fifth digit. sp.qreg solves the
linear programme. Standard errors differ by design: statsmodels
defaults to a kernel sandwich, sp.qreg to Stata's vce(iid).
variance_inflation_factor. It regresses each column of the array it
is given on the others, with no constant added. Called on the regressors
alone, as textbook code often does, it returns uncentred factors.
sp.vif includes the constant, which is variance_inflation_factor on
the design matrix that has one.
Granger causality. sp.granger_causality reports the Wald statistic
and, as its F, that statistic over its degrees of freedom, which is what
Stata's vargranger prints after var, small. The F of
grangercausalitytests and test_causality(kind="f") uses the residual
variance with divisor T - m and is smaller by (T - m) / T; fit the VAR
with se_df="r" to get it.
sp.acf. It is the Ackerberg-Caves-Frazer production-function
estimator. Autocorrelations and the Ljung-Box statistics are sp.corrgram.
A short session¶
import numpy as np
import pandas as pd
import statspai as sp
rng = np.random.default_rng(0)
n = 500
z, w, u = rng.normal(size=(3, n))
x = 0.8 * z + 0.4 * w + 0.6 * u + rng.normal(size=n)
inc = np.exp(rng.normal(size=n))
df = pd.DataFrame({
"y": 1 + 0.7 * x - 0.5 * w + 0.3 * inc - 0.02 * inc**2 + u,
"x": x, "w": w, "z": z, "inc": inc,
})
ols = sp.regress("y ~ x + w + inc + I(inc**2)", df, robust="hc1")
print(sp.test(ols, "inc = 0, I(inc**2) = 0")["pvalue"]) # joint F test
print(ols.predict(df.head(3), what="confidence")) # with intervals
iv = sp.ivreg("y ~ 1 + w + inc + [x ~ z]", df, robust="hc0")
print(sp.regtable(ols, iv))
cols = ["x", "w", "z", "inc"]
fit = sp.shrinkage(df.iloc[:400], "y", cols, method="lasso")
print(fit.summary())
print(fit.rmspe(df.iloc[400:])) # hold-out RMSPE