Run a population model

You may want to split the population into multiple cohorts, each with different parameters. The cohort module offers tools to do so. This tutorial will show to work with this module.

Purpose of the cohort module

Consider the model from the Limits of Universes tutorial in which half of the population is born in 1995 and dies at the age of 80 and the other half is born in 1996 and dies at 78. In that tutorial, we saw that we cannot use two different Universes in the same Model to simulate these cohorts.

Alternatively, we could simply create and run two models.

 1from miscore import Model, Universe
 2from miscore.processes import Birth, OC
 3
 4birth1995 = Birth(year=1995)
 5oc1995 = OC(age=80)
 6universe1995 = Universe(name="universe", processes=[birth1995, oc1995])
 7model1995 = Model(universes=[universe1995])
 8result1995 = model1995.run(n=1000, seed=123)
 9
10birth1996 = Birth(year=1996)
11oc1996 = OC(age=78)
12universe1996 = Universe(name="universe", processes=[birth1996, oc1996])
13model1996 = Model(universes=[universe1996])
14result1996 = model1996.run(n=1000, seed=456)

Now, we’d have two Result objects. This is inconvenient, especially when working with a lot of cohorts. Also, for reproducibility purposes, we’ll need to set a different seed for each run. Furthermore, as the cohorts can be run independently from each other, it would be interesting to distribute the work over multiple CPU cores.

The cohort takes care of all these problems: it aggregates the result, allows for passing a single seed and manages the multiprocessing.

Minimal working example

Now let’s build the same model using the cohort module. We create multiple Cohort which all contain one or more universes. The cohorts are then added to a PopulationModel. Finally, we specify the size ‘n’ for each of the cohorts.

 1from miscore import Universe
 2from miscore.processes import Birth, OC
 3from miscore.tools.cohort import Cohort, PopulationModel
 4
 5birth1995 = Birth(year=1995)
 6oc1995 = OC(age=80)
 7universe1995 = Universe(name="universe", processes=[birth1995, oc1995])
 8cohort1995 = Cohort(name="cohort1995", universes=[universe1995])
 9
10birth1996 = Birth(year=1996)
11oc1996 = OC(age=81)
12universe1996 = Universe(name="universe", processes=[birth1996, oc1996])
13cohort1996 = Cohort(name="cohort1996", universes=[universe1996])
14
15model = PopulationModel(cohorts=[cohort1995, cohort1996])
16
17result = model.run(
18    n={
19        "cohort1995": 1000,
20        "cohort1996": 1000
21    },
22    seed=123
23)

Working with more cohorts

When working with a large number of cohorts, it is better to generate the cohorts automatically. So, let’s write a function create_cohort() that creates the cohorts for us. The cohorts_input variable contains the parameters that vary across cohorts and can be extended to contain many more entries. In practice, you might want to read this data from e.g. a CSV file.

 1from miscore import Universe
 2from miscore.processes import Birth, OC
 3from miscore.tools.cohort import Cohort, PopulationModel
 4
 5
 6def create_cohort(name, birth_year, oc_age):
 7    birth = Birth(year=birth_year)
 8    oc = OC(age=oc_age)
 9    universe = Universe(name="universe", processes=[birth, oc])
10    return Cohort(name=name, universes=[universe])
11
12
13cohorts_input = [
14    ("cohort1995", 1995, 80, 1000),
15    ("cohort1996", 1996, 81, 1000)
16]
17
18cohorts = [create_cohort(*x[:3]) for x in cohorts_input]
19
20model = PopulationModel(cohorts=cohorts)
21
22result = model.run(
23    n={x[0]: x[3] for x in cohorts_input},
24    seed=123
25)

Note

Here, x[:3] selects the first three elements of x. The * symbol then ‘unpacks’ these three elements and passes them separately to create_cohort(). Thus, create_cohort(*x[:3]) is equivalent to create_cohort(x[0], x[1], x[2]).

Screening

It’s also possible to apply screening in a PopulationModel. You can do so by adding more than one Universe to each cohort. In the following example, we add a universe with and without screening to each Cohort.

 1from miscore import processes, Universe
 2from miscore.processes import Birth, EC, EC_screening, OC
 3from miscore.tools.cohort import Cohort, PopulationModel
 4
 5
 6def create_cohort(name, birth_year, oc_age):
 7    birth = Birth(year=birth_year)
 8    oc = OC(age=oc_age)
 9
10    ec = EC.from_data(processes.ec.data.us)
11    ec_screening = EC_screening.from_data(processes.ec_screening.data.example)
12
13    no_screening = Universe(name="no_screening", processes=[birth, oc, ec])
14    screening = Universe(name="screening", processes=[birth, oc, ec, ec_screening])
15
16    return Cohort(name=name, universes=[no_screening, screening])
17
18
19cohorts_input = [
20    ("cohort1995", 1995, 80, 1000),
21    ("cohort1996", 1996, 81, 1000)
22]
23
24cohorts = [create_cohort(*x[:3]) for x in cohorts_input]
25
26model = PopulationModel(cohorts=cohorts)
27
28result = model.run(
29    n={x[0]: x[3] for x in cohorts_input},
30    seed=123
31)

Multiprocessing

The run() also has a cores argument (see Multiprocessing). You can therefore specify cores="all". If you’re using the cohort module to construct and run models with different cohorts, this is most likely the best way to efficiently run your simulation.

 1from miscore import processes, Universe
 2from miscore.processes import Birth, EC, EC_screening, OC
 3from miscore.tools.cohort import Cohort, PopulationModel
 4
 5
 6def create_cohort(name, birth_year, oc_age):
 7    birth = Birth(year=birth_year)
 8    oc = OC(age=oc_age)
 9
10    ec = EC.from_data(processes.ec.data.us)
11    ec_screening = EC_screening.from_data(processes.ec_screening.data.example)
12
13    no_screening = Universe(name="no_screening", processes=[birth, oc, ec])
14    screening = Universe(name="screening", processes=[birth, oc, ec, ec_screening])
15
16    return Cohort(name=name, universes=[no_screening, screening])
17
18
19if __name__ == "__main__":
20    cohorts_input = [
21        ("cohort1995", 1995, 80, 1000),
22        ("cohort1996", 1996, 81, 1000)
23    ]
24
25    cohorts = [create_cohort(*x[:3]) for x in cohorts_input]
26
27    model = PopulationModel(cohorts=cohorts)
28
29    result = model.run(
30        n={x[0]: x[3] for x in cohorts_input},
31        seed=123,
32        cores=4
33    )

Processing results

The Result returned by run() differs slightly from what is returned by the equivalent method of Model. All DataFrame objects in Result (i.e. properties, events, durations, individual and snapshots) have an extra index: the cohort name.

Note

Sometimes it does make sense to not use a PopulationModel but to use more than one Model, as shown in the first example of this tutorial. The main difference is whether your Result objects are merged or are kept separately. You should make your own choice what is most convenient in your case. For example, when your aim is not to compare different cohorts, but to compare different parameter sets, for example after a calibration, it might be more convenient to store the Result objects as separate files.