Dynamic Treatment Effects

Regression blips, parallel-trends SNMMs, and dynamic covariate balance

Dynamic causal estimators must respect the order in which treatment, outcomes, and time-varying confounders are observed. Two methods below parameterize the effect of a treatment blip at one time on later outcomes; the third estimates a mean final outcome under an entire treatment path. All avoid interpreting an ordinary autoregressive distributed-lag regression as causal when it conditions on variables caused by earlier treatment.

This page documents three deliberately distinct estimators:

Two timing conventions

The methods use different observation orderings, so their array contracts are intentionally different.

Contemporaneous regression blips

RegressionBlip treats A_t as capable of affecting Y_t. Both arrays therefore have shape (n_units, n_periods). Let \bar A_t=(A_0,\ldots,A_t). For an additive, history-invariant impulse response,

b_t(\bar a_t,j;\gamma) = E\left[ Y_t(\bar a_{t-j},\bar 0_j)-Y_t(\bar a_{t-j-1},\bar 0_{j+1}) \mid \bar A_{t-j}=\bar a_{t-j} \right] =\gamma_j a_{t-j}.

After estimating lags 0,\ldots,j-1, define the partially blipped-down outcome

\widetilde Y_t^{\,j} =Y_t-\sum_{s=0}^{j-1}\widehat\gamma_s A_{t-s}.

At stage j, the implementation regresses \widetilde Y_t^{\,j} on A_{t-j} and information available when A_{t-j} was assigned: A_{t-j-1}, Y_{t-j-1}, optional user-supplied history L_{t-j}, and optional outcome-time indicators. The coefficient on A_{t-j} is \widehat\gamma_j. The recursion avoids conditioning on L_t when estimating the effect of A_{t-j} if L_t could have been caused by that treatment.

The identifying restriction is sequential ignorability. For every treatment history and assignment time s,

\{Y_r(\bar a_r):r\ge s\}\perp A_s\mid V_s,

where V_s contains the treatment and covariate history sufficient to remove confounding of A_s. The assumption permits A_s to affect future covariates and future treatment.

Orthogonal estimating equation

Let \pi_m(L_m)=E(A_m\mid L_m,\bar A_{m-1}) and

\nu_{mk}(L_m;\psi)=E\{H^\dagger_{mk}(\psi)\mid L_m,\bar A_{m-1}\}.

Shahn et al.’s doubly robust moment is

U_{mk}(\psi) =s_{mk}(L_m,\bar A_{m-1}) \{A_m-\pi_m(L_m)\} \{H^\dagger_{mk}(\psi)-\nu_{mk}(L_m;\psi)\}.

Its expectation is zero if either the conditional treatment mean or the conditional blipped-trend mean is correctly specified. For a linear blip, write

H^\dagger_{mk}(\psi)=\Delta Y_k-D_{mk}'\psi.

The implementation separately residualizes \Delta Y_k and D_{mk} on the history features. This produces a linear orthogonal equation,

