Results objects¶
Every tsecon estimator returns a plain dict of documented keys. That is a
deliberate design choice — dicts are transparent, serialisable, and impossible to
get locked out of.
tsecon.results adds an opt-in layer on top: objects that carry the same
data but can also render themselves — a .summary() an economist can read, and
.plot_*() methods for the standard figures.
tsecon.summarize — a readable view of any result¶
Six families (VAR, LP, GARCH, ARIMA, predictive-regression, DSGE) have
hand-written summaries. For everything else, tsecon.summarize(result) renders
any returned dict as an aligned, honest report — scalars in a key/value block,
small matrices as shaped tables, large arrays summarized by shape and range,
nested dicts one level deep. It never invents or hides a key, and its header
says "generic Result" so it is never mistaken for a bespoke model summary.
import numpy as np, tsecon
y = np.cumsum(np.random.default_rng(0).standard_normal(120))
print(tsecon.summarize(tsecon.adf(y), title="adf"))
====================================================================
adf — generic Result (5 fields)
====================================================================
statistic -0.794469
p_value 0.820741
used_lag 0
nobs 119
--------------------------------------------------------------------
crit (dict, 3 entries)
1% -3.486535
5% -2.886151
10% -2.579896
====================================================================
Pass a bespoke tsecon.results object and it is returned unchanged (you keep
the better summary); summarize deliberately does not guess a bespoke class
from a raw dict, because the raw dict has already dropped the labels and data a
rich summary needs, and shape-guessing collides. The result is still a dict
subclass, so the plain-data contract below holds.
Forgiving input — pandas and array-likes¶
Every estimator accepts a pandas DataFrame/Series (or any object exposing
.to_numpy, e.g. polars), float32/float16/non-contiguous arrays, integer
and boolean arrays (data read as int, a 0/1 recession dummy, a y > 0 mask),
and a plain Python list of numbers. All are converted to float64 at the
boundary.
The conversion is parameter-aware, so it never touches an argument that is not
data. Passing through untouched: the four integer label/index parameters
(hetero_svar.regime_labels, var_granger.caused/causing,
favar.slow_indices), restriction-tuple specs, tuple-valued options such as
box_cox_lambda(bounds=...), and callables. Ragged panel lists keep their list
container while each per-unit array is converted, so a list of Series works.
Two things are deliberately not guessed at. A nested Python list is left
alone, because [(0, 1), (0, 2)] is a restriction spec and [[1.0, 2.0], [3.0,
4.0]] is a matrix and nothing in the value distinguishes them — wrap a literal
matrix with np.array(...). And an argument of the wrong rank raises a message
naming the shapes it received and what to do about it, rather than a low-level
type error.
import pandas as pd
df = pd.DataFrame(np.random.default_rng(0).standard_normal((200, 3)),
columns=["gdp", "cons", "inv"])
tsecon.var_fit(df, lags=2) # a DataFrame in — no .to_numpy() needed
The key property:
A results object is a dict
Every class here subclasses dict. res["params"], isinstance(res, dict),
json, pickle, and **res unpacking all keep working exactly as before —
a results object only adds methods. Adopting them is additive, never a
breaking change, and you can always drop back to plain data with
res.to_dict().
import tsecon
from tsecon.results import VARResults
# the compiled function — unchanged, returns a plain dict
raw = tsecon.var_fit(data, lags=2)
# the same estimation, wrapped
fit = VARResults.fit(data, lags=2, names=["gdp", "infl", "rate"])
fit["aic"] # still a dict
set(fit) == set(raw) # True — identical keys
print(fit.summary()) # ...that can also render itself
fit.irf(horizon=12).plot()
The compiled functions are not shadowed: tsecon.var_fit remains the
compiled builtin returning a plain dict. tsecon.results is a namespace you
reach into deliberately.
Plotting is optional¶
matplotlib is an optional dependency. Nothing imports it until you call a plot method, and if it is missing the error tells you what to install:
What's available¶
| Class | Wraps | Highlights |
|---|---|---|
VARResults |
var_fit |
coefficient matrix, stability verdict, .irf() |
IRFArray |
var_irf |
a list subclass — .response(i, j), .plot() grid |
LPResults |
lp |
per-horizon table, .conf_int(), .peak(), .plot_irf() |
GARCHResults |
garch_fit |
robust SEs, .persistence(), .plot_volatility() |
ARIMAResults |
arima_fit |
.forecast_frame(), fan-chart .plot_forecast() |
PredictiveRegressionResults |
predictive_regression |
three estimators side by side |
IVXTestResults |
ivx_test |
joint IVX test |
DSGEResults |
dsge_solve |
Blanchard-Kahn verdict, .impulse_response() |
VARResults¶
====================================================================
VAR(2) — 2 equations, trend='c' — stable
====================================================================
llf -339.646 aic -2.7376 bic -2.5917 hqic -2.6788
reciprocal roots — min 1.6438 max 11.9838 (stable iff min > 1)
--------------------------------------------------------------------
coefficients — rows = regressors, cols = equations
regressor gdp infl
--------------------------------------------------------------------
const +0.00096 +0.04627
L1.gdp +0.52077 +0.06563
L1.infl +0.04191 +0.41989
L2.gdp -0.00972 +0.06450
L2.infl +0.03986 +0.00631
====================================================================
The stability line takes its verdict from is_stable and states the convention
explicitly — these are reciprocal characteristic roots, so stability means
min > 1. max_root alone is not a verdict (an explosive system can still have
max_root > 1); see the VAR/SVAR card.
IRFArray subclasses list, so irf[h][i][j], len(), slicing and
np.array(irf) behave exactly as the bare nested list always did — it just gains
.response("gdp", "infl"), a readable repr, and .plot() for the k×k
small-multiple grid.
PredictiveRegressionResults¶
The most useful summary in the library: one regression, three estimators, and an explicit statement of which p-value to trust.
====================================================================
Predictive regression r(t+1) = a + b*x(t) IVX p = 0.4083
====================================================================
nobs 399 rho(x) 0.9713 IVX rz 0.9966
intercept +0.21606 Stambaugh bias removed +0.00902
--------------------------------------------------------------------
estimator beta std err test stat p-value
--------------------------------------------------------------------
OLS +0.01867 0.01112 t +1.680 0.0930
Stambaugh +0.00966 0.01112 t +0.869 0.3851
IVX +0.01415 0.01711 W 0.6838 0.4083
--------------------------------------------------------------------
Report the IVX Wald p-value (0.4083). It is valid whatever the
persistence of x; the OLS t over-rejects when x is persistent,
so the OLS and Stambaugh p-values above are naive normal ones.
WARNING: rho(x) = 0.9713 > 0.95, so x is highly persistent
and the OLS t-statistic is unreliable here.
At the 5% level IVX does not reject b = 0: no evidence
that x predicts r(t+1).
====================================================================
.significant(level=0.05) reads the IVX p-value, not the OLS t — the
Monte Carlo suite shows why: at a unit root the OLS
t-test rejects a true null 28% of the time.
DSGEResults¶
====================================================================
Linear RE model (Blanchard-Kahn): unique stable solution
====================================================================
predetermined 1 jump 1 shocks 1 determinate yes
verdict: unique stable solution (1 unstable eigenvalue(s) = 1 jump
variable(s))
--------------------------------------------------------------------
eigenvalue moduli: stable (<1) 1 unstable (>1) 1
stable 0.60000
unstable 1.42857
--------------------------------------------------------------------
G policy: jump = G . predetermined [1x1]
+1.72414
P transition: k(t+1) = P . k(t) + Q . z [1x1]
+0.60000
Q impact: shock loading on the state [1x1]
+1.00000
====================================================================
.impulse_response(horizon=24) traces the saddle path by iterating
k_{t+1} = P·k_t from impact Q·shock and reading jumps off x_t = G·k_t — the
compiled binding returns matrices only, so this accessor genuinely adds
capability rather than just formatting.
Design note: why a dict subclass¶
The alternative — a bespoke Results class with .params attributes — would
have made the plain-dict contract a legacy path the moment it shipped, forcing
either a breaking change or two parallel APIs forever.
Subclassing dict sidesteps that entirely: the dict contract is preserved
as a subset of the richer object, so the library can add rendering
incrementally, per family, without ever invalidating code that treats results as
data. It also keeps results trivially serialisable, which matters for Monte Carlo
work where results are written to disk by the thousand.
This layer earned its keep before it shipped: writing a correct stability line
for VARResults.summary() is what exposed that var_fit was reporting the wrong
characteristic root — a bug that had propagated into three documentation pages.