Skip to content

Chapter 1

This example is based on the example in the introductory chapter of Microsimulation and Population Dynamics. This is a simple example - a basic cohort model using continuous-time case-based simulation of mortality (only) for a homogeneous population, using a constant mortality hazard rate. In other words, age at death is sampled from an exponential distribution

\[ p(t)=\lambda e^{-\lambda t} \]

which has a mean, i.e. life expectancy, of \(\mu=1/\lambda\).

The neworder implementation is as direct a port of the MODGEN model, as far as possible.

Examples Source Code

The code for all the examples can be obtained by either:

  • pulling the docker image virgesmith/neworder:latest (more here), or
  • installing neworder, downloading the source code archive from the neworder releases page and extracting the examples subdirectory.

Input

Here's the code that sets up and runs the model:

"""
Chapter 1
This is a direct neworder cover version of the Basic Cohort Model from the Belanger & Sabourin book
See https://www.microsimulationandpopulationdynamics.com/
"""
import numpy as np
import neworder
from person import People

# neworder.verbose() # uncomment for detailed output

# "An arbitrarily selected value, chosen to produce a life expectancy of about 70 years."
# (see the "mortality" example for a more realistic model)
mortality_hazard = 0.014
population_size = 100000

model = People(mortality_hazard, population_size)

neworder.run(model)

# now we can sample the population generated by the model to see the proportion of deaths at (arbitrarily) 10 year intervals
for age in np.linspace(10.0, 100.0, 10):
  neworder.log("Age %.0f survival rate = %.1f%%" % (age, model.alive(age) * 100.0))
[file: ./examples/chapter1/model.py]

Implementation

Each individual is an instance of the Person class:

class Person():
  """
  MODGEN equivalent: actor Person {...}
  Represents a single individual
  """
  def __init__(self, mortality_hazard: float) -> None:
    """ MODGEN equivalent: Person::Start() """
    self.alive = True
    # MODGEN would automatically create time and age, though they are not needed for this simple example
    self.mortality_hazard = mortality_hazard
    self.time_mortality = neworder.time.NEVER # to be computed later

  def finish(self) -> None:
    """ MODGEN equivalent: Person::Finish() """
    # nothing required here

  def state(self, t: float) -> bool:
    """ Returns the person's state (alive/dead) at age t """
    return self.time_mortality > t

  def time_mortality_event(self, mc: neworder.MonteCarlo) -> float:
    """ MODGEN equivalent: TIME Person::timeMortalityEvent() """
    if neworder.time.isnever(self.time_mortality):
      self.time_mortality = mc.stopping(self.mortality_hazard, 1)[0]
    return self.time_mortality

  def mortality_event(self) -> None:
    """ MODGEN equivalent: void Person::MortalityEvent() """
    # NB this is not used in this implementation
    self.alive = False
[file: ./examples/chapter1/person.py]

And the People model contains an array of Persons

class People(neworder.Model):
  """ A model containing an aggregration of Person objects """
  def __init__(self, mortality_hazard: float, n: int) -> None:

    # initialise base model with a nondeterministic seed results will vary (slightly)
    super().__init__(neworder.NoTimeline(), neworder.MonteCarlo.nondeterministic_stream)

    # initialise population
    self.population = [Person(mortality_hazard) for _ in range(n)]
    neworder.log("created %d individuals" % n)
[file: ./examples/chapter1/person.py]

The single timestep records each person's time of death

  def step(self) -> None:
    # sample each person's age at death.
    # (this is not an efficient implementation when everyone has the same hazard rate)
    [p.time_mortality_event(self.mc) for p in self.population]
[file: ./examples/chapter1/person.py]

And when the timeline has reached the end, the finalise method compares the mean of the sampled times of death with the expected value:

  def finalise(self) -> None:
    # compute mean sampled life expectancy against theoretical
    sample_le = sum([p.time_mortality for p in self.population]) / len(self.population)
    actual_le = 1.0 / self.population[0].mortality_hazard
    error = sample_le - actual_le
    neworder.log("Life expectancy = %.2f years (sampling error=%.2f years)" % (sample_le, error))
[file: ./examples/chapter1/person.py]

Then, from the model script, this function is called, displaying the proportion of the cohort that are still alive at 10-year intervals:

  def alive(self, t: float) -> float:
    return np.mean([p.state(t) for p in self.population])
[file: ./examples/chapter1/person.py]

Output

Some example output:

[py 0/1] created 100000 individuals
[py 0/1] Life expectancy = 71.60 years (sampling error=0.17 years)
[py 0/1] Age 10 survival rate = 86.9%
[py 0/1] Age 20 survival rate = 75.6%
[py 0/1] Age 30 survival rate = 65.8%
[py 0/1] Age 40 survival rate = 57.3%
[py 0/1] Age 50 survival rate = 49.8%
[py 0/1] Age 60 survival rate = 43.3%
[py 0/1] Age 70 survival rate = 37.5%
[py 0/1] Age 80 survival rate = 32.7%
[py 0/1] Age 90 survival rate = 28.3%
[py 0/1] Age 100 survival rate = 24.7%
and clearly a constant mortality rate isn't realistic as we see far more deaths at younger ages, and far less at older ages, than would be expected. The example mortality introduces a model with a time-dependent mortality hazard rate and shows how the framework can very efficiently model this.