\sum_{i,m,k} Z_{imk}\left[ \{\Delta Y_{ik}-\widehat q_{mk}(L_{im})\} -\{D_{imk}-\widehat r_{mk}(L_{im})\}'\psi \right]=0,

where Z_{imk} places A_{im}-\widehat\pi_m(L_{im}) in the coordinate for horizon k-m. Treatment, trend, and blip-design conditional means are fitted with ridge regressions on training folds and evaluated on held-out units. The covariance is an influence-function sandwich over unit-level sums of the orthogonal scores.

Dynamic covariate balance

Viviano and Bradic target the mean final potential outcome under a complete treatment path,

\mu_T(d_{1:T})=E\{Y_T(d_{1:T})\}.

Let H_{it} contain information observed before A_{it}, including baseline covariates, prior treatment, and any relevant lagged outcomes or time-varying covariates. Their potential local-projection model is

E\{Y_{iT}(d_{1:T})\mid H_{it}(d_{1:t-1})\} =H_{it}(d_{1:t-1})'\beta_d^{(t)}.

The fitted projections are obtained backwards. At t=T, regress Y_T on H_T among units with A_{1:T}=d_{1:T}. At every earlier t, regress the next fitted conditional mean H_{t+1}'\widehat\beta_d^{(t+1)} on H_t among units with A_{1:t}=d_{1:t}. The implementation uses ridge regularization with an unpenalized intercept; the paper’s high-dimensional theory instead permits appropriately convergent lasso projections.

Starting from \widehat\gamma_{i0}=1/n, period-t weights solve the exact-balance special case

\begin{aligned} \widehat\gamma_t =\arg\min_{\gamma\in\mathbb R^n}\;&\sum_{i=1}^n\gamma_i^2\\ \text{subject to }\;& \sum_i\gamma_iH_{it} =\sum_i\widehat\gamma_{i,t-1}H_{it},\\ &\sum_i\gamma_i=1,\qquad 0\leq\gamma_i\leq w_{\max},\\ &\gamma_i=0\quad\text{if }A_{i,1:t}\neq d_{1:t}. \end{aligned}

Thus the first-period path group is balanced to the whole sample. At later periods, the longer path group is balanced to the preceding path group after applying the previous period’s weights. DynamicCovariateBalance calls the same autoscaling, bounded quadratic dual map, solver sequence, normalization, and diagnostics used by BalancingWeights(objective="quadratic"); this is one shared calibration engine rather than a second weighting implementation.

Given the recursive predictions \widehat q_{it}=H_{it}'\widehat\beta_d^{(t)}, the augmented path-mean estimator is

\widehat\mu_T(d) =\sum_i\widehat\gamma_{iT}Y_{iT} -\sum_{t=2}^T\sum_i (\widehat\gamma_{it}-\widehat\gamma_{i,t-1})\widehat q_{it} -\sum_i(\widehat\gamma_{i1}-1/n)\widehat q_{i1}.

The paper permits coordinatewise \ell_\infty balance slack and explicit high-dimensional tuning. The first package implementation uses exact balance, ridge projections, and the existing scalar weight cap. It reports calibration diagnostics but deliberately does not report an analytic standard error yet; Monte Carlo dispersion or a unit bootstrap is required for uncertainty until the paper’s longitudinal influence-function variance is implemented.

API

import crabbymetrics as cm

# Blackwell--Glynn recursive regression blips.
reg = cm.RegressionBlip(max_lag=2, time_effects=True)
reg.fit(y_same_period, treatment_same_period, history)
reg.coef
reg.stage_standard_errors
reg.blip_down(y_same_period, treatment_same_period)
reg.summary()

# Shahn et al. parallel-trends SNMM.
pt = cm.ParallelTrendsSNMM(
    max_horizon=3,
    treatment_mode="initiation",
    n_folds=3,
    nuisance_penalty=1e-6,
    propensity_clip=0.01,
    seed=42,
)
pt.fit(y_forward, absorbing_treatment_status, history)
pt.coef
pt.standard_errors
pt.summary()

# Viviano--Bradic dynamic covariate balance for one treatment path.
dcb = cm.DynamicCovariateBalance(
    nuisance_penalty=1e-6,
    autoscale=True,
    max_weight=1.0,
)
dcb.fit(final_outcome, treatment, history, target_path=[0, 1, 1, 1])
dcb.potential_outcome
dcb.get_weights()  # n_units by n_periods
dcb.summary()

treatment_mode="blip" uses the supplied period-specific treatment directly. treatment_mode="initiation" requires a binary absorbing treatment-status panel, constructs a first-treatment pulse internally, and restricts each assignment-time equation to units not treated before that time. In initiation mode, \psi_h is the effect at horizon h of initiating at m rather than remaining untreated from m onward, among the relevant initiation history.

DynamicCovariateBalance.fit(...) takes only the final outcome because its estimand is a path-specific final mean. The treatment panel and target path have the same number of periods; history[:, t, :] must contain only pre-treatment information for period t. A path contrast requires two fitted objects, one for each path. summary() reports prefix support, effective sample size, maximum original-scale imbalance, per-period solver status, and the fitted path mean.

Numerical experiment 1: post-treatment covariate feedback

The first design makes Z_t a mediator of A_{t-1} and a confounder for A_t:

Z_t=0.8A_{t-1}+U_t, \qquad \Pr(A_t=1\mid Z_t,A_{t-1})=\operatorname{logit}^{-1}(-0.2+0.8Z_t+0.2A_{t-1}),

Y_t=2A_t+0.5A_{t-1}+1.5Z_t+\varepsilon_t.

The contemporaneous blip is \gamma_0=2. The first-lag blip includes both the direct path and the mediated path through Z_t:

\gamma_1=0.5+1.5(0.8)=1.7.

Code
import numpy as np
import pandas as pd
import crabbymetrics as cm

rng = np.random.default_rng(1234)
n, periods = 2_000, 7
a = np.zeros((n, periods))
y = np.zeros((n, periods))
z = np.zeros((n, periods, 1))

for t in range(periods):
    a_lag = a[:, t - 1] if t else 0.0
    z[:, t, 0] = 0.8 * a_lag + rng.normal(size=n)
    p = 1 / (1 + np.exp(-(-0.2 + 0.8 * z[:, t, 0] + 0.2 * a_lag)))
    a[:, t] = rng.binomial(1, p)
    y[:, t] = (
        2.0 * a[:, t]
        + 0.5 * a_lag
        + 1.5 * z[:, t, 0]
        + rng.normal(scale=0.7, size=n)
    )

reg = cm.RegressionBlip(max_lag=1)
reg.fit(y, a, z)
reg_result = reg.summary()
pd.DataFrame({
    "lag": reg_result["lags"].astype(int),
    "truth": [2.0, 1.7],
    "estimate": reg_result["coef"],
    "conditional_stage_se": reg_result["stage_se"],
})
lag truth estimate conditional_stage_se
0 0 2.0 1.981614 0.013754
1 1 1.7 1.637764 0.035863

An ADL regression that includes Z_t while interpreting the coefficient on A_{t-1} as its total lagged effect blocks the mediated path and targets 0.5, not 1.7. Omitting Z_t instead confounds the contemporaneous effect. The recursive blip regressions change the adjustment time with the causal lag.

Numerical experiment 2: staggered initiation with level confounding

The second design contains an unobserved U_i that affects both treatment initiation and the untreated outcome level. It does not affect untreated increments. The observed history L_{im} predicts both initiation and untreated increments:

Y_{i0}(0)=2U_i+\varepsilon_{i0}, \qquad Y_{i,m+1}(0)-Y_{im}(0)=0.35L_{im}+\varepsilon_{i,m+1},

\Pr(T_i=m\mid T_i\ge m,L_{im},U_i) =\operatorname{logit}^{-1}(-1.2+0.55L_{im}+0.65U_i).

The initiation response is (1.0,1.6,2.1) at horizons one through three. This violates level ignorability because U_i confounds initiation and Y_{i0}(0), but it satisfies the simulated conditional parallel-trends restriction.

Code
rng = np.random.default_rng(91)
n, periods = 5_000, 6
truth = np.array([1.0, 1.6, 2.1])
history = rng.normal(size=(n, periods, 1))
u = rng.normal(size=n)
status = np.zeros((n, periods))
first = np.full(n, periods + 1, dtype=int)
untreated = np.zeros((n, periods + 1))
untreated[:, 0] = 2.0 * u + rng.normal(scale=0.4, size=n)

for m in range(periods):
    untreated[:, m + 1] = (
        untreated[:, m]
        + 0.35 * history[:, m, 0]
        + rng.normal(scale=0.5, size=n)
    )
    risk = first > m
    p = 1 / (1 + np.exp(-(-1.2 + 0.55 * history[:, m, 0] + 0.65 * u)))
    start = risk & (rng.uniform(size=n) < p)
    first[start] = m
    status[first <= m, m] = 1.0

y_forward = untreated.copy()
for i in range(n):
    if first[i] < periods:
        for h, effect in enumerate(truth, start=1):
            if first[i] + h <= periods:
                y_forward[i, first[i] + h] += effect

pt = cm.ParallelTrendsSNMM(
    max_horizon=3,
    treatment_mode="initiation",
    n_folds=3,
    seed=7,
)
pt.fit(y_forward, status, history)
pt_result = pt.summary()
pd.DataFrame({
    "horizon": pt_result["horizons"].astype(int),
    "truth": truth,
    "estimate": pt_result["coef"],
    "se": pt_result["se"],
})
horizon truth estimate se
0 1 1.0 0.983159 0.009227
1 2 1.6 1.589484 0.016232
2 3 2.1 2.103710 0.022914
diagnostic
moment rows 4.526500e+04
minimum fitted treatment mean 1.000000e-02
maximum fitted treatment mean 6.400915e-01
maximum absolute sample moment 4.106337e-15

Comparative simulation study

The three estimators target different primitive objects. A common scalar comparison is possible in a linear distributed-lag design. There are four treatment decisions, t=0,1,2,3, and the target contrasts are

d^0=(0,0,0,0),\qquad d^1=(0,1,1,1).

Because the final outcome is Y_4, switching from d^0 to d^1 changes the treatments entering at horizons one, two, and three. The common target is therefore

\tau=\mu_4(d^1)-\mu_4(d^0) =1+0.6+0.4=2.

For each unit, draw mutually independent standard-normal X_i,U_i,L_{i,-1},\eta_{it},\varepsilon_{it}. Let \Lambda(v)=1/(1+e^{-v}), set A_{i,-1}=A_{i,-2}=0, and generate

L_{it}=0.5X_i+\eta_{it},

A_{it}\mid X_i,L_{it},A_{i,t-1},U_i \sim\operatorname{Bernoulli}\left[ \Lambda\{-0.1+0.35X_i+0.35L_{it}+0.2A_{i,t-1}+\rho U_i\} \right],

Y_{i0}=U_i+0.5X_i+0.4L_{i,-1}+\varepsilon_{i0},

Y_{i,t+1} =U_i+0.5X_i+0.15(t+1)+0.4L_{it} +A_{it}+0.6A_{i,t-1}+0.4A_{i,t-2}+\varepsilon_{i,t+1}.

The observed-selection design sets \rho=0. The history H_{it}=(X_i,L_{it},L_{i,t-1},A_{i,t-1}) then contains the treatment predictors. The fixed U_i shifts outcome levels but is independent of treatment.

Code
flowchart LR
  X["baseline X"] --> L["state L_t"]
  X --> A["treatment A_t"]
  L --> A
  AP["past treatment A_(t-1)"] --> A
  X --> Y["outcome Y_(t+1)"]
  L --> Y
  AP --> Y
  A --> Y

flowchart LR
  X["baseline X"] --> L["state L_t"]
  X --> A["treatment A_t"]
  L --> A
  AP["past treatment A_(t-1)"] --> A
  X --> Y["outcome Y_(t+1)"]
  L --> Y
  AP --> Y
  A --> Y

The hidden-selection design sets \rho=0.6. The unobserved U_i now causes both treatment and outcome levels. It does not enter the untreated trend because it cancels from Y_{i,t+1}(0)-Y_{it}(0). Sequential ignorability fails for the regression-blip and dynamic-balancing estimators, while the time-varying parallel-trends restriction remains plausible.

Code
flowchart LR
  U["unobserved level U"] --> A["treatment A_t"]
  U --> Y["outcome Y_(t+1)"]
  X["baseline X"] --> L["state L_t"]
  X --> A
  L --> A
  AP["past treatment A_(t-1)"] --> A
  X --> Y
  L --> Y
  AP --> Y
  A --> Y

flowchart LR
  U["unobserved level U"] --> A["treatment A_t"]
  U --> Y["outcome Y_(t+1)"]
  X["baseline X"] --> L["state L_t"]
  X --> A
  L --> A
  AP["past treatment A_(t-1)"] --> A
  X --> Y
  L --> Y
  AP --> Y
  A --> Y

For each draw, RegressionBlip estimates \tau as \widehat\gamma_0+\widehat\gamma_1+\widehat\gamma_2. ParallelTrendsSNMM estimates it as \widehat\psi_1+\widehat\psi_2+\widehat\psi_3. DynamicCovariateBalance fits \widehat\mu_4(d^1) and \widehat\mu_4(d^0) separately and differences them.

Code
import matplotlib.pyplot as plt

def simulate_comparison(seed, hidden_strength, n=1_200, periods=4):
    rng = np.random.default_rng(seed)
    x = rng.normal(size=n)
    u = rng.normal(size=n)
    treatment = np.zeros((n, periods))
    outcome = np.zeros((n, periods + 1))
    state = np.zeros((n, periods))
    lagged_state = np.zeros((n, periods))

    state_minus_one = 0.5 * x + rng.normal(size=n)
    outcome[:, 0] = (
        u + 0.5 * x + 0.4 * state_minus_one + rng.normal(scale=0.8, size=n)
    )
    previous_treatment = np.zeros(n)
    twice_lagged_treatment = np.zeros(n)
    previous_state = state_minus_one

    for time in range(periods):
        lagged_state[:, time] = previous_state
        state[:, time] = 0.5 * x + rng.normal(size=n)
        linear_probability = (
            -0.1
            + 0.35 * x
            + 0.35 * state[:, time]
            + 0.2 * previous_treatment
            + hidden_strength * u
        )
        propensity = 1.0 / (1.0 + np.exp(-linear_probability))
        treatment[:, time] = rng.binomial(1, propensity)
        outcome[:, time + 1] = (
            u
            + 0.5 * x
            + 0.15 * (time + 1)
            + 0.4 * state[:, time]
            + treatment[:, time]
            + 0.6 * previous_treatment
            + 0.4 * twice_lagged_treatment
            + rng.normal(scale=0.8, size=n)
        )
        twice_lagged_treatment = previous_treatment.copy()
        previous_treatment = treatment[:, time].copy()
        previous_state = state[:, time].copy()

    lagged_treatment = np.column_stack(
        [np.zeros(n), treatment[:, : periods - 1]]
    )
    pt_history = np.stack(
        [np.repeat(x[:, None], periods, axis=1), state, lagged_state, lagged_treatment],
        axis=2,
    )
    regression_treatment = np.zeros((n, periods + 1))
    regression_treatment[:, 1:] = treatment
    regression_history = np.zeros((n, periods + 1, 3))
    regression_history[:, 1:, 0] = x[:, None]
    regression_history[:, 1:, 1] = state
    regression_history[:, 1:, 2] = lagged_state
    dcb_history = np.stack(
        [
            np.repeat(x[:, None], periods, axis=1),
            state,
            lagged_state,
            lagged_treatment,
            outcome[:, :periods],
        ],
        axis=2,
    )

    regression = cm.RegressionBlip(max_lag=2)
    regression.fit(outcome, regression_treatment, regression_history)
    parallel_trends = cm.ParallelTrendsSNMM(
        max_horizon=3,
        treatment_mode="blip",
        n_folds=3,
        nuisance_penalty=1e-4,
        seed=seed,
    )
    parallel_trends.fit(outcome, treatment, pt_history)

    path_means = []
    dcb_success = []
    dcb_max_balance = []
    for path in ([0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0]):
        dcb = cm.DynamicCovariateBalance(nuisance_penalty=1e-4)
        dcb.fit(outcome[:, -1], treatment, dcb_history, path)
        diagnostics = dcb.summary()
        path_means.append(dcb.potential_outcome)
        dcb_success.append(diagnostics["success"])
        dcb_max_balance.append(np.max(diagnostics["max_abs_balance"]))

    return {
        "Regression blip": regression.coef.sum(),
        "Parallel-trends SNMM": parallel_trends.coef.sum(),
        "Dynamic balance": path_means[1] - path_means[0],
        "dcb_success": all(dcb_success),
        "dcb_max_balance": max(dcb_max_balance),
    }


records = []
for design, hidden_strength in [
    ("Selection on observables", 0.0),
    ("Selection on unobservables", 0.6),
]:
    for replication in range(100):
        result = simulate_comparison(1_000 + replication, hidden_strength)
        for estimator in [
            "Regression blip",
            "Parallel-trends SNMM",
            "Dynamic balance",
        ]:
            records.append(
                {
                    "design": design,
                    "replication": replication,
                    "estimator": estimator,
                    "estimate": result[estimator],
                    "dcb_success": result["dcb_success"],
                    "dcb_max_balance": result["dcb_max_balance"],
                }
            )

comparison = pd.DataFrame(records)
truth = 2.0
monte_carlo = (
    comparison.groupby(["design", "estimator"])["estimate"]
    .agg(mean="mean", mc_sd="std")
    .reset_index()
)
monte_carlo["bias"] = monte_carlo["mean"] - truth
monte_carlo["rmse"] = (
    comparison.assign(squared_error=lambda frame: (frame["estimate"] - truth) ** 2)
    .groupby(["design", "estimator"])["squared_error"]
    .mean()
    .pow(0.5)
    .to_numpy()
)
monte_carlo.round(3)
design estimator mean mc_sd bias rmse
0 Selection on observables Dynamic balance 2.000 0.197 0.000 0.196
1 Selection on observables Parallel-trends SNMM 1.997 0.095 -0.003 0.095
2 Selection on observables Regression blip 1.996 0.076 -0.004 0.075
3 Selection on unobservables Dynamic balance 2.918 0.201 0.918 0.939
4 Selection on unobservables Parallel-trends SNMM 1.957 0.086 -0.043 0.095
5 Selection on unobservables Regression blip 2.636 0.075 0.636 0.641

Sampling distributions for the common final-period path contrast.

Monte Carlo bias and standard deviation separate robustness from efficiency.

Under selection on observables, all three sampling distributions are centered near two. The recursive regression is most efficient in this correctly specified low-dimensional outcome model; the parallel-trends estimator pays for orthogonalization and cross-fitting, while the path-specific dynamic-balancing contrast pays for estimating two increasingly selective path means.

When U_i enters treatment assignment, the regression-blip and dynamic-balancing estimates inherit the positive level confounding. The parallel-trends estimate remains close to the target because differencing removes U_i. This is not generic robustness to hidden confounding: it relies specifically on U_i being time invariant and absent from untreated outcome trends.

strict_solver_success_rate maximum_retained_imbalance
design
Selection on observables 0.81 0.000130
Selection on unobservables 0.76 0.000053

BalancingWeights deliberately retains finite weights when its strict scaled-residual convergence gate is not met. DynamicCovariateBalance propagates that per-period status instead of relabeling the fit as converged. The simulation therefore reports both the strict solver-success rate and the maximum original-scale imbalance among retained fits; a production analysis should inspect both and tighten or relax the calibration design deliberately.

Interpretation and current boundaries

These classes do not estimate the same object under interchangeable assumptions.

Feature RegressionBlip ParallelTrendsSNMM DynamicCovariateBalance
Identifying restriction Sequential ignorability Time-varying conditional parallel trends Sequential ignorability plus potential projections
Primitive target Lag-specific additive blips Horizon-specific additive blips Mean final outcome under one path
Outcome timing A_t may affect Y_t A_m first affects Y_{m+1} History precedes A_t; final outcome supplied separately
Nuisance strategy Recursive outcome regressions Cross-fitted treatment and trend regressions Recursive potential projections plus sequential calibration
Robustness Outcome-regression specification Doubly robust orthogonal moment Product of projection error and imbalance
Unobserved level confounding Generally not allowed Allowed when it does not confound untreated trends Generally not allowed
Inference Stagewise unit-clustered SE Unit-level influence-function sandwich Not yet implemented; use a unit bootstrap

The initial implementation is intentionally narrow:

  1. The two blip estimators use additive linear effects, with one coefficient per lag or horizon. Dynamic balance instead estimates one path mean at a time.
  2. ParallelTrendsSNMM uses ridge nuisance regressions and a conditional-mean treatment model. Flexible Python callbacks are not yet part of the Rust boundary.
  3. RegressionBlip reports unit-clustered uncertainty conditional on earlier recursive blip estimates. A unit block bootstrap is required for joint recursive inference.
  4. In ParallelTrendsSNMM, cross-fitting is over units, never unit-period rows.
  5. DynamicCovariateBalance implements the paper’s full-interaction recursion with ridge, exact balance, and the package’s quadratic calibration geometry. It does not yet implement the paper’s lasso tuning, coordinatewise approximate-balance program, or analytic longitudinal variance.
  6. None of the estimators makes parallel trends or sequential ignorability testable. The analyst still owns the history set, timing, treatment coding, positivity argument, path support, and effect specification.

References

  • Blackwell, Matthew, and Adam N. Glynn. 2018. “How to Make Causal Inferences with Time-Series Cross-Sectional Data under Selection on Observables.” American Political Science Review 112(4): 1067–1082. Paper.
  • Shahn, Zach, Oliver Dukes, Meghana Shamsunder, David Richardson, Eric Tchetgen Tchetgen, and James Robins. 2022. “Structural Nested Mean Models Under Parallel Trends Assumptions.” arXiv:2204.10291.
  • Viviano, Davide, and Jelena Bradic. 2026. “Dynamic Covariate Balancing: Estimating Treatment Effects over Time with Potential Local Projections.” First circulated 2021. arXiv:2103.01280.