where \(\tilde W_i = (1, W_i^\top)^\top\). The first block estimates the linear working model for \(e(W)\), and the second block is the E-estimating equation for \(\beta\).
In a just-identified system like this, the point estimate has the familiar ratio form
but summary() reports a stacked-moment sandwich covariance so the nuisance estimation step is accounted for directly.
Show code
from html import escapeimport numpy as npfrom IPython.display import HTML, displayimport crabbymetrics as cmdef html_table(headers, rows): parts = ["<table>","<thead>","<tr>",*[f"<th>{escape(str(header))}</th>"for header in headers],"</tr>","</thead>","<tbody>", ]for row in rows: parts.append("<tr>")for cell in row: parts.append(f"<td>{escape(str(cell))}</td>") parts.append("</tr>") parts.extend(["</tbody>", "</table>"])return"".join(parts)rng = np.random.default_rng(2026)n =1200w = rng.normal(size=(n, 4))d =0.5+ w @ np.array([0.8, -0.5, 0.3, 0.2]) + rng.normal(scale=0.8, size=n)g =1.0+ w @ np.array([0.4, -0.2, 0.3, 0.1]) +0.5* w[:, 0] * w[:, 1]y =1.75* d + g + rng.normal(scale=0.7, size=n)naive_design = np.column_stack([np.ones(n), d, w])naive_beta = np.linalg.lstsq(naive_design, y, rcond=None)[0][1]model = cm.EPLM()model.fit(y, d, w)summary = model.summary()rows = [ ["True beta", f"{1.75: .4f}"], ["Naive OLS on [D, W]", f"{naive_beta: .4f}"], ["EPLM", f"{summary['coef']: .4f}"], ["EPLM SE", f"{summary['se']: .4f}"],]display(HTML(html_table(["Quantity", "Value"], rows)))
Quantity
Value
True beta
1.7500
Naive OLS on [D, W]
1.7228
EPLM
1.7228
EPLM SE
0.0324
Interpretation
This is the partially linear coefficient on the scalar treatment \(D\), not a generic nonparametric effect.
The current implementation is continuous-treatment only.
The nuisance model for \(e(W)\) is linear in the supplied controls. If that is too rigid, the cross-fit PartiallyLinearDML estimator is the next page to look at.