Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Checklist
Before you mark your PR as ready for review, please ensure you have completed the following.

- [ ] Added unit tests for new functionality
- [ ] Properly documented any new code
- [ ] Updated Sphinx documentation in `docs/` and checked for updated configuration/runtime options
- [ ] Added a jupyter notebook tutorial in `docs/source/tutorials` for any new functionality
- [ ] Verified local docs build with `sphinx-build -W source/ build`
29 changes: 29 additions & 0 deletions .github/workflows/build-docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Build docs

on:
pull_request:
branches:
- main

jobs:
build:
name: MegaLinter
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Pandoc
uses: pandoc/actions/setup@v1
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install dependencies
run: |
python --version
python -m pip install --upgrade pip
pip install -e .[dev]
- name: "Build docs"
working-directory: "docs"
run: |
sphinx-build -W source/ build
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests-drafts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
python --version
python -m pip install --upgrade pip
pip install -e .
pip install -e .[test]
pip install -e .[dev]
pip install pytest pytest-cov
- name: Register Jupyter Kernel
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
python --version
python -m pip install --upgrade pip
pip install -e .
pip install -e .[test]
pip install -e .[dev]
pip install pytest pytest-cov
- name: Register Jupyter Kernel
run: |
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ the inputs and outputs of the system under test, supported by mathematical found
enable causal inference. Each causal test case targets the causal effect of a specific intervention on the system under test--that is,
a deliberate modification to the input configuration expected to produce a corresponding change in one or more outputs.

