The MIDAS model¶
This page covers the single-indicator
MIDAS class: how to specify it,
estimate it (OLS vs NLS), add AR lags and outlier dummies, fit multiple
horizons in one call, and produce out-of-sample forecasts.
Model¶
The direct \(h\)-step MIDAS regression of a low-frequency target \(y_{t+h}\) on a single high-frequency indicator \(x_{t,j}\) (\(j=0,\dots,K-1\)) is
- \(K\) =
n_lagshigh-frequency lags per low-frequency observation. - \(w_h(j;\theta_h)\) = weight scheme controlled by a small number of shape parameters \(\theta_h\).
- \(\mathbf{d}_{t+h}\) = optional outlier-dummy block.
- \(p\) =
n_ar_lagsautoregressive lags of the target. - All parameters are horizon-specific (one set per \(h\)).
Input data¶
fit takes two 2-column DataFrames — a quarterly target and a
monthly regressors frame, each with exactly the columns date and
value. The frequencies are inferred from the date spacing:
# illustrative shape — build these from your own data
target = pd.DataFrame({"date": q_dates, "value": y}) # quarterly
regressors = pd.DataFrame({"date": m_dates, "value": x}) # monthly
The examples on this page use the built-in simulator so every block runs as-is. Each later block continues from the names defined here:
from nowcast_midas import MIDAS
from nowcast_midas.utils import sample_data
# date/value quarterly target + date/value monthly indicator
target, regressors = sample_data(n_obs=100, seed=42)
Specifying the model¶
from nowcast_midas import MIDAS
model = MIDAS(
method="almon", # 'almon', 'exp_almon', 'beta', 'unrestricted'
n_lags=6, # high-frequency lags per low-frequency obs
n_pars_weights=2, # shape parameters of the weight scheme
estimator=None, # 'ols' / 'nls' (auto-selected from method)
horizons=[0, 1, 4], # explicit list of horizons to fit (default None -> [0])
start_lag=0, # shift the lag window forward by k high-frequency periods
n_ar_lags=0, # autoregressive lags of y (0 = no AR term)
dummy_periods=None, # list[pd.Timestamp] of outlier quarters
)
horizons here is a list, not a count
MIDAS(horizons=...) takes an explicit list of horizon indices to fit
(None is treated as [0], i.e. nowcast only). This is different from
MidasCombo(horizons=...), which takes an int — the number of
application forecast steps to produce.
Weighting schemes (method)¶
| method | weight \(w(j;\theta)\) | parameters | typical estimator |
|---|---|---|---|
unrestricted |
\(\theta_j\) | \(K\) | OLS |
almon |
\(\sum_{i=0}^{P-1} \theta_i\, j^{i}\) | n_pars_weights |
OLS (linear in \(\theta\)) |
exp_almon |
\(\exp\!\big(\theta_1 j + \theta_2 j^2\big) / Z\) | n_pars_weights |
NLS |
beta |
Beta-density profile on \([0,1]\) | 2 | NLS |
unrestricted and almon are linear in their parameters, so the
model reduces to a closed-form least-squares problem (estimator='ols').
exp_almon and beta are non-linear; the package estimates them by
least_squares with an analytical Jacobian (estimator='nls').
See the weighting schemes page for full definitions.
Estimator (estimator)¶
None(default) —olsforalmon/unrestricted,nlsotherwise.'ols'— only valid foralmon/unrestricted; aValueErroris raised otherwise.'nls'— always available; uses Levenberg-Marquardt with an analytical Jacobian. This method is slower but required for the non-linear schemes.
Autoregressive lags (n_ar_lags)¶
Setting n_ar_lags=p appends \(p\) lags of the dependent variable
\(y_{t+h-1},\dots,y_{t+h-p}\) to the regressor block. The coefficients
\(\phi_{h,k}\) are estimated jointly with the MIDAS parameters
(closed-form OLS for almon/unrestricted, NLS otherwise) and
available as fit.phi. At forecast time the model automatically picks
up the most recent \(p\) target values strictly before the latest available
regressor date \(T_X\).
Outlier dummies (dummy_periods)¶
dummy_periods=[ts1, ts2, ...] adds one binary column per listed
quarter to the design matrix. The estimated coefficients live in
fit.gamma and represent the additive shift on \(y_{t+h}\) at those
quarters. Dummies travel with the target (so they are aligned with
\(y_{t+h}\), not with \(X_t\)).
Fitting and direct multi-horizon estimation¶
For each \(h\) in horizons, fit() independently estimates a model on
the pair \((y[h:],\, X[:T-h])\) — i.e. the direct \(h\)-step alignment
\(y_{t+h} \sim X_t\). Each fit is stored in model.fits_:
model.fits_[0] # 1qa model: y[t] ~ X[t]
model.fits_[1] # 2qa model: y[t+1] ~ X[t]
model.fits_[4] # 5qa model: y[t+4] ~ X[t]
Each entry is a FittedMidas dataclass with:
| attribute | shape | meaning |
|---|---|---|
alpha |
scalar | intercept \(\alpha_h\) |
beta |
scalar | scale \(\beta_h\) (always \(1\) in OLS, absorbed into weights) |
theta |
\((\text{n\_pars\_weights},)\) | weight-shape parameters \(\theta_h\) |
weights |
(n_lags,) |
resolved weight vector \(w_h(\cdot)\) |
gamma |
(n_dummies,) |
dummy coefficients |
phi |
(n_ar_lags,) |
AR coefficients |
fitted_values |
(T_h,) |
\(\hat y\) on the valid rows |
residuals |
(T_h,) |
\(y - \hat y\) |
nobs |
int | number of observations used |
y, X |
arrays | the data fitted at this horizon |
The model drops rows with missing required lags, including rows affected by
insufficient monthly data or AR warm-up. model.valid_mask_ records the
target rows used for estimation.
Forecasting¶
forecast() produces a long-format DataFrame with columns
date, horizon, spec, value — one row per fitted horizon. spec
is the indicator name (the regressor variable value, or "target"
when the frame has no variable column):
date horizon spec value
0 2025-03-31 0 target 1.23
1 2025-06-30 1 target 1.18
2 2026-03-31 4 target 1.05
Mechanics:
- The latest available regressor date \(T_X\) is the latest date in
regressors_forecastwith a finite value. This date supplies the most timely regressor observation; the model applies no additional cap. - Direct forecasting fits \(y_{t+h} \sim X_t\), so for each horizon \(h\) the
lag row is built at \(T_X\) and the forecast target date is
\(T_X + h\) quarters (
latest_regressor_date + h * 3 months). - Equivalently, to forecast \(y_{T+\text{step}}\) where \(T\) is the last observed target, the horizon used is the gap between the forecast date and the latest regressor: \(h = (T + \text{step}) - T_X\).
- If the model uses dummies, the dummy row is built at the forecast target date (consistent with the in-sample alignment).
- If the model uses AR lags, the \(p\) most recent target values at dates strictly before the latest available regressor date are pulled from the training target and used as \(y_{t-1}, \dots, y_{t-p}\).
To get a true OOS forecast at \(h=0\) the regressors must extend beyond the training period; for \(h>0\) the model already embeds the lead, so a forecast is OOS even with training-period regressors.
Long-format outputs¶
fit() also stores the in-sample fitted values as
model.fits_df_ (date, horizon, value), and forecast() stores its
return value as model.forecasts_df_ (date, horizon, spec, value).
Forecast decomposition¶
Splits every horizon's point forecast into additive components
(intercept, the weighted MIDAS block, dummy_i, ar_lagk) that sum
back to the value returned by forecast(). Columns: horizon, date,
component, contribution, weight. See
Interpreting decompositions.
Inspecting and plotting¶
model.summary() # text summary of every fitted horizon
model.summary(horizon=0) # only h=0
model.plot_fit(horizon=0) # in-sample fit
model.plot_weights(horizon=0) # resolved weight vector w(j;θ)
Synthetic data with a built-in DGP¶
utils.sample_data(horizon=h, ...) simulates a target that follows the
MIDAS DGP at horizon \(h\):
Fitting MIDAS(..., horizons=[h]) on this data should recover the
generating parameters and produce essentially zero residuals when
noise=0. The package's exact-recovery tests rely on this.