API Reference
Complete API documentation for deep-inference. This page consolidates every module into one reference.
The package exposes four top-level entry points.
from deep_inference import structural_dml, inference, did, riesz_inference
Entry point |
When to use |
|---|---|
|
Production workhorse, 13 GLM families, fixed target E[beta(X)] |
|
Flexible targets, regime detection, RCT support, custom loss |
|
Difference-in-differences (exact / neural / panel two-way FE) |
|
Automatic debiasing via Riesz representer (binary ATE) |
Entry points
structural_dml() - legacy API
The production-ready API supporting 13 GLM families. Trains a structural neural network with
influence-function-based inference and returns DMLResult.
from deep_inference import structural_dml
result = structural_dml(
Y, # (n,) outcomes
T, # (n,) treatments
X, # (n, d) covariates
family='linear', # Family name: 'linear', 'logit', 'poisson', etc.
target=None, # Target variant (e.g., 'ame' for logit)
hidden_dims=[64, 32], # Network architecture
epochs=100, # Training epochs
n_folds=50, # Cross-fitting folds
lr=0.01, # Learning rate
lambda_method='ridge', # Lambda estimation: ridge (default), aggregate, lgbm
verbose=False # Print progress
)
Supported families:
Family |
Model |
theta_dim |
Notes |
|---|---|---|---|
|
Y = alpha + beta*T + eps |
2 |
OLS-equivalent |
|
P(Y=1) = sigmoid(alpha + beta*T) |
2 |
Binary outcomes |
|
Y ~ Pois(exp(alpha + beta*T)) |
2 |
Count data |
|
Y ~ Gamma(k, exp(alpha + beta*T)) |
2 |
Positive continuous |
|
Y ~ Gumbel(alpha + beta*T, sigma) |
2 |
Extreme values |
|
Y = max(0, alpha + betaT + sigmaeps) |
3 |
Censored |
|
Y ~ NegBin(exp(alpha + beta*T), r) |
2 |
Overdispersed counts |
|
Y ~ Weibull(k, exp(alpha + beta*T)) |
2 |
Survival/duration |
|
Y ~ N(alpha + beta*T, sigma) |
3 |
Explicit heteroskedasticity |
|
P(Y=1) = Phi(alpha + beta*T) |
2 |
Binary, normal link |
|
Y ~ Beta(mu*phi, (1-mu)*phi) |
2 |
Fractional outcomes |
|
Y ~ ZeroInflated-Poisson(pi, lambda) |
4 |
Zero-inflated counts |
|
P(Y=j) = softmax(alpha_j + X’_j*beta) |
(J-1)+K |
Discrete choice |
Example:
import numpy as np
from deep_inference import structural_dml
np.random.seed(42)
n = 2000
X = np.random.randn(n, 10)
T = np.random.randn(n)
Y = X[:, 0] + 0.5 * T + np.random.randn(n)
result = structural_dml(Y=Y, T=T, X=X, family='linear', n_folds=50, epochs=100)
print(result.summary())
Output:
==============================================================================
Structural DML Results
==============================================================================
Family: Linear Target: E[beta]
No. Observations: 2000 No. Folds: 50
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
E[beta] 0.5012 0.0234 21.410 0.000 0.4553 0.5471
==============================================================================
Diagnostics:
Min Lambda eigenvalue: 0.998765
Mean condition number: 1.00
Correction ratio: 0.0523
------------------------------------------------------------------------------
Lambda method guidance:
Method |
Coverage |
Default |
Notes |
|---|---|---|---|
|
96% |
Yes |
Safe default, validated coverage |
|
95% |
No |
Stable, ignores X-dependence |
|
96% |
No |
High accuracy alternative |
|
67% |
No |
AVOID: invalid SEs despite high correlation |
Ridge requires heavy regularization (alpha=1000) to produce stable Lambda estimates.
inference() - flexible API
The new API with flexible targets and automatic regime detection. Accepts built-in model strings, custom loss functions, built-in and custom target functionals, and an explicit regime override.
from deep_inference import inference
result = inference(
Y, # (n,) outcomes
T, # (n,) treatments
X, # (n, d) covariates
# Model specification (choose one):
model='logit', # Built-in: 'linear', 'logit', 'multinomial_logit', 'quantile', 'did', 'did_fe'
loss=None, # OR custom loss function
theta_dim=None, # Required if custom loss
# Target specification (choose one):
target='beta', # Built-in target string (see list below)
target_fn=None, # OR custom target function
t_tilde=None, # Evaluation point (default: mean(T))
# Regime settings:
is_randomized=False, # True for RCTs
treatment_dist=None, # Known F_T (e.g., Normal(0, 1))
lambda_method=None, # Override auto-detection
# Cross-fitting:
n_folds=50,
n_repeats=1,
# Network:
hidden_dims=[64, 32],
epochs=100,
lr=0.01,
ridge=1e-4,
verbose=False
)
Recognized target= strings:
beta, average_slope, ame, elasticity, wtp, welfare, dose_response, profit,
tail_probability, conditional_variance, qte, tau, att, fe_effect.
Built-in target quick reference:
String |
Formula |
Use case |
|---|---|---|
|
E[beta(X)] |
Average treatment effect (log-odds for logit) |
|
E[p(1-p)*beta] |
Average marginal effect (probability scale) |
|
E[(1-p)betat] |
Price elasticity of demand |
|
E[-theta_attr / theta_price] |
Willingness to pay |
|
E[log(1+exp(V)) / abs(beta_price)] |
Consumer surplus (logsum) |
|
E[G(theta, t)] |
Average predicted outcome |
|
E[t * G(theta, t)] |
Expected revenue at price level |
|
E[P(Y > c |
theta, t)] |
|
E[Var(Y |
theta, t)] |
Three regimes (auto-detected):
Regime |
Condition |
Lambda method |
Cross-fitting |
|---|---|---|---|
A |
RCT with known F_T |
Compute via Monte Carlo |
2-way |
B |
Linear model |
Analytic closed-form |
2-way |
C |
Observational and nonlinear |
Estimate via neural net |
3-way |
Regime A example (randomized experiment):
from deep_inference import inference
from deep_inference.lambda_.compute import Normal
result = inference(
Y, T, X,
model='logit',
target='beta',
is_randomized=True,
treatment_dist=Normal(mean=0.0, std=1.0)
)
print(result.diagnostics['regime']) # 'A'
Custom target function example:
import torch
def avg_prediction(x, theta, t_tilde):
"""E[P(Y=1 | T=t_tilde)] = E[sigma(alpha + beta*t_tilde)]"""
return torch.sigmoid(theta[0] + theta[1] * t_tilde)
result = inference(Y, T, X, model='logit', target_fn=avg_prediction, t_tilde=0.0)
The Jacobian of any target_fn is computed automatically via PyTorch autodiff.
did() - difference-in-differences
A single entry point for difference-in-differences with influence-function inference. The method
is auto-selected from the arguments but can be overridden with method=.
|
Call signature |
What it does |
|---|---|---|
|
|
Closed-form 2x2, beta = mu_11 - mu_10 - mu_01 + mu_00; SE = HC0 |
|
|
Heterogeneous 2x2; network learns tau(X); target E[tau(X)] |
|
|
Two-way FE panel DiD; target E[tau(X)] |
Auto-selection rule (method='auto', the default): unit and time given implies 'panel_fe';
else X given implies 'neural'; else implies 'exact'.
Closed-form 2x2 (method='exact'):
The estimand is the group x post interaction beta. For the saturated cell-mean loss the expected
Hessian is Lambda = diag(p_00, p_01, p_10, p_11), so the influence function is closed-form and
SE = sqrt(Var(psi)/n) equals the HC0 robust OLS SE of Y = alpha + gamma*G + lambda*T + beta*(G*T) + u
to machine precision. Use use_bessel=False (the default); use_bessel=True applies an (n-1) correction.
import numpy as np
from deep_inference import did
rng = np.random.default_rng(0)
n = 2000
G = (rng.random(n) < 0.5).astype(float)
P = (rng.random(n) < 0.5).astype(float)
Y = 1.0 + 0.5*G + 0.4*P + 0.6*(G*P) + rng.standard_normal(n)
r = did(Y, G, P)
print(r.mu_hat, r.se) # beta-hat and HC0-equivalent SE
Heterogeneous 2x2 (method='neural'):
Pass covariates X and the network learns covariate-varying coefficients of
Y = alpha(X) + gamma(X)*G + lambda(X)*P + tau(X)*(G*P) + eps. The IF machinery returns
E[tau(X)]. Regime B (analytic Lambda = E[WW’ | X], two-way cross-fitting).
r = did(Y, G, P, X=X, n_folds=50, epochs=200)
tau_x = r.predict_theta(X_new)[:, 3] # conditional effect tau(X_new)
Two-way FE panel (method='panel_fe'):
Y and treatment D = group*post (or an explicit D=) are residualized by unit and time fixed
effects, then the network learns tau(X) in Y_tilde_it = D_tilde_it * tau(X_it) + eps_it.
Target E[tau(X)], Regime B (Lambda = E[D_tilde^2 | X]). Works for continuous and binary outcomes.
For binary Y this is a fixed-effects linear probability model with heteroskedasticity-robust IF SE.
r = did(Y, D=D, X=X, unit=unit, time=time)
tau_x = r.predict_theta(X_new)[:, 0] # conditional effect tau(X_new)
Note
For panel_fe, the standard error assumes errors are independent across observations.
Serial or within-unit correlation would require cluster-robust SEs, which are not yet implemented.
The neural methods are also reachable via the general API: inference(..., model='did', target='tau')
and inference(..., model='did_fe', target='fe_effect').
Algorithm overview
Cross-fitting (K-fold):
For k = 1 to K:
Train: Fit theta-hat(x) on folds != k
[If 3-way: Fit Lambda-hat(x) on a separate fold]
Eval: Compute psi on fold k
Aggregate: mu-hat = mean(psi), SE = std(psi) / sqrt(n)
Influence function:
The influence function corrects for regularization bias:
psi(z) = H(theta-hat) - H_theta * Lambda(x)^{-1} * ell_theta(z, theta-hat)
Where H(theta) is the target functional, H_theta is its Jacobian, Lambda(x) = E[ell_theta_theta | X=x] is the conditional Hessian, and ell_theta is the score (gradient of loss).
Expected performance:
Method |
Coverage |
SE ratio |
|---|---|---|
Naive (no correction) |
10-30% |
much less than 1 |
Influence function |
~95% |
~1.0 |
Network architecture guidelines:
Sample size |
Recommended hidden dims |
|---|---|
n < 1,000 |
|
1,000 to 10,000 |
|
10,000 to 100,000 |
|
n > 100,000 |
|
Cross-fitting folds:
Use case |
K |
|---|---|
Quick exploration |
10-20 |
Production |
50 |
Very large data |
20-50 |
Result objects
DMLResult
Returned by structural_dml().
Attribute |
Type |
Description |
|---|---|---|
|
float |
Debiased estimate of E[beta(X)] |
|
float |
Naive (biased) estimate |
|
float |
Standard error |
|
float |
Lower 95% CI bound |
|
float |
Upper 95% CI bound |
|
ndarray |
Estimated theta(x) for all observations, shape (n, theta_dim) |
|
ndarray |
Influence function values, shape (n,) |
|
dict |
Training diagnostics (min lambda eigenvalue, condition number, correction ratio) |
Both DMLResult and InferenceResult provide a summary() method that produces a
statsmodels-style table with a header (family, target, sample size, folds, date), a
coefficient table (estimate, SE, z-statistic, p-value, 95% CI), and diagnostics.
print(result.summary())
print(result.mu_hat) # point estimate
print(result.se) # standard error
print(result.ci_lower) # lower 95% CI
print(result.ci_upper) # upper 95% CI
InferenceResult
Returned by inference(), did(), and riesz_inference().
Attribute |
Type |
Description |
|---|---|---|
|
float |
Point estimate |
|
float |
Standard error |
|
float |
Lower 95% CI |
|
float |
Upper 95% CI |
|
Tensor |
Influence function values |
|
Tensor |
Estimated theta(x) |
|
dict |
Regime, lambda method, and other diagnostics |
For riesz_inference, theta_hat holds the learned representer a(Z) as an (n, 1) tensor,
and diagnostics includes procedure='riesznet', mu_naive, mean_abs_representer, and
per-repeat estimates.
Families
Statistical families defining the loss function, score (gradient), Hessian, and the per-observation
target and its Jacobian. All families inherit from BaseFamily.
Factory and registry
from deep_inference import get_family, FAMILY_REGISTRY
family = get_family("logit")
print(list(FAMILY_REGISTRY.keys()))
# ['linear', 'logit', 'poisson', 'tobit', 'negbin', 'gamma', 'gumbel', 'weibull',
# 'gaussian', 'probit', 'beta', 'zip', 'multinomial_logit']
BaseFamily interface
All families implement:
Method |
Signature |
Description |
|---|---|---|
|
|
Per-observation loss (NLL) |
|
|
Score: gradient w.r.t. theta |
|
|
Hessian w.r.t. theta |
|
|
Whether Hessian depends on theta (controls 2-way vs 3-way split) |
|
|
Per-observation target h(theta) |
|
|
Gradient of target |
import torch
from deep_inference import LinearFamily
family = LinearFamily()
n = 100
y = torch.randn(n)
t = torch.randn(n)
theta = torch.randn(n, 2) # [alpha, beta]
loss = family.loss(y, t, theta) # (n,)
grad = family.gradient(y, t, theta) # (n, 2)
hess = family.hessian(y, t, theta) # (n, 2, 2)
h = family.per_obs_target(theta, t) # (n,)
dh = family.per_obs_target_gradient(theta, t) # (n, 2)
GLM family formulas
Family |
Link |
Loss (NLL) |
Gradient |
Hessian weight |
theta_dim |
|---|---|---|---|---|---|
Linear |
Identity |
(y-mu)^2 |
-2(y-mu)*[1,t] |
2 (constant) |
2 |
Logit |
Logit |
log(1+exp(eta)) - y*eta |
(p-y)*[1,t] |
p(1-p) |
2 |
Poisson |
Log |
lambda - y*log(lambda) |
(lambda-y)*[1,t] |
lambda |
2 |
NegBin |
Log |
-lgamma(y+r) + … |
r(mu-y)/(r+mu)*[1,t] |
rmu(r+y)/(r+mu)^2 |
2 |
Gamma |
Log |
y/mu + log(mu) |
(1-y/mu)*[1,t] |
y/mu |
2 |
Weibull |
Log |
-log(k)+k*log(lam)-(k-1)*log(y)+z^k |
k(1-z)*[1,t] |
k^2*z |
2 |
Gumbel |
Identity |
z + exp(-z) |
(-1/sigma)(1-exp(-z))[1,t] |
exp(-z)/sigma^2 |
2 |
Tobit |
Identity |
Censored normal NLL |
Autodiff (Mills ratio) |
Autodiff |
3 |
Probit |
Phi(eta) |
-y*log(Phi) - (1-y)*log(1-Phi) |
Mills ratio |
Autodiff |
2 |
Beta |
Logit |
lgamma(mu*phi)+lgamma((1-mu)*phi)-… |
Digamma terms |
Autodiff |
2 |
Gaussian |
Identity |
(y-mu)^2/(2*sigma^2) + log(sigma) |
[(mu-y)/sigma^2, t*(mu-y)/sigma^2, 1-(y-mu)^2/sigma^2] |
Depends on sigma |
3 |
ZIP |
Mixed |
Mixture pi + (1-pi)*Poisson |
Autodiff |
Autodiff |
4 |
MultinomialLogit |
Softmax |
-log softmax(V)[y] |
(p_j - 1{Y=j})*[1,x] |
p_j*(delta_jm - p_m) blocks |
(J-1)+K |
Where: eta = alpha + beta*t, mu = g^{-1}(eta), z = (y/lambda)^k (Weibull) or (y-mu)/sigma (Gumbel), r = 1/overdispersion, Phi = normal CDF.
Family classes
LinearFamily
from deep_inference import LinearFamily
family = LinearFamily()
# Model: Y = alpha(X) + beta(X)*T + epsilon
# Loss: MSE
# Hessian: constant (2-way splitting)
LogitFamily
from deep_inference import LogitFamily
family = LogitFamily(target='beta') # log-odds target (default)
family = LogitFamily(target='ame') # average marginal effect
# Model: P(Y=1) = sigmoid(alpha(X) + beta(X)*T)
# Hessian: depends on theta (3-way splitting)
PoissonFamily
from deep_inference import PoissonFamily
family = PoissonFamily()
# Model: Y ~ Poisson(exp(alpha(X) + beta(X)*T))
# Hessian: depends on theta
TobitFamily
from deep_inference import TobitFamily
family = TobitFamily(target='latent') # effect on latent Y* (default)
family = TobitFamily(target='observed') # effect on observed E[Y]
# Model: Y = max(0, alpha(X) + beta(X)*T + sigma(X)*eps)
# theta_dim: 3 (alpha, beta, gamma=log(sigma))
NegBinFamily
from deep_inference import NegBinFamily
family = NegBinFamily(overdispersion=0.5)
# Model: Y ~ NegBin(mu, r) where mu = exp(alpha + beta*T)
GammaFamily
from deep_inference import GammaFamily
family = GammaFamily(shape=1.0)
# Model: Y ~ Gamma(shape, exp(alpha + beta*T))
GumbelFamily
from deep_inference import GumbelFamily
family = GumbelFamily(scale=1.0)
# Model: Y ~ Gumbel(alpha + beta*T, scale)
WeibullFamily
from deep_inference import WeibullFamily
family = WeibullFamily(shape=1.0)
# Model: Y ~ Weibull(shape, exp(alpha + beta*T))
GaussianFamily (import from deep_inference.families)
from deep_inference.families import GaussianFamily
family = GaussianFamily()
# Model: Y ~ N(alpha + beta*T, sigma)
# theta_dim: 3 (alpha, beta, log_sigma)
ProbitFamily (import from deep_inference.families)
from deep_inference.families import ProbitFamily
family = ProbitFamily()
# Model: P(Y=1) = Phi(alpha + beta*T)
# Hessian: autodiff
BetaFamily (import from deep_inference.families)
from deep_inference.families import BetaFamily
family = BetaFamily()
# Model: Y ~ Beta(mu*phi, (1-mu)*phi)
# Hessian: autodiff
ZIPFamily (import from deep_inference.families)
from deep_inference.families import ZIPFamily
family = ZIPFamily()
# Model: Y ~ ZeroInflated-Poisson(pi, lambda)
# theta_dim: 4
# Hessian: autodiff
MultinomialLogitFamily
from deep_inference import MultinomialLogitFamily
family = MultinomialLogitFamily(
n_alternatives=3,
n_attributes=2,
target='beta',
target_idx=0
)
# Model: P(Y=j) = softmax(alpha_j(W) + X'_ij * beta(W))
# theta_dim = (J-1) + K = 4
# Hessian: Fisher information (depends on theta, 3-way splitting)
Using a family instance with structural_dml:
from deep_inference import structural_dml, LogitFamily
family = LogitFamily(target='ame')
result = structural_dml(Y, T, X, family=family)
Targets
The targets module defines what quantity to estimate. A target is a functional H(theta) that
maps structural parameters to a scalar. Targets implement the Target protocol.
Target protocol
class Target(Protocol):
def h(self, x: Tensor, theta: Tensor, t_tilde: Tensor) -> Tensor:
"""Compute target value for a single observation."""
...
def jacobian(self, x: Tensor, theta: Tensor, t_tilde: Tensor) -> Tensor:
"""Compute dh/dtheta. Falls back to autodiff if not implemented (returns None)."""
...
Target overview
Target |
Formula |
Use case |
|---|---|---|
|
E[theta_j] |
Average treatment effect |
|
E[p(1-p)*beta] |
Marginal effect on probability |
|
E[(1-p)betat] (logit) |
Price elasticity of demand |
|
E[-theta_attr / theta_price] |
Willingness to pay |
|
E[log(1+exp(V)) / abs(beta_price)] |
Expected consumer surplus |
|
E[G(theta, t)] |
Average predicted outcome at treatment level |
|
E[t * G(theta, t)] |
Expected revenue at price level |
|
E[P(Y > c |
theta, t)] |
|
E[Var(Y |
theta, t)] |
|
E[G(theta, t) - G(theta, t_0)] |
Combinatorial treatment effect |
|
P(Y=j |
W, X) |
|
dP(Y=j)/dx_{jk} |
Multinomial marginal effect |
|
User-defined h(x, theta, t) |
Any custom functional |
AverageParameter
Average of a specific parameter across the population. Default target for most families.
from deep_inference.targets import AverageParameter
target = AverageParameter(param_index=1, theta_dim=2)
Formula: H(theta) = (1/n) sum theta_j(x_i)
Jacobian (closed-form): dH/dtheta = [0, ..., 1/n, ..., 0] with 1 at position j.
AME (Average Marginal Effect)
For logit models, the marginal effect on probability (not log-odds).
from deep_inference.targets import AME
target = AME(param_index=1, model_type='logit')
Formula (logit): H(theta, t) = (1/n) sum sigma'(alpha_i + beta_i*t) * beta_i = (1/n) sum p_i*(1-p_i)*beta_i
Jacobian (closed-form for logit):
dH/dalpha = p*(1-p)*(1-2p)*beta / n
dH/dbeta = p*(1-p)*[1 + (1-2p)*beta*t] / n
Elasticity
Price elasticity of demand. Key object for personalized pricing.
from deep_inference.targets import Elasticity
target = Elasticity(model_type='logit') # also 'poisson', 'gamma', 'negbin'
Formula (logit): H(theta, t) = (1 - p)*beta*t where p = sigma(alpha + beta*t)
Formula (log-link: poisson/gamma/negbin): H(theta, t) = beta*t
Jacobian (logit):
dH/dalpha = -p*(1-p)*beta*t
dH/dbeta = (1-p)*t - p*(1-p)*beta*t^2
Usage: result = inference(Y, T, X, model='logit', target='elasticity', t_tilde=1.0)
Reference: Dube and Misra (2022, JPE).
WTP (Willingness To Pay)
Marginal rate of substitution between an attribute and price.
from deep_inference.targets import WTP
target = WTP(attribute_index=1, price_index=2)
Formula: H(theta) = -theta_attribute / theta_price
Jacobian (closed-form delta method):
dH/dtheta_attr = -1 / theta_price
dH/dtheta_price = theta_attr / theta_price^2
Returns NaN and falls back to None Jacobian when the price coefficient is within 1e-8 of zero.
t_tilde is not used.
Usage: result = inference(Y, T, X, model='logit', target='wtp')
Reference: Dube and Misra (2022, JPE).
ConsumerWelfare
Expected consumer surplus from logit demand (Small and Rosen 1981 logsum), scaled by the marginal utility of income.
from deep_inference.targets import ConsumerWelfare
target = ConsumerWelfare(price_coef_index=1)
Formula (binary logit): H(theta, t) = log(1 + exp(V)) / abs(beta_price) where V = alpha + beta_price*t
Implemented with softplus(V) for numerical stability. Jacobian is computed via autodiff
(jacobian() returns None) because it involves derivatives of softplus and |beta_price|.
Usage: result = inference(Y, T, X, model='logit', target='welfare', t_tilde=0.0)
Reference: Small and Rosen (1981); Dube and Misra (2022, JPE).
DoseResponse
Average predicted outcome at a given treatment level.
from deep_inference.targets import DoseResponse
target = DoseResponse(model_type='logit')
Formula (logit): H(theta, t) = sigma(alpha + beta*t)
Jacobian (closed-form, where p = sigma(alpha + beta*t)):
dH/dalpha = p*(1-p)
dH/dbeta = p*(1-p)*t
Supports logit, linear, and poisson model types.
Usage: result = inference(Y, T, X, model='logit', target='dose_response', t_tilde=1.0)
Reference: Colangelo and Lee (2026, JBES).
Profit
Expected revenue per consumer at a given price level.
from deep_inference.targets import Profit
target = Profit(model_type='logit')
Formula (logit): H(theta, t) = t * sigma(alpha + beta*t)
Jacobian (closed-form):
dH/dalpha = t*p*(1-p)
dH/dbeta = t*p*(1-p)*t
Usage: result = inference(Y, T, X, model='logit', target='profit', t_tilde=2.0)
Reference: Dube and Misra (2023, JPE).
TailProbability
Probability that the outcome exceeds a threshold.
from deep_inference.targets import TailProbability
target = TailProbability(threshold=5, model_type='poisson')
Formula (Poisson): H(theta, t) = 1 - sum_{k=0}^{c} exp(-lambda)*lambda^k/k! where lambda = exp(alpha + beta*t)
Formula (logit, c=0): H(theta, t) = sigma(alpha + beta*t) (identical to DoseResponse)
Formula (linear/Gaussian): H(theta, t) = 1 - Phi((c - mu)/sigma) where mu = alpha + beta*t
Closed-form Jacobian for logit and linear; autodiff for Poisson.
Reference: Melnychuk and Feuerriegel (2026, ICLR).
ConditionalVariance
Model-implied variance of outcomes, capturing heterogeneity in risk across covariates.
from deep_inference.targets import ConditionalVariance
target = ConditionalVariance(model_type='logit')
Formula (logit): H(theta, t) = p*(1-p) where p = sigma(alpha + beta*t)
Jacobian (closed-form):
dH/dalpha = p*(1-p)*(1-2p)
dH/dbeta = p*(1-p)*(1-2p)*t
Usage: result = inference(Y, T, X, model='logit', target='conditional_variance', t_tilde=0.0)
Reference: Melnychuk and Feuerriegel (2026, ICLR).
MultiTreatmentATE
Average treatment effect for combinatorial experiments with multiple binary treatments.
from deep_inference.targets import MultiTreatmentATE
from deep_inference.models.combinatorial import CombinatorialModel
model = CombinatorialModel(n_treatments=3, link='gen_sigmoid_ii')
target = MultiTreatmentATE(
model=model,
treatment=[1, 0, 1], # apply treatments 1 and 3
control=[0, 0, 0], # vs no treatment
)
Formula: H(theta) = G(theta, t) - G(theta, t_0) where G is the structural link from CombinatorialModel.
Jacobian computed via autodiff.
Reference: Ye et al. (2025, Management Science).
ChoiceProbabilityTarget
For multinomial logit models: probability of choosing a specific alternative.
from deep_inference.targets.choice_probability import ChoiceProbabilityTarget
target = ChoiceProbabilityTarget(
alternative=1, # which alternative (0-indexed)
n_alternatives=3, # total alternatives J
n_attributes=2 # attributes per alternative K
)
Formula: H = P(Y=j | W, X) = softmax(V)[j] where V_j = alpha_j + x'_j * beta
Jacobian (closed-form):
dP_j/dalpha_m = P_{m+1}*(delta_{j,m+1} - P_j)
dP_j/dbeta_k = P_j*(x_tilde_{jk} - x_bar_{pk})
Where x_bar_{pk} = sum_j P_j * x_{jk} is the probability-weighted mean attribute.
MultinomialAME
Average Marginal Effect for multinomial logit: how a unit change in attribute k affects the probability of choosing alternative j.
from deep_inference.targets.choice_probability import MultinomialAME
target = MultinomialAME(
alternative=1, # which alternative
attribute=0, # which attribute
n_alternatives=3, # total alternatives J
n_attributes=2 # attributes per alternative K
)
Formula: dP_j/dx_{jk} = beta_k * P_j * (1 - P_j)
This generalizes the familiar binary logit AME to the multinomial context.
CustomTarget and target_from_fn
Define any target function; the Jacobian is computed via autodiff.
from deep_inference.targets import CustomTarget
import torch
def my_target(x, theta, t_tilde):
"""
Args:
x: Covariates (unused for average targets)
theta: (theta_dim,) parameter vector
t_tilde: Evaluation point for treatment
Returns:
Scalar value
"""
alpha, beta = theta[0], theta[1]
return torch.sigmoid(alpha + beta * t_tilde)
target = CustomTarget(h_fn=my_target)
Subclassing BaseTarget for a closed-form Jacobian (faster than autodiff):
from deep_inference.targets import BaseTarget
import torch
class MyTarget(BaseTarget):
def h(self, x, theta, t_tilde):
return theta[0] ** 2 + theta[1] * t_tilde
def jacobian(self, x, theta, t_tilde):
return torch.tensor([2 * theta[0], t_tilde])
Counterfactual comparison example:
def treatment_effect(x, theta, t_tilde):
"""E[P(Y=1|T=1) - P(Y=1|T=0)]"""
alpha, beta = theta[0], theta[1]
p1 = torch.sigmoid(alpha + beta * 1.0)
p0 = torch.sigmoid(alpha + beta * 0.0)
return p1 - p0
result = inference(Y, T, X, model='logit', target_fn=treatment_effect)
Autodiff Jacobian (how it works internally):
theta.requires_grad_(True)
h_value = target_fn(x, theta, t_tilde)
jacobian = torch.autograd.grad(h_value, theta)[0]
Lambda strategies
The lambda_ module handles estimation of Lambda(x) = E[d^2 ell / dtheta^2 | X=x], the
conditional expected Hessian of the loss. This is the load-bearing object for valid influence
function inference.
Regime overview
Regime |
Condition |
Lambda method |
Cross-fitting |
|---|---|---|---|
A |
RCT with known F_T |
Compute via Monte Carlo |
2-way |
B |
Linear model |
Analytic closed-form |
2-way |
C |
Observational and nonlinear |
Estimate via neural net |
3-way |
Regime detection
from deep_inference.lambda_ import detect_regime, Regime
regime = detect_regime(
model=my_model,
is_randomized=True,
has_treatment_dist=True
)
# Returns Regime.A, Regime.B, or Regime.C
Decision logic:
if is_randomized AND treatment_dist provided:
-> Regime A (ComputeLambda)
elif model is linear:
-> Regime B (AnalyticLambda)
else:
-> Regime C (EstimateLambda)
ComputeLambda (Regime A)
For randomized experiments where T is independent of X and has known distribution F_T.
from deep_inference import inference
from deep_inference.lambda_.compute import Normal, Bernoulli, Uniform
# Normal treatment: T ~ N(mu, sigma^2)
result = inference(Y, T, X, model='logit', is_randomized=True, treatment_dist=Normal(mean=0.0, std=1.0))
# Binary treatment: T ~ Bernoulli(p)
result = inference(Y, T, X, model='logit', is_randomized=True, treatment_dist=Bernoulli(p=0.5))
# Uniform treatment: T ~ Uniform(a, b)
result = inference(Y, T, X, model='logit', is_randomized=True, treatment_dist=Uniform(low=-1.0, high=1.0))
How it works:
Lambda(x, theta) = E_T[d^2 ell / dtheta^2 | theta]
~= (1/M) sum d^2 ell(y, t_m, theta) / dtheta^2
where t_m ~ F_T are Monte Carlo samples. No neural network for Lambda needed. 2-way
cross-fitting (faster). More stable than estimation.
Custom treatment distribution:
from deep_inference.lambda_.compute import TreatmentDistribution
import torch
class MyDistribution(TreatmentDistribution):
def sample(self, n: int) -> torch.Tensor:
return torch.randn(n) * 2 + 1 # N(1, 4)
AnalyticLambda (Regime B)
For linear models, Lambda has a closed-form solution. Automatically selected for the linear model.
Formula (linear): Lambda = E[[1, T]' [1, T]] = [[1, E[T]], [E[T], E[T^2]]]
result = inference(Y, T, X, model='linear') # AnalyticLambda selected automatically
EstimateLambda (Regime C)
For observational data with nonlinear models. Lambda(x) is regressed from sample Hessians.
result = inference(Y, T, X, model='logit') # EstimateLambda with method='ridge' (default)
How it works:
Compute sample Hessians
d^2 ell / dtheta^2for training data.Fit a regression model to predict Lambda-hat(x) from covariates X.
Predict Lambda-hat for the evaluation fold.
This requires 3-way splitting to avoid bias:
Fold A: train theta-hat(x).
Fold B: train Lambda-hat(x) using theta-hat from Fold A.
Fold C: evaluate psi using both.
Available estimation methods:
Method |
Correlation with oracle |
Coverage |
Default |
Notes |
|---|---|---|---|---|
|
0.508 |
96% |
Yes |
Recommended, validated coverage |
|
0.000 |
95% |
No |
Stable when Hessian does not depend on X |
|
0.978 |
96% |
No |
High accuracy alternative |
|
0.997 |
67% |
No |
AVOID: invalid SEs |
Note: MLP achieves the highest correlation with oracle Lambda but produces 67% coverage instead of 95%. High correlation does not guarantee valid inference. The variance of Lambda estimates matters.
LambdaStrategy protocol
All strategies implement:
class LambdaStrategy(Protocol):
requires_theta: bool # Does fit() need theta-hat?
requires_separate_fold: bool # 3-way cross-fitting?
def fit(self, X, T, Y, theta_hat, model) -> None:
"""Fit the strategy (if needed)."""
...
def predict(self, X, theta_hat) -> Tensor:
"""Return Lambda-hat(x) matrices of shape (n, d_theta, d_theta)."""
...
Strategy selection API
from deep_inference.lambda_ import select_lambda_strategy
strategy = select_lambda_strategy(
model=my_model,
is_randomized=True,
treatment_dist=Normal(0, 1),
lambda_method=None # auto-detect
)
To manually override:
result = inference(Y, T, X, model='logit', lambda_method='estimate')
Diagnostics
result = inference(Y, T, X, model='logit', verbose=True)
print(result.diagnostics['regime']) # 'A', 'B', or 'C'
print(result.diagnostics['lambda_method']) # 'ComputeLambda', 'AnalyticLambda', or 'EstimateLambda'
When to use each regime
Scenario |
Regime |
|---|---|
A/B test with known assignment probability |
A |
Survey experiment with normal dosage T ~ N(0, 1) |
A |
Linear regression with heterogeneous coefficients |
B |
Observational logit (health outcomes, etc.) |
C |
Custom nonlinear loss with unknown treatment selection |
C |
Models
Neural network architectures for structural estimation. Each model maps covariates X to structural parameters theta(X) and defines the loss, score, and Hessian.
StructuralNet
The backbone network. Input -> linear -> ReLU -> … -> theta_dim output.
Input (d features)
|
Linear(d, hidden_dims[0])
|
ReLU + Dropout
|
Linear(hidden_dims[0], hidden_dims[1])
|
ReLU + Dropout
|
...
|
Linear(hidden_dims[-1], theta_dim)
|
Output (theta_dim parameters per observation)
from deep_inference.models import StructuralNet
import torch
net = StructuralNet(
input_dim=10,
hidden_dims=[64, 32],
theta_dim=2,
dropout=0.1
)
X = torch.randn(100, 10)
theta = net(X) # (100, 2)
Training configuration:
Parameter |
Default |
Description |
|---|---|---|
|
|
Hidden layer sizes |
|
|
Training epochs |
|
|
Learning rate |
|
|
Mini-batch size |
|
|
L2 regularization |
|
|
Dropout rate |
Custom training loop:
import torch.nn as nn
from deep_inference.models import StructuralNet
from deep_inference import LinearFamily
net = StructuralNet(input_dim=10, hidden_dims=[64, 32], theta_dim=2)
family = LinearFamily()
optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
for epoch in range(100):
theta = net(X_tensor)
loss = family.loss(Y_tensor, T_tensor, theta).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
Built-in structural models
The inference() API accepts these model strings:
String |
Class |
theta_dim |
Notes |
|---|---|---|---|
|
|
2 |
Constant expected Hessian (Regime B) |
|
|
2 |
Softmax sigmoid output |
|
|
(J-1)+K |
McFadden conditional logit |
|
|
2 |
Smoothed check function |
|
|
4 |
Saturated 2x2 DiD |
|
|
1 |
Two-way FE panel DiD |
All built-in models are exported from deep_inference:
from deep_inference import Linear, Logit, MultinomialLogit, Quantile, CombinatorialModel, DiDModel, FEPanelDiDModel
MultinomialLogitModel
McFadden conditional logit for discrete choice with J alternatives and K attributes.
from deep_inference.models.multinomial import MultinomialLogitModel
model = MultinomialLogitModel(n_alternatives=3, n_attributes=2)
# theta_dim = (J-1) + K = 4
# theta = [alpha_1, ..., alpha_{J-1}, beta_1, ..., beta_K]
# Hessian: Fisher information (no Y dependence, depends on theta)
# Requires 3-way cross-fitting (Regime C, or Regime A if randomized)
Data encoding:
W= individual characteristics (NN input to get theta(W))T= packed alternative attributes(n, J*K), unpacked to(J, K)inside modelY= chosen alternative index(n,)as float (0, 1, …, J-1)
Example DGP (J=3, K=2, d_w=3): target mu* = E[beta_1(W)] = -0.8.
CombinatorialModel
For multi-treatment combinatorial experiments with binary treatment vectors T in {0,1}^m.
from deep_inference.models.combinatorial import CombinatorialModel
model = CombinatorialModel(n_treatments=3, link='gen_sigmoid_ii')
# theta_dim = m + 2 = 5 for gen_sigmoid_ii
Four link functions:
Link |
Formula |
theta_dim |
Description |
|---|---|---|---|
|
theta_0 * prod(1 + theta_k * t_k) |
m+1 |
Product interaction |
|
a/(1+exp(-(theta_0 + sum theta_k*t_k))) + b |
m+1 |
Bounded response with fixed scale |
|
theta_{m+1} * sigma(sum theta_k*t_k) |
m+1 |
Flexible scale, no intercept |
|
theta_{m+1} * sigma(theta_0 + sum theta_k*t_k) |
m+2 |
Most flexible (recommended) |
Hessian uses Fisher information: 2*G_theta*G_theta'. hessian_depends_on_theta = True,
hessian_depends_on_y = False. Requires 3-way cross-fitting (Regime C). For Regime A, compute
Lambda via Monte Carlo:
import torch
t_samples = torch.randint(0, 2, (1000, 3)).float()
Lambda = model.compute_lambda_integral(theta, t_samples)
Usage with MultiTreatmentATE:
from deep_inference.targets import MultiTreatmentATE
model = CombinatorialModel(n_treatments=3, link='gen_sigmoid_ii')
target = MultiTreatmentATE(model=model, treatment=[1, 0, 1])
Reference: Ye et al. (2025, Management Science), DeDL: Debiased Deep Learning for Combinatorial Experiments.
DiDModel
Saturated 2x2 DiD model. Network learns [alpha(X), gamma(X), lambda_coef(X), tau(X)].
from deep_inference import DiDModel
Accessible via inference(..., model='did', target='tau').
FEPanelDiDModel
Two-way fixed-effects panel DiD. Outcomes and treatment are residualized by unit and time FE
before the network learns tau(X) in the within-transformed regression.
from deep_inference import FEPanelDiDModel
Accessible via inference(..., model='did_fe', target='fe_effect').
CustomModel and model_from_loss
Build a structural model from any differentiable loss function.
from deep_inference import CustomModel
RieszNet module
deep_inference.riesz implements automatic debiasing via the Riesz representer
(Chernozhukov et al. 2022, ICML). Estimates the binary-treatment ATE contrast
E[g(1, X) - g(0, X)] using the doubly-robust moment, cross-fit over K folds and combined
across R repeats with the median-DML rule.
riesz_inference
from deep_inference import riesz_inference
result = riesz_inference(
Y, T, X,
outcome='linear', # 'linear', 'logit', 'poisson', 'gamma', 'negbin', 'probit'
n_folds=5,
n_repeats=3,
max_epochs=400,
patience=30,
seed=0,
nb_dispersion=3.0, # only used when outcome='negbin'
alpha=0.05, # 0.05 gives a 95% CI
store_data=True,
verbose=False,
) -> InferenceResult
Arguments:
Argument |
Meaning |
|---|---|
|
Outcomes |
|
Outcome model for the regression head. One of |
|
Cross-fitting folds K. |
|
Independent split repeats, median-DML combined. |
|
Per-fold training schedule. |
|
The negative-binomial size r, used only when |
|
CI level. |
Returns InferenceResult with mu_hat, se, ci_lower, ci_upper, psi_values
(doubly-robust moment from the first repeat), theta_hat (the learned representer a(Z) as
an (n, 1) tensor), and a diagnostics dict that includes procedure='riesznet', mu_naive,
mean_abs_representer, and per-repeat estimates.
Example:
import numpy as np
from deep_inference import riesz_inference
rng = np.random.default_rng(0)
n = 1500
X = rng.standard_normal((n, 3))
e = 1 / (1 + np.exp(-(0.8 * X[:, 0])))
T = (rng.random(n) < e).astype(float) # confounded binary treatment
Y = 1.0 + 0.5 * X[:, 0] + (0.5 + 0.3 * X[:, 1]) * T + rng.standard_normal(n)
result = riesz_inference(Y, T, X, outcome='linear', n_folds=5, n_repeats=3)
print(result.summary())
RieszNet
from deep_inference import RieszNet
The underlying torch.nn.Module. Architecture: shared trunk (3 ELU layers, width 200) with a
linear Riesz head and a regression head (2 ELU layers, width 100), plus an unpenalized
targeted-regularization parameter eps. Most users do not construct this directly.
riesz_inference builds, trains, and cross-fits it. It is exposed for inspection and for
custom training loops.
Metrics
Helper functions for computing inference quality metrics.
compute_coverage
from deep_inference import compute_coverage
covered = compute_coverage(mu_true, ci_lower, ci_upper)
# Returns bool: True if mu_true falls within [ci_lower, ci_upper]
compute_se_ratio
from deep_inference import compute_se_ratio
se_ratio = compute_se_ratio(estimated_se, empirical_se)
# Returns float: estimated_se / empirical_se (target: 1.0)
Key metrics
Metric |
Formula |
Target |
|---|---|---|
|
E[mu-hat] - mu* |
0 |
|
Var(mu-hat) |
- |
|
sqrt(Bias^2 + Var) |
Small |
|
sqrt(Var) |
- |
|
SE-hat / SE_emp |
1.0 |
|
P(mu* in CI) |
95% |
Validation targets
Metric |
Valid range |
Interpretation |
|---|---|---|
Coverage |
93-97% |
CI contains true value |
SE ratio |
0.9-1.2 |
SE properly calibrated |
min(lambda eigenvalue) |
> 1e-4 |
Hessian well-conditioned |
Monte Carlo validation example
import numpy as np
from deep_inference import structural_dml
M = 100 # number of simulations
N = 2000 # sample size
MU_TRUE = 0.5
results = []
for m in range(M):
np.random.seed(m)
X = np.random.randn(N, 10)
T = np.random.randn(N)
Y = X[:, 0] + MU_TRUE * T + np.random.randn(N)
result = structural_dml(Y, T, X, family='linear', verbose=False)
covered = result.ci_lower <= MU_TRUE <= result.ci_upper
results.append({'mu_hat': result.mu_hat, 'se': result.se, 'covered': covered})
coverage = np.mean([r['covered'] for r in results])
se_ratio = np.mean([r['se'] for r in results]) / np.std([r['mu_hat'] for r in results])
print(f"Coverage: {coverage:.1%}") # Target: 95%
print(f"SE Ratio: {se_ratio:.2f}") # Target: 1.0
Interpreting results
Good results:
Coverage: 95.0% [PASS - in 93-97% range]
SE Ratio: 1.02 [PASS - close to 1.0]
RMSE: 0.032 [Low bias and variance]
Warning signs:
Coverage: 30% [FAIL - severe undercoverage]
SE Ratio: 0.27 [FAIL - SE underestimated 4x]
Common causes of poor coverage: naive method (no IF correction), too few folds (K < 20), insufficient training epochs, or model misspecification.