Skip to content

Commit 282a225

Browse files
authored
Merge pull request #244 from CITCOM-project/update-covasim
Update: edited example_vaccine.py
2 parents 3b00417 + 4bb55d9 commit 282a225

File tree

3 files changed

+60
-157
lines changed

3 files changed

+60
-157
lines changed

examples/covasim_/vaccinating_elderly/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ four test cases: one focusing on each of the four previously mentioned outputs.
1313

1414
Further details are provided in Section 5.3 (Prioritising the elderly for vaccination) of the paper.
1515

16+
**Note**: this version of the CTF utilises the observational data collector in order to separate the software execution
17+
and testing. Older versions of this framework simulate the data using the custom experimental data collector and the
18+
`covasim` package (version 3.0.7) as outlined below.
19+
1620
## How to run
1721
To run this case study:
1822
1. Ensure all project dependencies are installed by running `pip install .` from the top
Lines changed: 25 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
# -*- coding: utf-8 -*-
2+
import os
3+
import logging
24
import pandas as pd
3-
import numpy as np
4-
import covasim as cv # Version used in our study is 3.07
5-
import random
65
from causal_testing.specification.causal_dag import CausalDAG
76
from causal_testing.specification.scenario import Scenario
87
from causal_testing.specification.variable import Input, Output
98
from causal_testing.specification.causal_specification import CausalSpecification
10-
from causal_testing.data_collection.data_collector import ExperimentalDataCollector
9+
from causal_testing.data_collection.data_collector import ObservationalDataCollector
1110
from causal_testing.testing.causal_test_case import CausalTestCase
1211
from causal_testing.testing.causal_test_outcome import Positive, Negative, NoEffect
1312
from causal_testing.testing.estimators import LinearRegressionEstimator
1413
from causal_testing.testing.base_test_case import BaseTestCase
1514

16-
import os
17-
import logging
1815

1916
logger = logging.getLogger(__name__)
2017
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
21-
2218
ROOT = os.path.realpath(os.path.dirname(__file__))
2319

2420

25-
def test_experimental_vaccinate_elderly(runs_per_test_per_config: int = 30, verbose: bool = False):
26-
"""Run the causal test case for the effect of changing vaccine to prioritise elderly. This uses the experimental
27-
data collector.
21+
def setup_test_case(verbose: bool = False):
22+
"""Run the causal test case for the effect of changing vaccine to prioritise elderly from an observational
23+
data collector that was previously simulated.
2824
29-
:param runs_per_test_per_config: Number of times to run each input configuration (control and treatment) per test.
30-
Hence, the total number of runs per test will be twice this value.
3125
:param verbose: Whether to print verbose details (causal test results).
3226
:return results_dict: A dictionary containing ATE, 95% CIs, and Test Pass/Fail
3327
"""
@@ -63,13 +57,10 @@ def test_experimental_vaccinate_elderly(runs_per_test_per_config: int = 30, verb
6357
# 4. Construct a causal specification from the scenario and causal DAG
6458
causal_specification = CausalSpecification(scenario, causal_dag)
6559

66-
# 5. Instantiate the experimental data collector for Covasim
67-
covasim_parameters_dict = {"pop_size": 50000, "pop_type": "hybrid", "pop_infected": 1000, "n_days": 50}
68-
control_input_configuration = {"covasim_parameters_dict": covasim_parameters_dict, "target_elderly": False}
69-
treatment_input_configuration = {"covasim_parameters_dict": covasim_parameters_dict, "target_elderly": True}
70-
data_collector = CovasimVaccineDataCollector(
71-
scenario, control_input_configuration, treatment_input_configuration, runs_per_test_per_config
72-
)
60+
# 5. Instantiate the observational data collector using the previously simulated data
61+
obs_df = pd.read_csv("simulated_data.csv")
62+
63+
data_collector = ObservationalDataCollector(scenario, obs_df)
7364

7465
# 6. Express expected outcomes
7566
expected_outcome_effects = {
@@ -85,161 +76,38 @@ def test_experimental_vaccinate_elderly(runs_per_test_per_config: int = 30, verb
8576
causal_test_case = CausalTestCase(
8677
base_test_case=base_test_case, expected_causal_effect=expected_effect, control_value=0, treatment_value=1
8778
)
88-
8979
# 7. Obtain the minimal adjustment set for the causal test case from the causal DAG
9080
minimal_adjustment_set = causal_dag.identification(base_test_case)
9181

92-
# 8. Build statistical model
82+
# 8. Build statistical model using the Linear Regression estimator
9383
linear_regression_estimator = LinearRegressionEstimator(
94-
vaccine.name, 1, 0, minimal_adjustment_set, outcome_variable.name
84+
treatment=vaccine.name,
85+
treatment_value=1,
86+
control_value=0,
87+
adjustment_set=minimal_adjustment_set,
88+
outcome=outcome_variable.name,
89+
df=obs_df,
9590
)
9691

9792
# 9. Execute test and save results in dict
9893
causal_test_result = causal_test_case.execute_test(linear_regression_estimator, data_collector)
94+
9995
if verbose:
10096
logging.info("Causation:\n%s", causal_test_result)
97+
10198
results_dict[outcome_variable.name]["ate"] = causal_test_result.test_value.value
99+
102100
results_dict[outcome_variable.name]["cis"] = causal_test_result.confidence_intervals
101+
103102
results_dict[outcome_variable.name]["test_passes"] = causal_test_case.expected_causal_effect.apply(
104103
causal_test_result
105104
)
106-
return results_dict
107-
108-
109-
class CovasimVaccineDataCollector(ExperimentalDataCollector):
110-
"""A custom experimental data collector for the elderly vaccination Covasim case study.
111105

