Skip to content

Commit c7227e6

Browse files
authored
Merge pull request #164 from CITCOM-project/examples-as-tests
Examples now runnable with pytest. Closes #5. Closes #136.
2 parents dc2cbcd + 9fdc4a2 commit c7227e6

File tree

15 files changed

+701
-580
lines changed

15 files changed

+701
-580
lines changed

.github/workflows/ci-tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
conda install -c conda-forge pygraphviz
2626
python --version
2727
pip install -e .
28+
pip install -e .[test]
2829
pip install pytest pytest-cov
2930
shell: bash -l {0}
3031
- name: Test with pytest

examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*.pdf
2+
results/

examples/covasim_/doubling_beta/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ To run this case study:
1414
1. Ensure all project dependencies are installed by running `pip install .` from the top
1515
level of this directory (instructions are provided in the project README).
1616
2. Change directory to `causal_testing/examples/covasim_/doubling_beta`.
17-
3. Run the command `python causal_test_beta.py`.
17+
3. Run the command `python test_beta.py`.
1818

1919
This will print out a series of test results covering a range of different causal questions that correspond to those
20-
in Table 3 of the paper.
20+
in Table 3 of the paper.

examples/covasim_/doubling_beta/causal_test_beta.py renamed to examples/covasim_/doubling_beta/example_beta.py

Lines changed: 128 additions & 93 deletions
Large diffs are not rendered by default.

examples/covasim_/vaccinating_elderly/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ To run this case study:
1919
level of this directory (instructions are provided in the project README).
2020
2. Additionally, in order to run Covasim, install version 3.0.7 by running `pip install covasim==3.0.7`.
2121
3. Change directory to `causal_testing/examples/covasim_/vaccinating_elderly`.
22-
4. Run the command `python causal_test_vaccine.py`.
22+
4. Run the command `python test_vaccine.py`.
2323

2424
This will run Covasim as described above and print out the causal test results for the effect of each input on each
2525
output.
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
import pandas as pd
23
import numpy as np
34
import covasim as cv # Version used in our study is 3.07
@@ -13,9 +14,17 @@
1314
from causal_testing.testing.estimators import LinearRegressionEstimator
1415
from causal_testing.testing.base_test_case import BaseTestCase
1516

17+
import os
18+
import logging
1619

17-
def experimental_causal_test_vaccinate_elderly(runs_per_test_per_config: int = 30, verbose: bool = False):
18-
""" Run the causal test case for the effect of changing vaccine to prioritise elderly. This uses the experimental
20+
logger = logging.getLogger(__name__)
21+
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
22+
23+
ROOT = os.path.realpath(os.path.dirname(__file__))
24+
25+
26+
def test_experimental_vaccinate_elderly(runs_per_test_per_config: int = 30, verbose: bool = False):
27+
"""Run the causal test case for the effect of changing vaccine to prioritise elderly. This uses the experimental
1928
data collector.
2029
2130
:param runs_per_test_per_config: Number of times to run each input configuration (control and treatment) per test.
@@ -25,78 +34,79 @@ def experimental_causal_test_vaccinate_elderly(runs_per_test_per_config: int = 3
2534
"""
2635

2736
# 1. Read in the Causal DAG
28-
causal_dag = CausalDAG('dag.dot')
37+
causal_dag = CausalDAG(f"{ROOT}/dag.dot")
2938

3039
# 2. Create variables
31-
pop_size = Input('pop_size', int)
32-
pop_infected = Input('pop_infected', int)
33-
n_days = Input('n_days', int)
34-
vaccine = Input('vaccine', int)
35-
cum_infections = Output('cum_infections', int)
36-
cum_vaccinations = Output('cum_vaccinations', int)
37-
cum_vaccinated = Output('cum_vaccinated', int)
38-
max_doses = Output('max_doses', int)
40+
pop_size = Input("pop_size", int)
41+
pop_infected = Input("pop_infected", int)
42+
n_days = Input("n_days", int)
43+
vaccine = Input("vaccine", int)
44+
cum_infections = Output("cum_infections", int)
45+
cum_vaccinations = Output("cum_vaccinations", int)
46+
cum_vaccinated = Output("cum_vaccinated", int)
47+
max_doses = Output("max_doses", int)
3948

4049
# 3. Create scenario by applying constraints over a subset of the input variables
41-
scenario = Scenario(variables={pop_size, pop_infected, n_days, cum_infections, vaccine,
42-
cum_vaccinated, cum_vaccinations, max_doses},
43-
constraints={pop_size.z3 == 50000, pop_infected.z3 == 1000, n_days.z3 == 50})
50+
scenario = Scenario(
51+
variables={
52+
pop_size,
53+
pop_infected,
54+
n_days,
55+
cum_infections,
56+
vaccine,
57+
cum_vaccinated,
58+
cum_vaccinations,
59+
max_doses,
60+
},
61+
constraints={pop_size.z3 == 50000, pop_infected.z3 == 1000, n_days.z3 == 50},
62+
)
4463

4564
# 4. Construct a causal specification from the scenario and causal DAG
4665
causal_specification = CausalSpecification(scenario, causal_dag)
4766

4867
# 5. Instantiate the experimental data collector for Covasim
49-
covasim_parameters_dict = {'pop_size': 50000,
50-
'pop_type': 'hybrid',
51-
'pop_infected': 1000,
52-
'n_days': 50}
53-
control_input_configuration = {'covasim_parameters_dict': covasim_parameters_dict,
54-
'target_elderly': False}
55-
treatment_input_configuration = {'covasim_parameters_dict': covasim_parameters_dict,
56-
'target_elderly': True}
57-
data_collector = CovasimVaccineDataCollector(scenario, control_input_configuration,
58-
treatment_input_configuration,
59-
runs_per_test_per_config)
68+
covasim_parameters_dict = {"pop_size": 50000, "pop_type": "hybrid", "pop_infected": 1000, "n_days": 50}
69+
control_input_configuration = {"covasim_parameters_dict": covasim_parameters_dict, "target_elderly": False}
70+
treatment_input_configuration = {"covasim_parameters_dict": covasim_parameters_dict, "target_elderly": True}
71+
data_collector = CovasimVaccineDataCollector(
72+
scenario, control_input_configuration, treatment_input_configuration, runs_per_test_per_config
73+
)
6074

6175
# 6. Express expected outcomes
62-
expected_outcome_effects = {cum_infections: Positive(),
63-
cum_vaccinations: Negative(),
64-
cum_vaccinated: Negative(),
65-
max_doses: NoEffect()
66-
}
67-
results_dict = {'cum_infections': {},
68-
'cum_vaccinations': {},
69-
'cum_vaccinated': {},
70-
'max_doses': {}
71-
}
76+
expected_outcome_effects = {
77+
cum_infections: Positive(),
78+
cum_vaccinations: Negative(),
79+
cum_vaccinated: Negative(),
80+
max_doses: NoEffect(),
81+
}
82+
results_dict = {"cum_infections": {}, "cum_vaccinations": {}, "cum_vaccinated": {}, "max_doses": {}}
7283

7384
# 7. Create an instance of the causal test engine
7485
causal_test_engine = CausalTestEngine(causal_specification, data_collector, index_col=0)
7586

7687
for outcome_variable, expected_effect in expected_outcome_effects.items():
77-
base_test_case = BaseTestCase(treatment_variable=vaccine,
78-
outcome_variable=outcome_variable)
79-
causal_test_case = CausalTestCase(base_test_case=base_test_case,
80-
expected_causal_effect=expected_effect,
81-
control_value=0,
82-
treatment_value=1)
88+
base_test_case = BaseTestCase(treatment_variable=vaccine, outcome_variable=outcome_variable)
89+
causal_test_case = CausalTestCase(
90+
base_test_case=base_test_case, expected_causal_effect=expected_effect, control_value=0, treatment_value=1
91+
)
8392

8493
# 8. Obtain the minimal adjustment set for the causal test case from the causal DAG
8594
minimal_adjustment_set = causal_dag.identification(base_test_case)
8695

8796
# 9. Build statistical model
88-
linear_regression_estimator = LinearRegressionEstimator(vaccine.name, 1, 0,
89-
minimal_adjustment_set,
90-
outcome_variable.name)
97+
linear_regression_estimator = LinearRegressionEstimator(
98+
vaccine.name, 1, 0, minimal_adjustment_set, outcome_variable.name
99+
)
91100

92101
# 10. Execute test and save results in dict
93-
causal_test_result = causal_test_engine.execute_test(linear_regression_estimator, causal_test_case, 'ate')
102+
causal_test_result = causal_test_engine.execute_test(linear_regression_estimator, causal_test_case, "ate")
94103
if verbose:
95-
print(f"Causation:\n{causal_test_result}")
96-
results_dict[outcome_variable.name]['ate'] = causal_test_result.test_value.value
97-
results_dict[outcome_variable.name]['cis'] = causal_test_result.confidence_intervals
98-
results_dict[outcome_variable.name]['test_passes'] = causal_test_case.expected_causal_effect.apply(
99-
causal_test_result)
104+
logging.info("Causation:\n%s", causal_test_result)
105+
results_dict[outcome_variable.name]["ate"] = causal_test_result.test_value.value
106+
results_dict[outcome_variable.name]["cis"] = causal_test_result.confidence_intervals
107+
results_dict[outcome_variable.name]["test_passes"] = causal_test_case.expected_causal_effect.apply(
108+
causal_test_result
109+
)
100110
return results_dict
101111

102112

@@ -108,19 +118,19 @@ class CovasimVaccineDataCollector(ExperimentalDataCollector):
108118
"""
109119

110120
def run_system_with_input_configuration(self, input_configuration: dict) -> pd.DataFrame:
111-
""" Run the system with a given input configuration.
121+
"""Run the system with a given input configuration.
112122
113123
:param input_configuration: A nested dictionary containing Covasim parameters, desired number of repeats, and
114124
a bool to determine whether elderly should be prioritised for vaccination.
115125
:return: A dataframe containing results for this input configuration.
116126
"""
117-
results_df = self.simulate_vaccine(input_configuration['covasim_parameters_dict'],
118-
self.n_repeats,
119-
input_configuration['target_elderly'])
127+
results_df = self.simulate_vaccine(
128+
input_configuration["covasim_parameters_dict"], self.n_repeats, input_configuration["target_elderly"]
129+
)
120130
return results_df
121131

122132
def simulate_vaccine(self, pars_dict: dict, n_simulations: int = 100, target_elderly: bool = False):
123-
""" Simulate observational data that contains a vaccine that is optionally given preferentially to the elderly.
133+
"""Simulate observational data that contains a vaccine that is optionally given preferentially to the elderly.
124134
125135
:param pars_dict: A dictionary containing simulation parameters.
126136
:param n_simulations: Number of simulations to run.
@@ -129,38 +139,49 @@ def simulate_vaccine(self, pars_dict: dict, n_simulations: int = 100, target_eld
129139
"""
130140
simulations_results_dfs = []
131141
for sim_n in range(n_simulations):
132-
print(f'Simulation {sim_n + 1}/{n_simulations}.')
142+
logging.info("Simulation %s/%s.", sim_n + 1, n_simulations)
133143

134144
# Update simulation parameters with vaccine and optionally sub-target
135145
if target_elderly:
136-
print("Prioritising the elderly for vaccination")
137-
vaccine = cv.vaccinate_prob(vaccine="Pfizer", label="prioritise_elderly",
138-
subtarget=self.vaccinate_by_age, days=list(range(7, pars_dict['n_days'])))
146+
logger.info("Prioritising the elderly for vaccination")
147+
vaccine = cv.vaccinate_prob(
148+
vaccine="Pfizer",
149+
label="prioritise_elderly",
150+
subtarget=self.vaccinate_by_age,
151+
days=list(range(7, pars_dict["n_days"])),
152+
)
139153
else:
140-
print("Using standard vaccination protocol")
141-
vaccine = cv.vaccinate_prob(vaccine="Pfizer", label="regular", days=list(range(7, pars_dict['n_days'])))
142-
143-
pars_dict['interventions'] = vaccine
144-
pars_dict['use_waning'] = True # Must be set to true for vaccination
145-
sim_results_df = self.run_sim_with_pars(pars_dict=pars_dict,
146-
desired_outputs=['cum_infections', 'cum_deaths', 'cum_recoveries',
147-
'cum_vaccinations', 'cum_vaccinated'],
148-
n_runs=1)
149-
150-
sim_results_df['interventions'] = vaccine.label # Store label in results instead of vaccine object
151-
sim_results_df['target_elderly'] = target_elderly
152-
sim_results_df['vaccine'] = int(target_elderly) # 0 if standard vaccine, 1 if target elderly vaccine
153-
sim_results_df['max_doses'] = vaccine.p['doses'] # Get max doses for the vaccine
154+
logger.info("Using standard vaccination protocol")
155+
vaccine = cv.vaccinate_prob(vaccine="Pfizer", label="regular", days=list(range(7, pars_dict["n_days"])))
156+
157+
pars_dict["interventions"] = vaccine
158+
pars_dict["use_waning"] = True # Must be set to true for vaccination
159+
sim_results_df = self.run_sim_with_pars(
160+
pars_dict=pars_dict,
161+
desired_outputs=[
162+
"cum_infections",
163+
"cum_deaths",
164+
"cum_recoveries",
165+
"cum_vaccinations",
166+
"cum_vaccinated",
167+
],
168+
n_runs=1,
169+
)
170+
171+
sim_results_df["interventions"] = vaccine.label # Store label in results instead of vaccine object
172+
sim_results_df["target_elderly"] = target_elderly
173+
sim_results_df["vaccine"] = int(target_elderly) # 0 if standard vaccine, 1 if target elderly vaccine
174+
sim_results_df["max_doses"] = vaccine.p["doses"] # Get max doses for the vaccine
154175
simulations_results_dfs.append(sim_results_df)
155176

156177
# Create a single dataframe containing a row for every execution
157178
obs_df = pd.concat(simulations_results_dfs, ignore_index=True)
158-
obs_df.rename(columns={'interventions': 'vaccine_type'}, inplace=True)
179+
obs_df.rename(columns={"interventions": "vaccine_type"}, inplace=True)
159180
return obs_df
160181

161182
@staticmethod
162183
def run_sim_with_pars(pars_dict: dict, desired_outputs: [str], n_runs: int = 1, verbose: int = -1):
163-
""" Runs a Covasim COVID-19 simulation with a given dict of parameters and collects the desired outputs,
184+
"""Runs a Covasim COVID-19 simulation with a given dict of parameters and collects the desired outputs,
164185
which are given as a list of output names.
165186
166187
:param pars_dict: A dictionary containing the parameters and their values for the run.
@@ -170,17 +191,17 @@ def run_sim_with_pars(pars_dict: dict, desired_outputs: [str], n_runs: int = 1,
170191
171192
:return results_df: A pandas df containing the results for each run
172193
"""
173-
results_dict = {k: [] for k in list(pars_dict.keys()) + desired_outputs + ['rand_seed']}
194+
results_dict = {k: [] for k in list(pars_dict.keys()) + desired_outputs + ["rand_seed"]}
174195
for _ in range(n_runs):
175196
# For every run, generate and use a new a random seed.
176197
# This is to avoid using Covasim's sequential random seeds.
177198
random.seed()
178199
rand_seed = random.randint(0, 10000)
179-
pars_dict['rand_seed'] = rand_seed
180-
print(f"Rand Seed: {rand_seed}")
200+
pars_dict["rand_seed"] = rand_seed
201+
logger.info("Rand Seed: %s", rand_seed)
181202
sim = cv.Sim(pars=pars_dict)
182203
m_sim = cv.MultiSim(sim)
183-
m_sim.run(n_runs=1, verbose=verbose, n_cpus=1)
204+
m_sim.run(n_runs=1, verbose=False, n_cpus=1)
184205

185206
for run in m_sim.sims:
186207
results = run.results
@@ -191,14 +212,15 @@ def run_sim_with_pars(pars_dict: dict, desired_outputs: [str], n_runs: int = 1,
191212
# Append outputs to results
192213
for output in desired_outputs:
193214
if output not in results:
194-
raise IndexError(f'{output} is not in the Covasim outputs.')
215+
raise IndexError(f"{output} is not in the Covasim outputs.")
195216
results_dict[output].append(
196-
results[output][-1]) # Append the final recorded value for each variable
217+
results[output][-1]
218+
) # Append the final recorded value for each variable
197219

198220
# Any parameters without results are assigned np.nan for each execution
199221
for param, results in results_dict.items():
200222
if not results:
201-
results_dict[param] = [np.nan] * len(results_dict['rand_seed'])
223+
results_dict[param] = [np.nan] * len(results_dict["rand_seed"])
202224
return pd.DataFrame(results_dict)
203225

204226
@staticmethod
@@ -223,5 +245,5 @@ def vaccinate_by_age(simulation):
223245

224246

225247
if __name__ == "__main__":
226-
test_results = experimental_causal_test_vaccinate_elderly(runs_per_test_per_config=30, verbose=True)
227-
print(test_results)
248+
test_results = test_experimental_vaccinate_elderly(runs_per_test_per_config=30, verbose=True)
249+
logging.info("%s", test_results)

examples/lr91/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ To run this case study:
1818
1. Ensure all project dependencies are installed by running `pip install .` in the top level directory
1919
(instructions are provided in the project README).
2020
2. Change directory to `causal_testing/examples/lr91`.
21-
3. Run the command `python causal_test_max_conductances.py` or `python causal_test_max_conductances_test_suite.py`
21+
3. Run the command `python test_max_conductances.py` or `python test_max_conductances_test_suite.py`
2222

2323
This should print a series of causal test results covering the effects of a range of different sized interventions made
24-
to the inputs on APD90, and should also plot Figure 2 from the paper.
24+
to the inputs on APD90, and should also plot Figure 2 from the paper.

0 commit comments

Comments
 (0)