diff --git a/doc/examples/index.rst b/doc/examples/index.rst index 394134d3..5382a253 100644 --- a/doc/examples/index.rst +++ b/doc/examples/index.rst @@ -18,6 +18,7 @@ General Examples :name: case-studies-py py_double_ml_basics.ipynb + py_double_ml_maketables.ipynb py_double_ml_pension.ipynb py_double_ml_sensitivity.ipynb py_double_ml_apo.ipynb diff --git a/doc/examples/py_double_ml_maketables.ipynb b/doc/examples/py_double_ml_maketables.ipynb new file mode 100644 index 00000000..3f7f6419 --- /dev/null +++ b/doc/examples/py_double_ml_maketables.ipynb @@ -0,0 +1,832 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d035f818", + "metadata": {}, + "source": [ + "# Regression tables with MakeTables\n", + "\n", + "[MakeTables](https://py-econometrics.github.io/maketables/) creates formatted regression tables from fitted DoubleML models. This example uses simulated data to show model comparisons, confidence intervals, multiple treatments, and HTML and LaTeX exports.\n", + "\n", + "This example requires the DoubleML development version with MakeTables support and `maketables>=0.2.0`. Install MakeTables separately with `python -m pip install maketables`. DoubleML itself does not require MakeTables.\n", + "\n", + "The integration applies to models inheriting from `DoubleML`, including `DoubleMLPLR`, `DoubleMLPLIV`, and `DoubleMLIRM`. It does not yet cover standalone result containers such as `DoubleMLFramework` or `DoubleMLBLP`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fff65940", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:33.388222Z", + "iopub.status.busy": "2026-11-02T20:40:33.388043Z", + "iopub.status.idle": "2026-11-02T20:40:35.939052Z", + "shell.execute_reply": "2026-11-02T20:40:35.910723Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/private/tmp/doubleml-maketables/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from IPython.display import HTML, display\n", + "from maketables import ETable\n", + "from sklearn.linear_model import LinearRegression, LogisticRegression, Ridge\n", + "\n", + "import doubleml as dml\n", + "\n", + "np.random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "id": "fde02221", + "metadata": {}, + "source": [ + "## Fit a partially linear model\n", + "\n", + "The simulated outcome depends on two treatments and three covariates. The treatment coefficients are 0.6 and 0.4. We estimate both effects with linear nuisance models and three repetitions of two-fold cross-fitting." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b32dc4fa", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:35.952500Z", + "iopub.status.busy": "2026-11-02T20:40:35.951734Z", + "iopub.status.idle": "2026-11-02T20:40:36.032883Z", + "shell.execute_reply": "2026-11-02T20:40:36.032036Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
coefstd errtP>|t|2.5 %97.5 %
education0.6073410.04105814.7924321.639224e-490.5268690.687812
experience0.3761110.0454078.2831451.199641e-160.2871150.465107
\n", + "
" + ], + "text/plain": [ + " coef std err t P>|t| 2.5 % 97.5 %\n", + "education 0.607341 0.041058 14.792432 1.639224e-49 0.526869 0.687812\n", + "experience 0.376111 0.045407 8.283145 1.199641e-16 0.287115 0.465107" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_obs = 500\n", + "x = np.random.normal(size=(n_obs, 3))\n", + "education = 0.5 * x[:, 0] + np.random.normal(size=n_obs)\n", + "experience = 0.3 * x[:, 1] + np.random.normal(size=n_obs)\n", + "income = 0.6 * education + 0.4 * experience + x[:, 2] + np.random.normal(size=n_obs)\n", + "frame = pd.DataFrame(x, columns=[\"x1\", \"x2\", \"x3\"])\n", + "frame[\"education\"] = education\n", + "frame[\"experience\"] = experience\n", + "frame[\"income\"] = income\n", + "\n", + "data = dml.DoubleMLData(frame, \"income\", [\"education\", \"experience\"])\n", + "linear = dml.DoubleMLPLR(data, LinearRegression(), LinearRegression(), n_folds=2, n_rep=3).fit()\n", + "linear.summary" + ] + }, + { + "cell_type": "markdown", + "id": "21e970fc", + "metadata": {}, + "source": [ + "## Create a table\n", + "\n", + "Pass the fitted model directly to `ETable`. By default, the table includes the sample size. The `coef_fmt` argument controls which statistics appear: `b` is the estimate, `se` its standard error, `t` its t-statistic, and `p` its p-value. An asterisk requests significance stars.\n", + "\n", + "Fit models before creating tables. An unfitted model has no coefficient rows, just as its DoubleML summary has no estimates." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "05cb4c3e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:36.039470Z", + "iopub.status.busy": "2026-11-02T20:40:36.038760Z", + "iopub.status.idle": "2026-11-02T20:40:36.066681Z", + "shell.execute_reply": "2026-11-02T20:40:36.065941Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
\n", + " income\n", + "
(1)
coef
Education0.607***
(0.041)
Experience0.376***
(0.045)
stats
Observations500
Significance levels: * p < 0.1, ** p < 0.05, *** p < 0.01. Format of coefficient cell: Coefficient (Std. Error)
\n", + "\n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = {\"education\": \"Education\", \"experience\": \"Experience\"}\n", + "table = ETable(linear, coef_fmt=\"b:.3f*\\n(se:.3f)\", labels=labels)\n", + "display(HTML(table.make(\"html\")))" + ] + }, + { + "cell_type": "markdown", + "id": "775283df", + "metadata": {}, + "source": [ + "## Compare nuisance learners\n", + "\n", + "Use the same sample splits to compare linear and ridge nuisance models. Each model becomes a column; both treatments appear as rows." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "28856448", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:36.070182Z", + "iopub.status.busy": "2026-11-02T20:40:36.068628Z", + "iopub.status.idle": "2026-11-02T20:40:36.121699Z", + "shell.execute_reply": "2026-11-02T20:40:36.120530Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
Treatment effects with different nuisance learners
\n", + "  \n", + " \n", + " income\n", + "
\n", + " Linear\n", + " \n", + " Ridge\n", + "
(1)(2)
coef
Education0.607***
(0.041)
0.608***
(0.041)
Experience0.376***
(0.045)
0.377***
(0.046)
stats
Observations500500
Significance levels: * p < 0.1, ** p < 0.05, *** p < 0.01. Format of coefficient cell: Coefficient (Std. Error)
\n", + "\n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ridge = dml.DoubleMLPLR(data, Ridge(alpha=10), Ridge(alpha=10), n_folds=2, n_rep=3, draw_sample_splitting=False)\n", + "ridge.set_sample_splitting(linear.smpls)\n", + "ridge.fit()\n", + "comparison = ETable(\n", + " [linear, ridge],\n", + " labels=labels,\n", + " model_heads=[\"Linear\", \"Ridge\"],\n", + " coef_fmt=\"b:.3f*\\n(se:.3f)\",\n", + " caption=\"Treatment effects with different nuisance learners\",\n", + ")\n", + "display(HTML(comparison.make(\"html\")))" + ] + }, + { + "cell_type": "markdown", + "id": "ba6d6443", + "metadata": {}, + "source": [ + "## Show confidence intervals\n", + "\n", + "The tokens `ci95l` and `ci95u` contain the bounds returned by DoubleML's pointwise 95% confidence intervals. For repeated sample splits, these use DoubleML's aggregation procedure. They are not simultaneous confidence intervals across treatments." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "39e3cb96", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:36.124999Z", + "iopub.status.busy": "2026-11-02T20:40:36.124621Z", + "iopub.status.idle": "2026-11-02T20:40:36.149503Z", + "shell.execute_reply": "2026-11-02T20:40:36.148709Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
\n", + " income\n", + "
(1)
coef
Education0.607
[0.527, 0.688]
Experience0.376
[0.287, 0.465]
stats
Observations500
Pointwise 95% confidence intervals in brackets.
\n", + "\n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "interval_table = ETable(\n", + " linear,\n", + " labels=labels,\n", + " coef_fmt=\"b:.3f\\n[ci95l:.3f, ci95u:.3f]\",\n", + " notes=\"Pointwise 95% confidence intervals in brackets.\",\n", + ")\n", + "display(HTML(interval_table.make(\"html\")))" + ] + }, + { + "cell_type": "markdown", + "id": "a2604d1a", + "metadata": {}, + "source": [ + "## Binary treatments\n", + "\n", + "The same interface works with `DoubleMLIRM`. Here the simulated treatment effect is 0.7, and treatment assignment depends on the covariates." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6bbd33e4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:36.151779Z", + "iopub.status.busy": "2026-11-02T20:40:36.151594Z", + "iopub.status.idle": "2026-11-02T20:40:36.205449Z", + "shell.execute_reply": "2026-11-02T20:40:36.204402Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
\n", + " y\n", + "
(1)
coef
d0.634***
(0.091)
stats
Observations500
Significance levels: * p < 0.1, ** p < 0.05, *** p < 0.01. Format of coefficient cell: Coefficient (Std. Error)
\n", + "\n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "propensity = 1 / (1 + np.exp(-0.5 * x[:, 0]))\n", + "treatment = np.random.binomial(1, propensity)\n", + "outcome = 0.7 * treatment + x[:, 2] + np.random.normal(size=n_obs)\n", + "irm_data = dml.DoubleMLData.from_arrays(x, outcome, treatment)\n", + "irm = dml.DoubleMLIRM(irm_data, LinearRegression(), LogisticRegression(), n_folds=2, n_rep=3).fit()\n", + "display(HTML(ETable(irm, coef_fmt=\"b:.3f*\\n(se:.3f)\").make(\"html\")))" + ] + }, + { + "cell_type": "markdown", + "id": "69baba3f", + "metadata": {}, + "source": [ + "## Export tables\n", + "\n", + "`make(\"tex\")` returns LaTeX source. To write files, use `table.save(\"tex\", \"results.tex\")` or `table.save(\"html\", \"results.html\")`. See the [MakeTables LaTeX guide](https://py-econometrics.github.io/maketables/docs/LatexTables.html) for document setup." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "07af0b52", + "metadata": { + "execution": { + "iopub.execute_input": "2026-11-02T20:40:36.208011Z", + "iopub.status.busy": "2026-11-02T20:40:36.207642Z", + "iopub.status.idle": "2026-11-02T20:40:36.212601Z", + "shell.execute_reply": "2026-11-02T20:40:36.211711Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\\begin{threeparttable}\n", + "\\begingroup\n", + "\\renewcommand\\cellalign{t}\n", + "\\renewcommand\\arraystretch{1}\n", + "\\setlength{\\tabcolsep}{3pt}\n", + "\\begin{tabularx}{\\linewidth}{@{}>{\\raggedright\\arraybackslash}l>{\\centering\\arraybackslash}X}\n", + "\\toprule\n", + " & \\multicolumn{1}{c}{income} \\\\\n", + "\\cmidrule(lr){2-2}\n", + " & (1) \\\\\n", + "\\midrule\n", + "\\addlinespace[1ex]\n", + "Education & \\makecell{0.607***\\\\(0.041)} \\\\\n", + "\\addlinespace[0.5ex]\n", + "\\addlinespace[0.5ex]\n", + "Experience & \\makecell{0.376***\\\\(0.045)} \\\\\n", + "\\addlinespace[0.5ex]\n", + "\\midrule\n", + "\\addlinespace[1ex]\n", + "Observations & 500 \\\\\n", + "\\addlinespace[0.5ex]\n", + "\\bottomrule\n", + "\\end{tabularx}\n", + "\\endgroup\n", + "\\noindent\\begin{minipage}{\\linewidth}\\smallskip\\footnotesize\n", + "Significance levels: * p < 0.1, ** p < 0.05, *** p < 0.01. Format of coefficient cell: Coefficient (Std. Error)\\end{minipage}\n", + "\n", + "\\end{threeparttable}\n" + ] + } + ], + "source": [ + "print(table.make(\"tex\"))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/requirements.txt b/requirements.txt index 45cc5c53..8bab5654 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ lightgbm flaml # notebooks +maketables>=0.2.0 ipykernel pyreadr # tabpfn # only relevant for TabPFN example notebooks \ No newline at end of file