112-
This experimental data collector runs covasim with a normal Pfizer vaccine and then again with the same vaccine but
113-
this time prioritising the elderly for vaccination.
114-
"""
106+
return results_dict
115107

116-
def run_system_with_input_configuration(self, input_configuration: dict) -> pd.DataFrame:
117-
"""Run the system with a given input configuration.
118108

119-
:param input_configuration: A nested dictionary containing Covasim parameters, desired number of repeats, and
120-
a bool to determine whether elderly should be prioritised for vaccination.
121-
:return: A dataframe containing results for this input configuration.
122-
"""
123-
results_df = self.simulate_vaccine(
124-
input_configuration["covasim_parameters_dict"], self.n_repeats, input_configuration["target_elderly"]
125-
)
126-
return results_df
127-
128-
def simulate_vaccine(self, pars_dict: dict, n_simulations: int = 100, target_elderly: bool = False):
129-
"""Simulate observational data that contains a vaccine that is optionally given preferentially to the elderly.
130-
131-
:param pars_dict: A dictionary containing simulation parameters.
132-
:param n_simulations: Number of simulations to run.
133-
:param target_elderly: Whether to prioritise vaccination for the elderly.
134-
:return: A pandas dataframe containing results for each run.
135-
"""
136-
simulations_results_dfs = []
137-
for sim_n in range(n_simulations):
138-
logging.info("Simulation %s/%s.", sim_n + 1, n_simulations)
139-
140-
# Update simulation parameters with vaccine and optionally sub-target
141-
if target_elderly:
142-
logger.info("Prioritising the elderly for vaccination")
143-
vaccine = cv.vaccinate_prob(
144-
vaccine="Pfizer",
145-
label="prioritise_elderly",
146-
subtarget=self.vaccinate_by_age,
147-
days=list(range(7, pars_dict["n_days"])),
148-
)
149-
else:
150-
logger.info("Using standard vaccination protocol")
151-
vaccine = cv.vaccinate_prob(vaccine="Pfizer", label="regular", days=list(range(7, pars_dict["n_days"])))
152-
153-
pars_dict["interventions"] = vaccine
154-
pars_dict["use_waning"] = True # Must be set to true for vaccination
155-
sim_results_df = self.run_sim_with_pars(
156-
pars_dict=pars_dict,
157-
desired_outputs=[
158-
"cum_infections",
159-
"cum_deaths",
160-
"cum_recoveries",
161-
"cum_vaccinations",
162-
"cum_vaccinated",
163-
],
164-
n_runs=1,
165-
)
166-
167-
sim_results_df["interventions"] = vaccine.label # Store label in results instead of vaccine object
168-
sim_results_df["target_elderly"] = target_elderly
169-
sim_results_df["vaccine"] = int(target_elderly) # 0 if standard vaccine, 1 if target elderly vaccine
170-
sim_results_df["max_doses"] = vaccine.p["doses"] # Get max doses for the vaccine
171-
simulations_results_dfs.append(sim_results_df)
172-
173-
# Create a single dataframe containing a row for every execution
174-
obs_df = pd.concat(simulations_results_dfs, ignore_index=True)
175-
obs_df.rename(columns={"interventions": "vaccine_type"}, inplace=True)
176-
return obs_df
177-
178-
@staticmethod
179-
def run_sim_with_pars(pars_dict: dict, desired_outputs: [str], n_runs: int = 1, verbose: int = -1):
180-
"""Runs a Covasim COVID-19 simulation with a given dict of parameters and collects the desired outputs,
181-
which are given as a list of output names.
182-
183-
:param pars_dict: A dictionary containing the parameters and their values for the run.
184-
:param desired_outputs: A list of outputs which should be collected.
185-
:param n_runs: Number of times to run the simulation with a different seed.
186-
:param verbose: Covasim verbose setting (0 for no output, 1 for output).
187-
188-
:return results_df: A pandas df containing the results for each run
189-
"""
190-
results_dict = {k: [] for k in list(pars_dict.keys()) + desired_outputs + ["rand_seed"]}
191-
for _ in range(n_runs):
192-
# For every run, generate and use a new a random seed.
193-
# This is to avoid using Covasim's sequential random seeds.
194-
random.seed()
195-
rand_seed = random.randint(0, 10000)
196-
pars_dict["rand_seed"] = rand_seed
197-
logger.info("Rand Seed: %s", rand_seed)
198-
sim = cv.Sim(pars=pars_dict)
199-
m_sim = cv.MultiSim(sim)
200-
m_sim.run(n_runs=1, verbose=False, n_cpus=1)
201-
202-
for run in m_sim.sims:
203-
results = run.results
204-
# Append inputs to results
205-
for param in pars_dict.keys():
206-
results_dict[param].append(run.pars[param])
207-
208-
# Append outputs to results
209-
for output in desired_outputs:
210-
if output not in results:
211-
raise IndexError(f"{output} is not in the Covasim outputs. Are you using v3.0.7?")
212-
results_dict[output].append(
213-
results[output][-1]
214-
) # Append the final recorded value for each variable
215-
216-
# Any parameters without results are assigned np.nan for each execution
217-
for param, results in results_dict.items():
218-
if not results:
219-
results_dict[param] = [np.nan] * len(results_dict["rand_seed"])
220-
return pd.DataFrame(results_dict)
221-
222-
@staticmethod
223-
def vaccinate_by_age(simulation):
224-
"""A custom method to prioritise vaccination of the elderly. This method is taken from Covasim Tutorial 5:
225-
https://github.com/InstituteforDiseaseModeling/covasim/blob/7bdf2ddf743f8798fcada28a61a03135d106f2ee/
226-
examples/t05_vaccine_subtargeting.py
227-
228-
:param simulation: A covasim simulation for which the elderly will be prioritised for vaccination.
229-
:return output: A dictionary mapping individuals to vaccine probabilities.
230-
"""
231-
young = cv.true(simulation.people.age < 50)
232-
middle = cv.true((simulation.people.age >= 50) * (simulation.people.age < 75))
233-
old = cv.true(simulation.people.age > 75)
234-
inds = simulation.people.uid
235-
vals = np.ones(len(simulation.people))
236-
vals[young] = 0.1
237-
vals[middle] = 0.5
238-
vals[old] = 0.9
239-
output = dict(inds=inds, vals=vals)
240-
return output
109+
if __name__ == "__main__":
241110

111+
test_results = setup_test_case(verbose=True)
242112

243-
if __name__ == "__main__":
244-
test_results = test_experimental_vaccinate_elderly(runs_per_test_per_config=30, verbose=True)
245-
logging.info("%s", test_results)
113+
logging.info("%s", test_results)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
pop_size,pop_type,pop_infected,n_days,vaccine_type,use_waning,rand_seed,cum_infections,cum_deaths,cum_recoveries,cum_vaccinations,cum_vaccinated,target_elderly,vaccine,max_doses
2+
50000,hybrid,1000,50,pfizer,True,1169,6277.0,15.0,6175.0,629466.0,530715.0,True,1,2
3+
50000,hybrid,1000,50,pfizer,True,8888,6381.0,18.0,6274.0,630796.0,532010.0,True,1,2
4+
50000,hybrid,1000,50,pfizer,True,370,6738.0,15.0,6621.0,631705.0,532864.0,True,1,2
5+
50000,hybrid,1000,50,pfizer,True,9981,6784.0,18.0,6682.0,634582.0,535795.0,True,1,2
6+
50000,hybrid,1000,50,pfizer,True,6305,6757.0,20.0,6659.0,631292.0,532464.0,True,1,2
7+
50000,hybrid,1000,50,pfizer,True,1993,5844.0,17.0,5755.0,633314.0,534478.0,True,1,2
8+
50000,hybrid,1000,50,pfizer,True,1938,6465.0,19.0,6353.0,627724.0,528993.0,True,1,2
9+
50000,hybrid,1000,50,pfizer,True,4797,7044.0,15.0,6919.0,631246.0,532433.0,True,1,2
10+
50000,hybrid,1000,50,pfizer,True,2308,6878.0,6.0,6801.0,628865.0,530038.0,True,1,2
11+
50000,hybrid,1000,50,pfizer,True,4420,6429.0,11.0,6348.0,633803.0,535030.0,True,1,2
12+
50000,hybrid,1000,50,pfizer,True,2314,6566.0,15.0,6477.0,629288.0,530550.0,True,1,2
13+
50000,hybrid,1000,50,pfizer,True,7813,6913.0,17.0,6818.0,629290.0,530512.0,True,1,2
14+
50000,hybrid,1000,50,pfizer,True,1050,6963.0,14.0,6860.0,627981.0,529212.0,True,1,2
15+
50000,hybrid,1000,50,pfizer,True,3215,6671.0,17.0,6577.0,628802.0,530038.0,True,1,2
16+
50000,hybrid,1000,50,pfizer,True,2286,6597.0,13.0,6505.0,628986.0,530195.0,True,1,2
17+
50000,hybrid,1000,50,pfizer,True,3080,6926.0,16.0,6834.0,633636.0,534904.0,True,1,2
18+
50000,hybrid,1000,50,pfizer,True,7405,6438.0,15.0,6347.0,630353.0,531540.0,True,1,2
19+
50000,hybrid,1000,50,pfizer,True,9668,6577.0,15.0,6485.0,631257.0,532409.0,True,1,2
20+
50000,hybrid,1000,50,pfizer,True,8211,6197.0,13.0,6103.0,633827.0,535056.0,True,1,2
21+
50000,hybrid,1000,50,pfizer,True,4686,6761.0,16.0,6653.0,630557.0,531737.0,True,1,2
22+
50000,hybrid,1000,50,pfizer,True,3591,7328.0,24.0,7214.0,629949.0,531124.0,True,1,2
23+
50000,hybrid,1000,50,pfizer,True,4834,6617.0,22.0,6512.0,632609.0,533705.0,True,1,2
24+
50000,hybrid,1000,50,pfizer,True,6142,7017.0,17.0,6902.0,635965.0,537252.0,True,1,2
25+
50000,hybrid,1000,50,pfizer,True,6877,6845.0,15.0,6753.0,635678.0,536925.0,True,1,2
26+
50000,hybrid,1000,50,pfizer,True,1878,6480.0,20.0,6390.0,630807.0,531999.0,True,1,2
27+
50000,hybrid,1000,50,pfizer,True,3761,6972.0,16.0,6890.0,631100.0,532329.0,True,1,2
28+
50000,hybrid,1000,50,pfizer,True,1741,6581.0,20.0,6491.0,632835.0,534088.0,True,1,2
29+
50000,hybrid,1000,50,pfizer,True,5592,6561.0,19.0,6461.0,636799.0,537959.0,True,1,2
30+
50000,hybrid,1000,50,pfizer,True,7979,7075.0,17.0,6966.0,632902.0,534140.0,True,1,2
31+
50000,hybrid,1000,50,pfizer,True,71,6291.0,13.0,6203.0,631694.0,532901.0,True,1,2

0 commit comments

Comments
 (0)