![Causal Testing Workflow](images/schematic-dark.png#gh-dark-mode-only)
![Causal Testing Workflow](images/schematic.png#gh-light-mode-only)
![Causal Testing Workflow](docs/source/_static/images/testing-workflow-dark.png#gh-dark-mode-only)
![Causal Testing Workflow](docs/source/_static/images/testing-workflow.png#gh-light-mode-only)

## Installation

Expand Down
7 changes: 5 additions & 2 deletions causal_testing/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
parser_discover.add_argument(
"-t",
"--technique",
help="The name of the technique to use. Currently supported are 'HillClimberDiscovery' and 'NSGADiscovery'",
required=True,
default="HillClimberDiscovery",
help=(
"The name of the technique to use. Currently supported are 'HillClimberDiscovery' and 'NSGADiscovery'. "
"Defaults to HillClimberDiscovery."
),
)
parser_discover.add_argument(
"-V",
Expand Down
2 changes: 1 addition & 1 deletion causal_testing/discovery/nsga_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def binary_string_to_causal_dag(self, individual: np.array) -> CausalDAG:
Converts a binary string representation of a causal DAG to a CausalDAG object.

:param individual: Bitstring of the same length as `possible_edges` such that 1 at position `i` represents
possible_edges[i] being an edge in the graph and 0 represents it not being.
possible_edges[i] being an edge in the graph and 0 represents it not being.
:returns: The converted CausalDAG instance.
"""
causal_dag = CausalDAG()
Expand Down
10 changes: 7 additions & 3 deletions causal_testing/estimation/abstract_regression_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
def _get_adjusted_variables(self, tree: ast.AST) -> set[str]:
"""
Recursively return variables in an AST.

:returns: Set of all variables not used as part of a function.
"""
if isinstance(tree, ast.Name) and tree.id != self.treatment_variable:
Expand Down Expand Up @@ -108,7 +109,9 @@ def _setup_covariates(self, df: pd.DataFrame) -> pd.Series:
Parse the formula and set up the covariates from the design matrix so we can use them in the statsmodels array
API. This allows us to only parse the formula once, rather than using the formula API, which parses it every
time the regression model is fit, which can be a lot if using causal test adequacy.

:param df: The data to use.

:returns: The data and the covariate columns.
"""
_, covariate_data = dmatrices(self.formula, df, return_type="dataframe")
Expand Down Expand Up @@ -154,9 +157,10 @@ def treatment_columns(self, model: RegressionResultsWrapper) -> list[str]:
This is a workaround for statsmodels mangling the names of categorical variables to include the values.

:param model: The fitted model from which to extract the variable names.

:returns: A list of the feature names in the model that represent the treatment. Normally this will just be
[treatment_name], but for categorical treatments, you'll have
[treatment_name[value_1], treatment_name[value_2]].
[treatment_name], but for categorical treatments, you'll have
[treatment_name[value_1], treatment_name[value_2]].
"""
return [
param
Expand All @@ -170,7 +174,7 @@ def _predict(self, df) -> pd.DataFrame:
:param df: The data to use.
:param: adjustment_config: The values of the adjustment variables to use.

:return: The estimated outcome under control and treatment, with confidence intervals in the form of a
:returns: The estimated outcome under control and treatment, with confidence intervals in the form of a
dataframe with columns "predicted", "se", "ci_lower", and "ci_upper".
"""
model = self.fit_model(df)
Expand Down
10 changes: 6 additions & 4 deletions causal_testing/testing/causal_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CausalTestCase:
variables, a CausalTestCase stores the values of these variables. Also the outcome variable and value are
specified. The goal of a CausalTestCase is to test whether the intervention made to the control via the treatment
causes the model-under-test to produce the expected change.

:param base_test_case: A BaseTestCase object consisting of a treatment variable, outcome variable and effect
:param expected_causal_effect: The expected causal effect (Positive, Negative, No Effect).
:param effect_measure: A string which denotes the type of estimate to return.
Expand Down Expand Up @@ -70,10 +71,11 @@ def measure_adequacy(
) -> DataAdequacy:
"""
Calculate the adequacy measurement, and populate the data_adequacy field.

:param df: The original dataset to use.
:param bootstrap_size: The number of bootstrap samples to use. (Defaults to 100)
:param group_by: For IPCWEstimator - the "id" column to ensure that entire individuals are sampled rather than
random rows.
random rows.
"""
results = []
outcomes = []
Expand Down Expand Up @@ -130,8 +132,7 @@ def execute_test(
:param suppress_estimation_errors: Set to True to suppress estimation errors. (Defaults to False)
:param bootstrap_size: The number of bootstrap samples to use. (Defaults to 100)
:param group_by: For IPCWEstimator - the "id" column to ensure that entire individuals are sampled rather than
random rows.
:return causal_test_result: A CausalTestResult for the executed causal test case.
random rows.
"""
if not self.skip:
try:
Expand Down Expand Up @@ -161,7 +162,8 @@ def estimate_effect(self, df: pd.DataFrame) -> CausalTestResult:
Execute a causal test case and return the causal test result.

:param df: The data to use.
:return causal_test_result: A CausalTestResult for the executed causal test case.

:returns: A CausalTestResult for the executed causal test case.
"""
if self.query:
df = df.query(self.query)
Expand Down
3 changes: 3 additions & 0 deletions docs/source/_static/images/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.aux
*.log
*.pdf
Binary file added docs/source/_static/images/discovery-workflow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 114 additions & 0 deletions docs/source/_static/images/discovery-workflow.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
\documentclass{standalone}

\usepackage{tikz}
\usetikzlibrary{arrows.meta,positioning,shapes,calc,fit,overlay-beamer-styles, backgrounds}
\usepackage{dsfont,pifont}
\newcommand*{\expe}{\mathds{E}}
\usepackage{amsmath}
\usepackage{booktabs}
\usepackage{fontawesome7}

\usepackage[default]{FiraSans}
\usepackage[mathrm=sym]{unicode-math}
\setmathfont{Fira Math}

\newcommand{\indep}{\perp \!\!\! \perp}

\begin{document}
\tikzset{
node/.style={circle, draw, minimum size=3ex, inner sep=0.2},
edge/.style={-{Stealth[length=3mm]}},
}

\newcommand{\cmark}{\ding{51}}%
\newcommand{\xmark}{\ding{55}}%

\begin{tikzpicture}[background rectangle/.style={fill=none}, show background rectangle, color=black]

% Data
\begin{scope}[name prefix=data-, local bounding box=test-data]
\node[draw=none, rectangle] (title) {Test Data};
\node[anchor=north] (table) at (title.south) {
\begin{tabular}{rrrrrr}
\toprule
$X_1$ & $X_2$ & $I$ & $Y_1$ & $Y_2$ & $Y_3$ \\
\midrule
1.2 & ``UK'' & 0.3 & 7.8 & 4 & True \\
3.2 & ``UK'' & 0.1 & 7.6 & 8 & False \\
\multicolumn{6}{c}{$\vdots$} \\
\bottomrule
\end{tabular}
};
\node[draw, rectangle] [fit=(title) (table)] {};
\end{scope}


% ci
\begin{scope}[name prefix=ci-, local bounding box=ci, anchor=west, shift={($(data-test-data.east) + (1, 0)$)}]
\node[draw=none, rectangle, shift={(0, -0.05)}] (title) at ({(0, 0)} |- data-title) {Causal Discovery};
\begin{scope}[shift={($(title.south) + (-0.65, -0.3)$)}, local bounding box=brain]
\path[draw,line width=0.025cm] (0.9871, -0.2624) circle (0.066cm);
\path[draw,line width=0.025cm] (0.8736, -0.5912) circle (0.066cm);
\path[draw,line width=0.025cm] (0.8597, -0.8965) circle (0.066cm);
\path[draw,line width=0.025cm] (1.0417, -1.1239) circle (0.066cm);
\path[draw,line width=0.025cm] (0.4807, -1.1496) circle (0.066cm);
\path[draw,line width=0.025cm] (0.4566, -0.876) circle (0.066cm);
\path[draw,line width=0.025cm] (0.2702, -0.5893) circle (0.066cm);
\path[draw,line width=0.025cm] (0.4498, -0.313) circle (0.066cm);

\path[draw,line width=0.025cm,miter limit=4.0] (0.5183, 0) -- (0.1888, -0.1903) -- (0.1888, -0.4236) -- (0, -0.5325) -- (0, -0.8925) -- (0.1592, -0.9844) -- (0.1592, -1.2186) -- (0.5134, -1.4231) -- (0.6931, -1.3338) -- (0.8729, -1.4231) -- (1.227, -1.2186) -- (1.227, -0.9844) -- (1.3862, -0.8925) -- (1.3862, -0.5325) -- (1.1975, -0.4236) -- (1.1975, -0.1903) -- (0.868, 0) -- (0.6931, -0.101) -- cycle;
\path[draw,line width=0.025cm,miter limit=4.0] (0.6931, -1.3338) -- (0.6931, -0.101);
\path[draw,line width=0.025cm] (0.3839, -0.313) -- (0.1888, -0.313);
\path[draw,line width=0.025cm] (0.2702, -0.5234) -- (0.2702, -0.313);
\path[draw,line width=0.025cm] (0.4567, -0.81) -- (0.4567, -0.5785) -- (0.6931, -0.5785);
\path[draw,line width=0.025cm] (0.4148, -1.1496) -- (0.2428, -1.1496) -- (0.2428, -0.8899) -- (0, -0.8899);
\path[draw,line width=0.025cm] (0.9871, -0.3283) -- (0.9871, -0.4203) -- (0.6931, -0.4203);
\path[draw,line width=0.025cm] (0.8736, -0.5253) -- (0.8736, -0.4203);
\path[draw,line width=0.025cm] (0.9256, -0.8965) -- (1.1381, -0.8965) -- (1.1381, -0.7111) -- (1.3862, -0.7111);
\path[draw,line width=0.025cm] (0.9758, -1.1239) -- (0.8728, -1.1239) -- (0.8728, -1.423);
\end{scope}

\coordinate (bot) at ({(0, 0)} |- data-table.south);

\node[draw, rectangle] [fit=(title) (brain) (bot)] {};
\end{scope}

% Causal DAG
\begin{scope}[name prefix=dag-, anchor=west, shift={($(ci-ci.east) + (1.275, 0)$)}]
\node[draw=none, rectangle, shift={(0, -0.05)}] (title) at ({(0, 0)} |- data-title) {Causal DAG};

% Shift origin to title's bottom edge, then push down by 1cm
\begin{scope}[anchor=north, shift={([yshift=-1.5cm]title.south)}, local bounding box=dag]
\node[node] (x1) at (-1, 0) {$X_1$};
\node[node] (x2) at (-1, 1.4) {$X_2$};
\node[node] (i) at (0, 0.7) {$I$};
\node[node] (y1) at (1,0) {$Y_{1}$};
\node[node] (y2) at (1,0.7) {$Y_2$};
\node[node] (y3) at (1,1.4) {$Y_3$};

\draw[edge] (x1) to (i);
\draw[edge] (x2) to (i);
\draw[edge] (i) to (y1);
\draw[edge] (i) to (y2);
\draw[edge] (i) to (y3);
\draw[edge] (x1) to (y1);
\draw[edge] (x2) to (y3);
\end{scope}

\node[draw=none, rectangle] (nodes) [fit=(x1) (x2) (y1) (y2) (y3) (i)] {};

% DAG outline
\coordinate (top) at ({(0, 0)} |- data-title.north);
\coordinate (bot) at ({(0, 0)} |- data-table.south);
\node[draw, rectangle] (dag) [fit=(dag-title) (dag) (top) (bot)] {};
\end{scope}





%Information flow
\draw[edge,dashed] (data-test-data) -- (ci-ci);
\draw[edge, dashed] (ci-ci) -- (dag-dag);
\end{tikzpicture}
\end{document}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/images/testing-workflow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading