Chapter 7. A Step-by-Step Guide¶
This is complete guide for building DYNAMIS-POP-MRT starting from a simple model template and adding functionality at each step. For each step, the following information is provided:
- An overview what is added at this step
- New Modegn/openM++ programming concepts introduced at this step
- A step-by-step guide for what to add to complete this step, starting from the previous step
Steps 1 - 9 replicate DemProj, a typical macro population projection model, and are implemented as a so-called case-based model (i.e., a model with no interaction between simulated persons) and a starting population created by sampling from a distributional table of persons by age, sex, and province.
- 7.1. Step 1: Model Templates
- 7.2. Step 2: Creating a Population
- 7.3. Step 3: Population Scaling
- 7.4. Step 4: Fertility
- 7.5. Step 5: Refining Mortality
- 7.6. Step 6: Migration
- 7.7. Step 7: Emigration
- 7.8. Step 8: Immigration
- 7.9. Step 9: Micro-Data Output
Step 10 converts the starting population type to a micro-data file. Up to this point, models of each step can also be converted to time-based models, which can be used as templates for other applications. This is illustrated in Step 11, which creates a time-based starting template for the next phase of model development going beyond the replication of a macro model.
- 7.10. Step 10: Micro-Data Input
- 7.11. Step 11: Conversion to a Time-Based Model
Steps 12+ build an interacting population micro-simulation model step-wise, introducing new behaviors like first union formation, education, and refined models for demographic behaviors such as fertility, (infant) mortality, and migration by education.
- 7.12. Step 12: Sampling from a detailed starting population
- 7.13. Step 13: Primary Education
- 7.14. Step 14: First Union Formation
- 7.15. Step 15: Fertility refined
- 7.16. Step 16: Infant mortality
- 7.17. Step 17: Migration refined
- 7.18. Step 18: Extending table output
7.1. Step 1: Model Templates¶
7.1.1. Overview¶
Model Templates¶
DYNAMIS-POP-MRT model development starts from a generic model template for continuous-time models. The model creates a cohort of people all born at age 0 and subject to age-specific mortality. Mortality is the only stochastic event at this step. Accordingly, the model has only one parameter: mortality rates by age. The population size is part of the scenario settings and can be chosen by the user, too. The model output consists of a set of tables, which have mostly demonstrative use. They contain demographic measures like life expectancy, and, for model validation, age-specific mortality rates calculated from the simulated cohort of people.
The template contains the following mpp files, which contain the following model codes:
- DYNAMIS-POP-MRT.mpp The simulation engine which handles the simulation run
- ModelSettings.mpp A file with settings specific to the model version
- PersonCore.mpp A core file of the actor Person
- Mortality.mpp Mortality module
- Tables.mpp The module containing all output tables
The template also contains the data file:
- Base(PersonCore).dat File containing all parameters common to all versions at this step
- Base(ModelSettings).dat File containing all version-specific parameters of this modeling step
Model Versions¶
This step-by step modeling guide and documentation cover four model types and allow conversion between types at any step until Step 8, which completes the model’s base version, the replication of a macro model. Besides the introduction of type-specific concepts, this supports the use of the model steps as templates for other models.
- Case-based versus time-based models: Time-based models allow for interactions between all persons. While no interactions are required in the base version of DYNAMIS-POP-MRT, they are important in the following model extensions. The DYNAMIS-POP-MRT.mpp file, i.e., the simulation engine, is the only file specific to this model distinction; all other modules except ModelSettings are generic. All case-based models can be converted to time-based models by replacing the simulation engine and modifying a few settings in ModelSettings.mpp.
- Sampled versus file-based populations: The base version of DYNAMIS-POP-MRT projects a population by age, sex, and province, which is the only information required for the starting point of the simulation. This information can be contained in a distributional table from which the model samples; alternatively, information can be contained in a micro-data file. The first approach works only for a very small number of variables and all variables must be categorical, a limitation that can be overcome by representing the population in a file. This will be important when extending the model beyond its base version. While building the base version, the distributional table can be replaced by a file at any modeling step, starting with Step 1, by replacing the starting population file and changing a few settings in ModelSettings.mpp. This will be documented in the next step of model building.
The most efficient model type for the base version of DYNAMIS-POP-MRT is a case-based model that samples characteristics from a table. Time-based models starting from a micro-data file are the typical approach for most micro-simulation models.
7.1.2. Concepts¶
Overview¶
The code of this model template gives a first overview of main Modgen programming concepts. At this step we cover the following topics:
- Modular code organization
- Code organization within modules
- Modgen types
- Creating parameter tables
- States
- Events
- Random numbers
- Functions Start() and Finish()
Code organization in modules¶
Modgen model code is organized into .mpp files. While in theory, all code could be put into the .mpp file with the name of the application (in our case DYNAMIS-POP-MRT.mpp), a modular organization is advised. Typically, the following files can be found:
- The simulation engine: DYNAMIS-POP-MRT.mpp. This file contains core simulation functions and definitions. When starting from a template or using the Modgen wizard, developers typically do not have to modify or understand this file code. The file contains the definition of the model type; in our case, it’s a case-based continuous-time model. The second part consists of two functions that handle the simulation, one to process a single case, the second to loop through all cases.
- Actor Core Files: Each actor typically has a core file. In our case, we have a single actor type Person, thus one core file PersonCore.mpp. Core modules contain the basic information that defines the corresponding actor, and the functions Start() and Finish() for initializing actor states at creation and deleting the actor at death. This file is also a good place for insert code that does not belong to specific behaviors or is used by various other modules.
- Specific modules: These are files for specific behaviors or life course domains (like in our case Mortality.mpp) or functionality. Developers typically spend the most time on this module type. Also, building up a model typically consists in adding modules of this type, as will be the case here, when we add Fertility.mpp, Migration.mpp, etc.
- Table module(s): This module(s) contain the tables of the model. Tables can be placed anywhere in the code, and/or spread to various files. In simple models, putting all tables together, e.g., into Tables.mpp, is a good way to organize code.
Code organization within modules¶
Most modules follow this organization:
- Documentation
- Type definitions
- Parameters
- Definitions of actor states, functions, and events
- Implementation of functions and events
Documentation:¶
Labels and notes for modules (as well as any symbols used in the program) are used for the automatically generated model help file for users. Good documentation, therefore, is not only a best practice for model development, but also creates detailed documentation for the user.
- A label: a one-line description of the module
- A note: a more detailed description of the module
Example (from Mortality.mpp):
//LABEL(Mortality, EN) Mortality Module
/* NOTE(Mortality, EN)
This module implements a simple model of mortality.
People can die at any moment of time based on age-specific mortality rates.
The only state of this module is 'alive' which is set to 'false' at death.
Also at death, the Modgen function Finish() is called which clears up memory space.
*/
Placed above or in the same line of a newly introduced symbol, notes can also be written as:
//EN The text of the note
You encounter such notes throughout the code. These notes are introduced by a language code (EN for English in our case). Modgen also supports multilingual applications, in which these labels are translated into (an)other language(s). Besides code documentation, these notes are used in the user interface.
Type definitions¶
Modgen types are typically used as types of states, and dimensions for parameter and output tables. This step introduces the type range.
Example (from PersonCore.mpp): AGE_RANGE defines a range of of valid integer numbers for age, allowing a maximum age of 100 years.
range AGE_RANGE { 0, 100 }; //EN Age range
Other important Modgen types are classification for categorical variables and partition to divide continuous variables like time or age into intervals.
Examples (introduced at later steps):
classification SEX //EN Sex
{
FEMALE, //EN Female
MALE //EN Male
};
partition AGE5_PART { 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 }; //EN Age Groups
Parameters¶
Parameters have a type and can have any number of dimensions. Each parameter corresponds to a table in the user interface. Tables can be as simple as a single on/off checked box (the type of such a parameter being “logical”) or consist of multidimensional tables, like tables by age, sex, region, etc.
The most important Modgen types
- logical true/false 0/1
- integer integer numbers
- double floating point numbers
- TIME in continuous time models equivalent with double
- SEX a Modgen classification declared in the code
- AGE_RANGE a Modgen range declared in the code
At this step, we define a single parameter table MortalityTable for mortality, contained in the Mortality.mpp module. The parameter is of type double (a precise floating point number) and has the dimension AGE_RANGE.
Parameters are organized in parameters code blocks, which can be placed in multiple files for introducing parameters in the modules in which they are used.
Example (from Mortality.mpp):
parameters
{
double MortalityTable[AGE_RANGE]; //EN Mortality by age
};
Parameters can be grouped into parameter_group for to better organize the user interface. This is optional and the same parameter can be placed into multiple groups, allowing for example a group of “most important” parameters. Groups can also be nested.
Example (from Mortality.mpp):
parameter_group PG01_Mortality //EN Mortality Parameters
{
MortalityTable
};
Declarations of actor states, functions, and events¶
Definitions of actor states are placed into actor ActorName {}; code blocks, which can be placed into multiple files for declaring states and events in the module they belong to.
Example (from Mortality.mpp):
actor Person
{
// States
logical alive = { TRUE }; //EN Alive
// Events
event timeMortalityEvent, MortalityEvent; //EN Mortality event
};
States are the variables that describe an actor. Like parameters, states have a type. For example, the state “alive” is of type logical, and initialized with { TRUE }. Note that all initializations with values have to be put into curly brackets {}. Such states are called simple states; the values of simple states are changed in Events. States can also be initialized by formulas containing other states, called derived states, in which case (like Formulas in Excel cells) the states are updated automatically. If derived states contain a “self-scheduling” function (see below), they create their own update events and are called self-scheduling states.
Two states are automatically created and maintained by Modgen: age and time. These two states can be set only within the Start() function (see below).
Currently the model has only two other states: the time at birth and integer_age:
actor Person
{
// States
TIME time_at_birth = { TIME_INFINITE }; //EN Time at birth
AGE_RANGE integer_age = COERCE(AGE_RANGE, self_scheduling_int(age)); //EN Age in full years
...
The time_at_birth is recorded at the start of the actors’ life (currently 0 for all actors) and initialized with the Modgen constant TIME_INFINITE—a very distant future never to happen.
The state integer_age is frequently used as table index (e.g., for getting the current mortality) and as table dimension. It is a self-scheduling state of type AGE_RANGE (declared above) using two Modgen functions.
- COERCE() forces an integer number into a range.
- self_scheduling_int(age) creates an integer value based on age, automatically updating itself at each birthday. This is a very powerful Modgen concept, sparing us the manual programming of a birthday event for updating the integer_age variable.
Events: At this step, the only ‘manually programmed’ event is MortalityEvent, which, at death, changes the state alive from TRUE to FALSE. Events have two functions: a time function (when does the event happen?) and the function called when the event happens.
Implementation of events¶
After being declared in an actor code block, events have to be implemented. The time function returns a value of type TIME and is thus of type TIME. The event function does not return a value and is thus—this is C++ notation—void.
The syntax of C++ functions is:
ReturnValueType ObjectName::FunctionName(ParametersIfAny)
{
code;
return ReturnValue; (if function is not of type 'void')
}
Example (from Mortality.mpp):
TIME Person::timeMortalityEvent()
{
TIME dEventTime = TIME_INFINITE;
// check if a person is at risk
if ( MortalityTable[integer_age] > 0.0 )
{
// determine the event time
// the formula [ -log(rand) / hazard ] calculates an exponentially distributed waiting time
// based on a uniform distributed random number and a given hazard rate
dEventTime = WAIT(-log(RandUniform(1)) / MortalityTable[integer_age]);
}
// return the event time, if the maximum age is not reached at that point
if (dEventTime < time_at_birth + MAX(AGE_RANGE) + 1.0) return dEventTime;
// otherwise, return the moment, at which the maximum age is reached
else return time_at_birth + MAX(AGE_RANGE) + 0.9999;
}
void Person::MortalityEvent()
{
alive = FALSE;
Finish(); // Remove the actor from the simulation.
}
The time function of the example is a typical time function for scheduling events in continouse time. It first checks if a person is at risk of the event (here, if the hazard rate is not 0.0; but some events will only affect certain people, like those of specific age or in specific cirumstances). If a person is at risk, a waiting time is calculated. For a given waiting time, the event can be scheduled. (The function WAIT(WaitingTime) in the code example just adds the current time to the waiting time). Modgen time functions update themselves whenever something happens that affects the waiting time. In our example, this is the case if integer_age changes, as at each birthday, a new age-specific hazard rate applies. Thus, the two competing events of our model are birthdays and death. If a person survives until the next birthday, the scheduled death event is automatically removed from the event queue and a new waiting time is determined, until death wins the race.
Uniform distributed random numbers¶
Modgen uses the RandUniform(n) function for obtaining a uniform distributed random number between 0 and 1. The parameter (n) indicates the random number stream. Developers can leave the parentheses empty and type RandUniform(), and Modgen will automatically add a number. Modgen uses a different random number stream for each random function. This ensures that when adding a new random process, the sequence of random numbers of other processes stays untouched. This way, differences in results can be attributed to the added process only, and not to monte carlo disturbances by altering the sequence of random numbers. The root of all random number streams is set by the user in the simulation settings. Unless changed, re-running the same simulation will produce exactly the same results.
Special Functions: Start() and Finish()¶
The module PersonCore.mpp contains two functions, Start() and Finish(), which are part of each Modgen model and exist for each actor type separately. The functions can take parameters. In our example, Start() has two parameters, a record number and a link to a person. The latter allow inherited characteristics from mother to child. If not used, the function can be called with NULL as argument:
actor Person
{
...
void Start(long PersNr, Person *prPerson); //EN Starts the actor
void Finish(); //EN Finishes the actor
};
The Start() function is called when an actor is created. It is the only point in the code, where the state’s time and age can be set and used to initiate states with values.
void Person::Start(long PersNr, Person *prPerson)
{
// Modgen initializes all actor variables before the code in this function is executed.
// The Start() function is the only place where the Modgen states age and time can be set.
// The states age and time are then automatically maintained by Modgen
age = 0;
time = 0;
time_at_birth = time;
}
The function Finish() has to be called when an actor is deleted in the death event. It is the place for “last-minute actions.” In our example, the function is empty but will still perform automatic routines, like cleaning up the memory space. Typical code examples are writing records containing selected states and “memories” of an actor, or distributing inheritances to linked persons.
void Person::Finish()
{
// Developers can add code here, currently no additional routines are required.
// After the code in this function is executed, Modgen automatically removes the actor from the
// simulation and recuperates any memory used by the actor.
}
Tables¶
Modgen has a very powerful tabeling language. At this point, some key functions and the basic syntax are introduced. The best way to learn about tables is by example and by doing. The basic table syntax is:
table ActorType TableName //EN Table Label
[ optional filters selecting specific actors or specific moments in time]
{
OptionalTableDimensionA * //EN Dimension A label
...
{
FirstTableFormulaA, //EN First label decimals=n
...,
LastTableFormula //EN Last label decimals=n
} //EN Label for collection of formulas
* OptionalTableDimensionX //EN Dimension X label
...
}
Example 1 (from Tables.mpp):
table Person DurationOfLife //EN Duration of Life
{
{
unit, //EN Number of persons ever entering the simulation
duration(), //EN The total time lived by all actors together
min_value_out(duration()), //EN Minimum duration of life decimals=4
max_value_out(duration()), //EN Maximum duration of life decimals=4
duration() / unit, //EN Life expectancy decimals=4
//EN Life expectancy decimals=4
value_at_transitions(alive, TRUE, FALSE, age) / transitions(alive, TRUE, FALSE)
} //EN Demographic characteristics
};
This table has no filters and no dimensions, so everybody is recorded over the whole life. Note that the default number of decimals can be set as part of the value labels. Users can change the view of tables, including decimal paces, the ordering of dimensions.
Used table functions and keywords are:
- unit a counter: everybody entering the table (cell)
- duration() Time lived while in the table accumulated over all actors
- min_value_out(x) Minimum value of x in the population when leaving the table
- max_value_out(x) Maximum value of x in the population when leaving the table
- value_at_transitions(x, a, b, y) Accumulated values of y when x changes from state a to b
- transitions(x, a, b) Number of transitions of stete a from level a to b
Example 2 (from Tables.mpp):
This table introduces a filter that triggers a specific moment of time: the time of death. Thus, actors enter this table at death, and disappear immediately after. Such tables are used to produce cross-sectional output at specific points in time or at specific events. Note that functions like duration() will not work and would just return 0 as the duration, at which the condition that holds true is of zero length.
table Person StatisticsAtDeath //EN Statistics at death
[ trigger_transitions(alive, TRUE, FALSE) ]
{
{
min_value_in(age), //EN Minimum duration of life decimals=4
max_value_in(age), //EN Maximum duration of life decimals=4
value_in(age) / unit //EN Life expectancy decimals=4
} //EN Demographic characteristics
};
Used Modgen functions are:
- trigger_transitions(x, a, b) True at the moment the value of state x changes from a to b
- value_in(x) The value of state x when entering the table
Example 3 (from Mortality.mpp)
In this example, a filter is set for a specific age, 80. The filter is true as long as a person is at age 80, thus records a period of time and not a single snapshot, as in the previous example. As a consequence, value_in(x) and value_out(x) of a state can differ, as x might change while a person meets the condition of being age 80.
table Person StatisticsAtAge80 //EN Statistics at age 80
[ integer_age == 80 ]
{
{
unit, //EN Number of persons entering age 80
value_in(alive), //EN Number of persons entering age 80
value_out(alive), //EN Number of persons surviving age 80
transitions(alive, TRUE, FALSE) //EN Number of persons dying at age 80
} //EN Demographic characteristics
};
Example 4 (from Mortality.mpp)
In this example, we add a table dimension, integer_age, and produce a table by age. All formulas are calculated for each age. In our case, we used this for producing a validation table of simulated mortality rates by age, which can be compared to the input parameter.
table Person DeathRates //EN Death rates and probabilities
{
integer_age *
{
transitions(alive, TRUE, FALSE), //EN Number of persons dying at this age
transitions(alive, TRUE, FALSE) / duration(), //EN Death rate decimals=4
transitions(alive, TRUE, FALSE) / unit //EN Death probability decimals=4
} //EN Demographic characteristics
};
7.1.3. How to reproduce this modeling step¶
This is a ready-made template and nothing has to be added at this step.
Useful Visual Studio settings:¶
Syntax Highlighting¶
To switch on syntax highlighting for Modgen files (.mpp code and .dat data files):
- Go to: TOOLS / Options… / Text Editor / File Extensions
- Choose Editor / Microsoft Visual C++
- Add the following extensions (type into box and click add): mpp, dat
Switching off the squiggles – spell checker¶
By default, the Visual Studio underlines misspelled words with squiggles. This can be annoying in a programming language. To switch squiggles off:
- Go to: TOOLS / Options… / Text Editor / C++ / Advanced
- In the IntelliSense list, put ‘Disable Squiggles TRUE’
7.1.4. Model versions¶
Converting the model to a template for time-based models¶
As at any modeling step of this guide, the case-based model can be converted to a time-based model by replacing the DYNAMIS-POP-MRT.mpp module, changing some settings in ModelSettings.mpp, and adapting the model parameters in the data files.
- Replace the module DYNAMIS-POP-MRT.mpp file by the time-based version of the file (see Appendix)
- Adapt the model settings in ModelSettings.mpp
- Adapt the data file containing the parameters of ModelSettings.mpp
Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
The module ModelSettings.mpp in a time-based version at Step 0¶
The essential code of this module consists in the declaration of one single parameter for the population sample size, as the scenario settings for time-based models do not contain such a parameter.
parameters
{
long TotalPopSampleSize; //EN Simulated sample size
};
7.2. Step 2: Creating a Population¶
7.2.1. Overview¶
This step adds a module StartPopSampling.mpp to the model, which is used to sample the three characteristics of people typically found in macro population projection models: age, sex, and region. With this step we move from a cohort model, where all were born at the same time, to a population model, creating a population with characteristics as observed today.
Sampling from a distributional table allows the user to select any sample size; large samples will reduce monte carlo variation and results convert to those obtained from a cohort-component macro model. This approach works as long as two central limitations of cell-based models is satisfied: the number of characteristics is small and all characteristics are categorical, as the population has to be split into cells. In micro-simulation, this can be overcome by replacing a distributional table by a population micro-data file, an approach to which we will convert at a later point when extending the model.
While converting the model to a population model is straightforward, there are some issues to consider:
- As the distributional table reflects the population as observed today, when creating people in the past (starting their simulation at age 0), we have to disable mortality in the past, as today we only observe survivors. The alternative option would be to create the population as of today, thus people being born with their current age. We opted for the first option to create the starting population and will demonstrate the second option later for immigrants.
- Existing tables have to be carefully revised. For example, life expectancy, as currently calculated, has little meaning, as it now reflects the life expectancy calculated from people who survived until today.
- While most of the code can be added within the additional module StartPopSampling.mpp, we must make modifications to other modules, too. Besides Mortality.mpp, we also modify PersonCore.mpp: At Start() we call the sampling function to obtain initial characteristics. Also, StartPopSampling.mpp is a good place to put some of the new type definitions and states, such as sex and region, as they are expected to be used in many other modules.
- We have two design options for ending a simulation: let everybody live until death, or set an end time when the simulation will be terminated and all actors killed. At this step we opt for the first option, setting the time horizon to 2113; as the youngest people in the starting population are born in 2012, we allow them to reach the maximum age of 100.9999 years.
7.2.2. Concepts¶
Overview¶
At this step, we introduce two key Modgen concepts and functionality:
- Adding a new module
- Sampling from a distributional table
Adding a new module¶
Adding a new module:
- Right-click Modules(mpp) in the solution explorer
- Select add / new item / Modgen
- Select Modgen12EmptyModuleVide
- Give the module a name
A new empty module of the chosen name appears in the list of mpp files in the solution explorer.
Sampling from a distributional parameter table¶
To allow sampling from a table, Modgen uses the parameter type cumrate [n]. A cumrate parameter is of value type double and allows sampling using a function Lookup_TableName(). For each cumrate parameter, such a function is available automatically. The [n] indicates the number of dimensions sampled; when smaller than the number of total dimensions, the last n dimensions are sampled. Cumrate parameters are declared like other parameter tables.
Example (From StartPopSampling.mpp)
parameters
{
cumrate[3] StartingPopulationTable[SEX][AGE_RANGE][PROVINCE_NAT]; //EN Starting population
};
For sampling from a cumrate parameter table, the Lookup_TableName function is used. This is a C++ type function following some C++ syntax. The function takes a random number as a parameter, and integer variables for the table dimensions. The last [n] - n is set when declaring the parameter—characteristics are sampled, the local integer variables taking up the sampled values are introduced with the value sign &. In our case, all three dimensions are sampled.
As the Lookup function returns integer values for the table position, the sampled characteristics typically have to be cast in the types of the corresponding states. For example ‘(SEX)nNumber’ converts the integer nNumber into a category of the classification SEX. (Levels of classifications correspond to integers starting at 0.)
void Person::GetStartCharacteristics()
{
int nAge, nSex, nProv;
Lookup_StartingPopulationTable(RandUniform(2), &nSex, &nAge, &nProv);
integer_age = nAge // Age in full years
sex = (SEX)nSex; // Sex
province_nat = (PROVINCE_NAT)nProv; // Province of residence
}
If we sample age and province for given sex, the code would change as follows:
cumrate[2] DistributionTable[SEX][AGE_RANGE][PROVINCE_NAT]; //EN Distribution table
Lookup_DistributionTable(RandUniform(2), (int)sex, &nAge, &nProv);
7.2.3. How to reproduce this modeling step¶
Adding new types in PersonCore.mpp¶
Besides age, which was the only characteristic people had before, actors will get three more characteristics, which we will add in the PersonCore.mpp module: sex, year of birth, and region of residence. For that we define two classifications (one for sex, one for province) and a range for projected years (allowing the simulation to run from 2013 to 2113) and all years (1912-2113).
classification SEX //EN Sex
{
FEMALE, //EN Female
MALE //EN Male
};
classification PROVINCE_NAT //EN Province
{
PN_PROV00, //EN Hodh-Charghy
PN_PROV01, //EN Hodh-Gharby
PN_PROV02, //EN Assaba
PN_PROV03, //EN Gorgol
PN_PROV04, //EN Brakna
PN_PROV05, //EN Trarza
PN_PROV06, //EN Adrar
PN_PROV07, //EN Dakhlett-Nouadibou
PN_PROV08, //EN Tagant
PN_PROV09, //EN Guidimagha
PN_PROV10, //EN Tirs-Ezemour
PN_PROV11, //EN Inchiri
PN_PROV12 //EN Nouakchott
};
range SIM_YEAR_RANGE{ 2013, 2113 }; //EN Projected calendar years
range ALL_YEAR_RANGE{ 1912, 2113 }; //EN All calendar years
New states in PersonCore.mpp¶
We add the following states:
- Year_of_birth: a derived state calculated automatically from the existing state time_of_birth by the function int(), which typecasts a double number to integer.
- Sex: a simple state initialized to { FEMALE } - the actual sex will be sampled at creation of each actor.
- Province_nat: region within the national territory (as opposed to regions including foreign). Another simple state initialized with the first province of the list: the actual province of residence will be sampled at creation of each actor.
- Calendar_year: a self-scheduling state based on time, within the range of past and projected years.
- Sim_year: a derived state based on calendar_year but forced into the range of projected years.
- In_projected_time: an indicator, in cases where the simulation has moved from the past to the projected time. This state can be used in tables, for example, to filter output to the future, allowing the state sim_year to be used as a table dimension.
actor Person
{
ALL_YEAR_RANGE year_of_birth = int(time_of_birth); //EN Year of birth
SEX sex = { FEMALE }; //EN Sex
PROVINCE_NAT province_nat = { PN_PROV00 }; //EN Province of residence
//EN Calendar year
integer calendar_year = self_scheduling_int(time);
//EN Projected year
SIM_YEAR_RANGE sim_year = COERCE(SIM_YEAR_RANGE, calendar_year);
//EN The simulation is within the projected time
logical in_projected_time = (calendar_year >= MIN(SIM_YEAR_RANGE)) ? TRUE : FALSE;
};
Create a new module StartPopSampling.mpp¶
We add a new module StartPopSampling.mpp by:
- Right-click Modules(mpp) in the solution explorer
- Select add / new item / Modgen
- Select Modgen12EmptyModuleVide
- Name it StartPopSampling
A new empty module StartPopSampling.mpp appears in the list of mpp files in the solution explorer.
Code of the new module StartPopSampling.mpp¶
Documentation
As with any other module, the module starts with a label and note for documentation:
//LABEL(StartPopSampling, EN) Starting population sampling module
/* NOTE(StartPopSampling, EN)
Added in Step 2
This module implements the sampling of characteristics of the starting population.
At the creation of each person of the starting population, the characteristics
age, sex, and region are drawn from a distributional table
*/
Parameters
The module has one single parameter: the distributional table from which the characteristics are sampled at birth of an actor. To allow sampling from a table, Modgen uses the parameter type cumrate. A cumrate parameter is of value type double and allows sampling using a function Lookup_TableName(). For each cumrate parameter, such a function is available automatically. The [n] indicates the number of dimensions sampled; when smaller than the number of total dimensions, the last n dimensions are sampled.
parameters
{
cumrate[3] StartingPopulationTable[SEX][AGE_RANGE][PROVINCE_NAT]; //EN Starting population
};
parameter_group PG01_StartPop //EN Population
{
StartingPopulationTable
};
Actor function
The actor Person block of this module contains a single function for sampling characteristics. The parameter PersonNr is not used at this step, but allows easy conversion of the model to a version that reads in a starting population file.
actor Person
{
void GetStartCharacteristics(long PersonNr); //EN Sample charcteristics, called at Start()
};
Function implementation
The Lookup_TableName function is used for sampling from the cumrate table,. It takes a random number as a parameter and integer values for the table dimensions. The last [n] - n is set when declaring the parameter, characteristics are sampled, and the local integer variables taking up the sampled values are introduced with the value sign &. In our case, all three dimensions are sampled.
As the Lookup function returns integer values for the table position, the sampled characteristics have to be casted into the types of the corresponding states. For example (SEX)nNumber converts the integer nNumber into a category of the classification SEX. (The levels of classification correspond to integers starting at 0.)
void Person::GetStartCharacteristics(long PersonNr)
{
int nAge, nSex, nProv;
// Samling of characteristics sex, age and province from the cumrate table StartingPopulationTable
// Cumrate parameters automatically provide a sampling function Lookup_ParameterName
// The lookup function returns integer values
Lookup_StartingPopulationTable(RandUniform(2), &nSex, &nAge, &nProv);
// Storing the sampled variables into the corresponding states
// Integer values can be casted to classifications by (CLASSIFICATION_NAME)nIntegerVariable
sex = (SEX)nSex; // Sex
province_nat = (PROVINCE_NAT)nProv; // Province of residence
// Time of birth at a random moment within the year
time_at_birth = MIN(SIM_YEAR_RANGE) - nAge - RandUniform(3);
}
Modify the Start() function in PersonCore.mpp¶
The Start() function now calls the GetStartCharacteristics() function and sets the state time to the time_of_birth calculated in the sampling function from the sampled current age.
void Person::Start(long PersonNr, Person *prPerson)
{
GetStartCharacteristics();
age = 0;
time = time_at_birth;
}
Modify timeMortailityEvent() in Mortality.mpp¶
While persons in the starting population are created in the past, their composition reflects today’s population, so they are survivors until the start of the simulation and mortality has to be disabled in the past. This is accomplished by adding an additional condition: the logical state in_projected_time, when assessing if a person is at risk of death.
// check if a person is at risk
if ( MortalityTable[integer_age] > 0.0 && in_projected_time )
Adding the parameter and values in the Base(PersonCore).dat file¶
The parameter has 2 x 101 x 13 = 2626 values (sex x age x province). Tables by age and province by sex can be easily produced from a micro-data file or are available from macro population projections, which take up the same parameter. Tables by sex are easy to copy and paste into the table in the graphical user interface. To assign initial values to the parameter, a (repeater) can be used:
cumrate[3] StartingPopulationTable[SEX][AGE_RANGE][PROVINCE_NAT] = { (2626) 1 };
Add and modify tables in Tables.mpp¶
Many cohort tables have limited meaning in the population model. We switched off mortality in the past, as people of the starting population are survivors.
At this step, we keep the validation table only for death rates, but add a filter [in_projected_time] to calculate rates in the projected time.
table Person DeathRates //EN Death rates and probabilities
[ in_projected_time ]
{
integer_age *
{
transitions(alive, TRUE, FALSE), //EN Number of persons dying at this age
transitions(alive, TRUE, FALSE) / duration(), //EN Death Rate decimals=4
transitions(alive, TRUE, FALSE) / unit //EN Death probability decimals=4
}
};
We add a second validation table for displaying the simulated starting population by age and province by sex. This table just counts people at the moment the simulation starts by placing the filter [trigger_entrances(in_projected_time, TRUE)].
table Person SimulatedStartingPopulation //EN Simulated starting population
[ trigger_entrances(in_projected_time, TRUE) ]
{
sex+ *
{
unit //EN Number of persons
}
* integer_age+
* province_nat+
};
As we are not able to calculate the life expectancy at birth because the starting population contains only survivors up to date, we remove all other previous tables and replace them by a table for the remaining life expectancy by age at the start of the simulation. This age can be programmed as a derived state, as it is used only for the table, and is placed in the Tables.mpp file.
actor Person
{
//EN Age at start
AGE_RANGE tab_age_start = value_at_transitions(in_projected_time, FALSE, TRUE, integer_age);
};
table Person RemainingLifeExpectancyAge //EN Remaining life expectancy by age at start
[ in_projected_time ]
{
tab_age_start *
{
duration() / unit //EN Remaining life expectancy in simulation decimals=4
}
};
7.2.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.2.5. The module ModelSettings.mpp in alternative versions of Step 1¶
Sampling - time based¶
parameters
{
long TotalPopSampleSize; //EN Simulated sample size
};
Micro-data file - case based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
void PreSimulation()
{
//Total population
TotalPopSize = StartPopSize;
TotalPopSampleSize = GetAllCases();
StartPopSampleSize = TotalPopSampleSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
//Scale Population
ScalePopulation = GetPopulationScalingRequired();
if (ScalePopulation) SetPopulation(TotalPopSize);
};
Micro-data file - time based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
};
parameter_group PG_ModelSettings //EN Model Settings
{
StartPopSize, TotalPopSampleSize, ScalePopulation
};
void PreSimulation()
{
TotalPopSize = StartPopSize;
StartPopSampleSize = TotalPopSampleSize;
};
7.3. Step 3: Population Scaling¶
7.3.1. Overview¶
This step adds automatic population scaling. The user can select both the sample size and real population size and the model output is automatically scaled to the real population size. While this feature is automatically provided by Modgen in case-based models (the population sizes and a scaling option are part of the model settings), in time-based models, parameters for scaling and scaling has to be handled in the code. In the case-based version of DYNAMIS-POP-MRT, information on total population size is already contained in the distributional table; we will introduce a routine that uses this information and overwrites the parameter settings with the calculated value.
7.3.2. Concepts¶
How to scale model output¶
Modgen provides a series of useful functions for handling population scaling (weighting). In DYNAMIS-POP-MRT, all persons have the same weight and scaling can be switched on and off.
Scaling in case-based models¶
The user can set three parameters in the scenario settings: the number of cases, target population size, and a box to check if population scaling is requested. This information can be accessed by the following functions:
GetPopulationScalingRequired(); // returns true if the checkbox is clicked
GetAllCases(); // returns the number of cases to be simulated
GetPopulation(); // returns the total target population size
If population scaling is clicked on, scaling is switched on automatically and no more programming is required. In some cases, like in DYNAMIS-POP-MRT), it is useful to overwrite the total population size, as the information may be contained in parameters. In this case, population size can be set in the code and no more programming is required: if population scaling is switched on, the target size is automatically used:
SetPopulation(SizeOfThePopulation); // Sets the total target population size
A good place to put this function is in PreSimulation().
Scaling in time-based models¶
Time-based models do not contain scenario settings for population scaling, and the SetPopulation() function has no effect in time-based models. Additional parameters therefore must be added to the model. Once the weight of an actor is calculated, it has to be set by the following functions:
Set_actor_weight(ActorWeight);
Set_actor_subsample_weight(ActorWeight);
A good place to set the actor weight is in the Start() function.
7.3.3. How to reproduce this step¶
- We add a model-generates parameter in StartPopSampling.mpp.
- We add code to ModelSettings.mpp for making scenario settings transparent and allow population scaling both in case-based and time-based model versions.
- We modify the Start() function to weight actors in time-based models.
Changes in StartPopSampling.mpp¶
We change the starting population sampling table to a model_generated parameter and add a population table of the same shape as a normal parameter. This allows us to calculate the size of the starting population from the parameter table in the pre-simulation, and this enables scaling the simulation and determining the probability that an actor comes from the starting population versus the immigrant population. This cannot be done directly from a cumrate table, as Modgen optimizes and re-scales cumrate tables, with the information of the original cell values lost. To solve this problem, we count values and copy them over from the parameter table to the model-generated cumrate table.
parameters
{
double StartingPopulation[SEX][AGE_RANGE][PROVINCE_NAT]; //EN Starting population
//EN Starting population
model_generated cumrate[3] StartingPopulationTable[SEX][AGE_RANGE][PROVINCE_NAT]; };
parameter_group PG01_StartPop //EN Starting Population Characteristics
{
StartingPopulation
};
Code in the module ModelSettings.mpp¶
We add a series of model-generated parameters that can be set in a PreSimulation() function of the model and are not visible to the user. In the case-based model version, some of these parameters will be set by scenario settings, while time-based models will have to make these parameters real parameters, set by the user. Making all parameters explicit and placing them into one file makes the model more transparent and simplifies model conversions.
Parameters:
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
model_generated double StartPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
Pre-Simulation
In the case-based version of DYNAMIS-POP-MRT, all information is contained in the scenario settings. As the total population size is already known from the sampling table parameter, which has population totals by age, sex, and province, we can add up the totals in the table and overwrite the parameter in the scenario settings as it became redundant. As we use the population table for sampling, we need a table of type cumrate. Unfortunately, cumrate parameters do not allow access to their values, as the tables are internally optimized for sampling. One could use the inelegant work-around of copying values on a table of type double into a model-generated cumrate table of the same shape.
At the end of the function, when requested by the user and when the model is case-based (the parameter model_is_case_based is provided by the simulation engine), we set the target population, which automatically scales the population, in case-based models. We also calculate the actor weight “manually,“as this information will be used by time-based model versions in the Start() function.
void PreSimulation()
{
// Starting population
StartPopSize = 0.0;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_NAT); nProv++)
{
StartPopSize = StartPopSize + StartingPopulation[nSex][nAge][nProv];
StartingPopulationTable[nSex][nAge][nProv] = StartingPopulation[nSex][nAge][nProv];
}
}
}
//Total population
TotalPopSize = StartPopSize;
TotalPopSampleSize = GetAllCases();
StartPopSampleSize = TotalPopSampleSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
//Scale Population
ScalePopulation = GetPopulationScalingRequired();
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
Changes in PersonCore.mpp¶
We add the following code for population scaling in the Start() function:
// Population scaling
if (ScalePopulation && !model_is_case_based)
{
Set_actor_weight(ActorWeight);
Set_actor_subsample_weight(ActorWeight);
}
This code is executed in time-based models only.
7.3.4. Model versions¶
Converting the model to a time-based and/or micro-data based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
The module ModelSettings.mpp in alternative versions of Step 2¶
Sampling - time based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
model_generated double StartPopSize; //EN Total population size
long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG00_ModelSettings //EN Model Settings
{
TotalPopSampleSize, ScalePopulation
};
void PreSimulation()
{
// Starting population
StartPopSize = 0.0;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_NAT); nProv++)
{
StartPopSize = StartPopSize + StartingPopulation[nSex][nAge][nProv];
StartingPopulationTable[nSex][nAge][nProv] = StartingPopulation[nSex][nAge][nProv];
}
}
}
//Total population
TotalPopSize = StartPopSize;
StartPopSampleSize = TotalPopSampleSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
};
Micro data file - case based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
void PreSimulation()
{
//Total population
TotalPopSize = StartPopSize;
TotalPopSampleSize = GetAllCases();
StartPopSampleSize = TotalPopSampleSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
//Scale Population
ScalePopulation = GetPopulationScalingRequired();
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
Micro data file - time based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG_ModelSettings //EN Model Settings
{
StartPopSize, TotalPopSampleSize, ScalePopulation
};
void PreSimulation()
{
//Total population
TotalPopSize = StartPopSize;
StartPopSampleSize = TotalPopSampleSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
};
7.4. Step 4: Fertility¶
7.4.1. Overview¶
This step adds the module Fertility.mpp to the model. The module has three parameters: the distribution of age-specific fertility, total fertility rate, and sex ratio. The first two parameters are used to internally calculate age-specific fertility rates in the pre-simulation function, which is called before the start of the simulation. It is a so-called model-generated parameter not visible to users.
At the birth event, a new actor is created and the Start() function of this actor is called. To pass on a link to the mother, which is used by the child to determine its province, time of birth, etc., the Start() function is modified, now taking on a parameter—a link to the mother.
7.4.2. Concepts¶
Overview¶
At this step, we introduce two key Modgen concepts and functions:
- RANGE_POS
- Model-generated parameters
- Pre-simulation
- Creating an actor and inheriting characteristics
RANGE_POS¶
RANGE_POS(NAME_RANGE, value) returns the position of the value within a range, the first position being 0. This is used to address values within a range.
Example:
Range: range FERTILE_AGE_RANGE { 15,49 };
Parameter: double FertilityRate[FERTILE_AGE_RANGE];
derived state: double current_fertility = ( WIHIN(FERTILE_AGE_RANGE,integer_age) ) ?
FertilityRate[RANGE_POS(FERTILE_AGE_RANGE, integer_age)] : 0.0;
As the fertile age range starts at 15, to get the right index for a given age in full years, we use the RANGE_POS function, which returns the index of a specific value within a range. For example, RANGE_POS(FERTILE_AGE_RANGE, 15) returns 0, as 15 is the first value in the range.
Model-generated parameters¶
Model-generated parameters are not visible to users, and are calculated in the Pre-Simulation function from the other parameters. Model-generated parameters are declared like other parameters, but headed by model_generated.
//EN Age specific fertility rate
model_generated double AgeSpecificFertilityRate[FERTILE_AGE_RANGE][SIM_YEAR_RANGE];
Pre-Simulation¶
The function PreSimulation() is called before the start of the simulation and before actors are created, so that new “model_generated” parameters can be calculated and other initialization can be made. PreSimulation() functions can be placed in the code multiple times, allowing to code module-specific pre-simulations within the module to which it belongs.
Creating and actor and inheriting characteristics¶
Creating and initializing a new actor requires only two lines of C++ code, which may look esoteric to non-C++ programmers. The first creates a new actor by declaring a pointer to an object of type Person created by the keyword new. The pointer can then be used to call the Start() function of the new Person, addressing it with “->”. Start functions allow parameters, in our case, of type “pointer to a person.” When calling the function with “this” as a parameter, a pointer to the calling person (i.e., the mother) is passed to the child. Within the Start function, the child can now make use of the pointer to access characteristics of the mother.
actor Person
{
void Start(long PersonNr, Person *peMother); // Start function with a parameter 'pointer to a person'
};
void Person::BirthEvent()
{
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1, this); // Call Start() of the new actor and pass own address
}
void Person::Start(long PersonNr, Person *peMother)
{
[...]
time_of_birth = peMother->time; // The time of creation
[...]
}
7.4.3. How to reproduce this modeling step¶
Most of the code of this step is contained in a new module, Fertility.mpp.
The module Fertility.mpp¶
Add a new module Fertility.mpp¶
To add Fertility.mpp, the module will follow a typical organization of code: - Documentation - Dimensions - Parameters - Actor declarations - Function / Event implementations - pre-simulation routines.
Documentation¶
It is good practice to start with the label and a note describing the module.
//LABEL(Fertility, EN) Fertility
/* NOTE(Fertility, EN)
Added at step 3
This module implements fertility.
Fertility is based on age-specific fertility rates calculated from two parameters:
an age distribution of fertility and the Total fertility rate TFR for future years
Another parameter is the sex-ratio
*/
New type definitions¶
The module introduces two additional ranges: one for the number of children, which we limit to 15, and the second for the fertile age range 15-49.
range FERTILE_AGE_RANGE{ 15, 49 }; //EN Fertile age range
range PARITY_RANGE{ 0, 15 }; //EN Parity range
Parameters¶
The model has three parameter tables plus an internal model-generated parameter calculated from the other parameters. Model-generated parameters are calculated in the Pre-Simulation phase as described below. They are not visible to users. Model-generated parameters are declared like other parameters, but headed by model_generated.
parameters
{
double AgeSpecificFertility[FERTILE_AGE_RANGE][SIM_YEAR_RANGE]; //EN Age distribution of fertility
double TotalFertilityRate[SIM_YEAR_RANGE]; //EN Total fertility rate
double SexRatio[SIM_YEAR_RANGE]; //EN Sex ratio
//EN Age specific fertility rate
model_generated double AgeSpecificFertilityRate[FERTILE_AGE_RANGE][SIM_YEAR_RANGE];
};
parameter_group PG03_Fertility //EN Fertility
{
AgeSpecificFertility, TotalFertilityRate, SexRatio
};
Actor declarations¶
The module introduces two simple states: parity and an indicator that a person has “started life,” i.e., left the Start() function. Parity simply counts the number of children and is incremented in the birth event.
The SetAlive Event is used to switch the state set_alive to true immediately after an actor is created and has processed its Start() function. Transitions are not visible if a state is changed/initialized in Start(). The state is used, for example, in tables for counting births, transitions(set_alive,FALSE,TRUE), and (later) to avoid linkages between actors before their time of birth is known, which is immediately after Start().
actor Person
{
PARITY_RANGE parity = { 0 }; //EN Number of children born
logical set_alive = { FALSE }; //EN Person is set to be alive already
event timeBirthEvent, BirthEvent; //EN Birth event
event timeSetAliveEvent, SetAliveEvent; //EN Event to set set_alive to true
};
Event implementation¶
Birth is a typical event based on hazard rates. In the time function, it is checked if a person is at risk, and if so, a waiting time is drawn from an exponential distribution based on the age and period-specific hazard.
When a birth happens, the state parity is incremented by 1 (in C++ this can be done by parity++). The last two lines create a new child actor; this is C++ notation for creating a pointer to a new object and invoking a function—here, the Start() function—of the new actor. To establish communication between mother and child, the mother passes on her own address (“self”) to the child. Note that in C++, Object->Function() indicates that the function of another object (here, the child) is called.
TIME Person::timeBirthEvent()
{
double dEventTime = TIME_INFINITE;
if (sex==FEMALE && WITHIN(FERTILE_AGE_RANGE, integer_age) && parity < MAX(PARITY_RANGE)
&& WITHIN(SIM_YEAR_RANGE, calendar_year))
{
double dHazard = AgeSpecificFertilityRate[RANGE_POS(FERTILE_AGE_RANGE, integer_age)]
[RANGE_POS(SIM_YEAR_RANGE, calendar_year)];
if (dHazard > 0.0) dEventTime = WAIT(-TIME(log(RandUniform(4)) / dHazard));
}
return dEventTime;
}
void Person::BirthEvent()
{
parity++; // increment parity
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1,this); // Call Start() of the new actor and pass own address
}
Note that when addressing AgeSpecificFertilityRate, we do not use integer_age and calendar_year directly as index, but write:
AgeSpecificFertilityRate[RANGE_POS(FERTILE_AGE_RANGE, integer_age)][RANGE_POS(SIM_YEAR_RANGE, calendar_year)]
This is valid, as ranges in Modgen internally start with index 0. As the fertile age range starts at 15 (range FERTILE_AGE_RANGE { 15,49 };), to get the right index for a given age in full years, we use the RANGE_POS function, which returns the index of a specific value within a range; for example, RANGE_POS(FERTILE_AGE_RANGE, 15) returns 0, as 15 is the first value in the range.
The SetAliveEvent is straightforward: if a person is not set alive yet, this is done immediately. In this way, the event occurs immediately after the Start() function was processed and the change in state can be observed, which will be useful for creating tables counting the number of births.
TIME Person::timeSetAliveEvent() { if (!set_alive) return WAIT(0); else return TIME_INFINITE; }
void Person::SetAliveEvent() { set_alive = TRUE; }
Pre-Simulation¶
The function PreSimulation() is called before the start of the simulation and before actors are created. This way, new “model_generated” parameters can be calculated, and other initialization can be made. PreSimulation() functions can be placed multiple times in the code, allowing to code module-specific pre-simulations within the module to which it belongs.
In our case, we perform the calculation of age-specific fertility rates for each projected year in this function. The parameter is built by scaling the known age distribution of the period to total 1, and then multiplying it by the known total fertility rate of the period.
void PreSimulation()
{
double dSum;
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
dSum = 0.0;
// check if distribution parameter adds up too 1
for (int nAge = 0; nAge < SIZE(FERTILE_AGE_RANGE); nAge++)
{
dSum = dSum + AgeSpecificFertility[nAge][nYear];
}
// scale distribution to 1 and convert to fertility rates; copy to model generated parameter
for (int nAge = 0; nAge < SIZE(FERTILE_AGE_RANGE); nAge++)
{
if (dSum > 0.0)
{
AgeSpecificFertilityRate[nAge][nYear]
= AgeSpecificFertility[nAge][nYear] / dSum * TotalFertilityRate[nYear];
}
else AgeSpecificFertilityRate[nAge][nYear] = 0.0;
}
}
}
Modification of the Start() Function in PersonCore.mpp¶
Using the parameter of the Start() function¶
As we pass on the address of the mother to the child at birth, we make use of one of the two parameters of the start function:
void Start(long PersonNr, Person *peMother); //EN Starts the actor
The parameter is also used to distinguish whether a person is created from the starting population (in this case the mother is unknown and NULL will be used as parameter) or created at a birth event and thus knows her mother. In the second case, the characteristics at birth are not sampled as before—by calling GetStartCharacteristics()—but initialized within the Start function.
- Sex is randomly chosen based on the given sex ratio
- Time_of_birth is set by accessing the mother’s state time
- Province_nat is inherited from the mother
void Person::Start(Person *peMother)
{
age = 0;
if (peMother == NULL) // No mother: the person comes from the starting population
{
GetStartCharacteristics();
}
else // There is a mother: the person is a child born in the simulation
{
// assign a sex according to the sex ratio parameter
sex = MALE;
if (RandUniform(5) <100.0 /(100.0 + SexRatio[RANGE_POS(SIM_YEAR_RANGE, calendar_year)]))
{
sex = FEMALE;
}
// inherit characteristics from the mother
time_of_birth = peMother->time; // The time of creation
province_nat = peMother->province_nat; // The province at birth
}
time = time_at_birth;
}
Add parameters to the data file and initialize them with values¶
The model code is now complete. To run the model, we still have to add the parameters and parameter values to the data file. If values will be copied and pasted later, dummy tables can be generated by:
double AgeSpecificFertility[FERTILE_AGE_RANGE][SIM_YEAR_RANGE] = {(100)1};
double TotalFertilityRate[SIM_YEAR_RANGE] = {(100) 4};
double SexRatio[SIM_YEAR_RANGE] = {(100) 106.0};
Tables¶
We add two tables to the output, one for age-specific fertility rates, the second for number of births.
// A New table group for fertility tables
table_group TG_03_FertilityTables //EN Fertility Tables
{
FertilityRates, NumberBirths
};
// A state used for the following table
actor Person
{
FERTILE_AGE_RANGE tab_fert_age = COERCE(FERTILE_AGE_RANGE, integer_age); //EN Age
};
// Tables
table Person FertilityRates //EN Fertility rates
[WITHIN(SIM_YEAR_RANGE, calendar_year) && WITHIN(FERTILE_AGE_RANGE, integer_age) && sex == FEMALE]
{
{
parity / duration() //EN Fertility rates decimals=4
}
* tab_fert_age
* sim_year
};
table Person NumberBirths //EN Number of births
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
{
entrances(set_alive, TRUE) //EN Births
}
* sim_year
* sex +
};
Modifications of an existing table¶
We add a filter condition to include those born in the future from the life expectancy calculation.
table Person RemainingLifeExpectancyAge //EN Remaining life expectancy by age at start
[in_projected_time && time_of_birth < MIN(SIM_YEAR_RANGE)]
{
tab_age_start *
{
duration() / unit //EN Remaining life expectancy in simulation decimals=4
}
};
7.4.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step of this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step, starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
No changes affecting model conversions are made at this step, compared to the previous step. Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.5. Step 5: Refining Mortality¶
7.5.1. Overview¶
This step refines the mortality module. The current life table is extended to model mortality by sex. The mortality table interprets a standard life table, which is used for modeling the age-specific mortality patterns, while we add a second parameter for life expectancy at birth. In the pre-simulation phase, a calibration factor for each simulation year is searched for, which, when applied as proportional factor, adjusts the standard mortality table to produce the target life expectancy.
7.5.2. Concepts¶
This step does not introduce new Modgen concepts, but does introduce some C++ programming within a PreSimulation function performing a binary search for finding calibration factors. This scales the mortality table to produce the target period life expectancies, which are a model parameter.
Example for a binary search¶
This binary search algorithm is used to find calibration factors that will be copied into a model-generated trend parameter.
void PreSimulation()
{
double dLower, dUpper, dCenter, dResult, dTarget, dAlive, dDeaths;
int nIterations;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
dTarget = LifeExpectancy[nYear][nSex]; // Target: life expectancy
dResult = 0.0; // Search result: life expectancy
nIterations = 10000; // Maximum number of iterations in search
dLower = 0.1; // Lower limit of calibration factor (relative risk)
dUpper = 3.0; // Upper limit of calibration factor (relative risk)
while (abs(dResult - dTarget) > 0.0001 && nIterations > 0)
{
nIterations--;
dCenter = (dLower + dUpper) / 2.0; // New calibration factor for probing
dResult = 0.0;
dAlive = 1.0; // Proportion of people still alive
// Life expectancy calculated applying calibration factor
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
// proportion of deaths in year: survival = exp(-hazard)
dDeaths = dAlive * (1 - exp(-MortalityTable[nAge][nSex] * dCenter));
dAlive = dAlive - dDeaths;
// people dying in this year are assumed to die in th emiddle of the year
dResult = dResult + dDeaths * 0.5 + dAlive;
}
// Moving the search limits for next iteration
if (dTarget < dResult) dLower = dCenter;
else dUpper = dCenter;
}
// copying the best solution into the model-generated mortality trend parameter
MortalityTrend[nYear][nSex] = dCenter;
}
}
}
7.5.3. How to reproduce this modeling step¶
All code changes at this step are made in the existing module, Mortality.mpp, and the data file.
- Update the documentation
- Extend the standard mortality table by an additional dimension, sex
- Add a parameter for life expectancy
- Add a model-generated parameter for calibration factors reflecting mortality trends
- Change the time function of mortality to model mortality by sex and apply trend adjustments
- Calculate (find by binary search) the model-generated parameter MortalityTrend in PreSimulation()
- Add parameters with values to .dat file
- Modify tables
Code changes and new code in Mortality.mpp¶
Update the documentation¶
/* NOTE(Mortality, EN)
Changed at step 4
This module implements a simple model of mortality.
People can die at any moment of time based on age-specific mortality rates.
Age specific mortality rates are calculated from a standard life table and a trend factor.
The trend factor is found in pre-simulation and calibrates mortality to reach a target
period life expectancy. The standard life table and target life expectancies are model parameters.
The only state of this module is 'alive' which is set to 'false' at death.
Also at death, the Modgen function Finish() is called which clears up memory space.
*/
Update parameters¶
parameters
{
double MortalityHazard[AGE_RANGE][SEX]; //EN Mortality hazard by age
double LifeExpectancy[SIM_YEAR_RANGE][SEX]; //EN Life Expectancy
model_generated double MortalityTrend[SIM_YEAR_RANGE][SEX]; //EN Mortality trend
};
parameter_group PG02_Mortality //EN Mortality
{
MortalityTable, LifeExpectancy
};
As with any parameter changes, parameters also have to be updated in the data file.
Updtate the mortality time function¶
To apply the new mortality hazard, calculated by mortality, age, sex, and the trend factor, we add a new local variable, dMortalityHazard, and initialize it with the formula. We then use dMortalityHazard instead of the previous one-dimensional parameter in the code, where the hazard is needed to check if a person is at risk, and to calculate a waiting time.
TIME Person::timeMortalityEvent()
{
TIME dEventTime = TIME_INFINITE;
double dMortalityHazard
= MortalityTable[integer_age][sex] * MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, calendar_year)][sex];
// check if a person is at risk
if (dMortalityHazard > 0.0 && in_projected_time)
{
// determine the event time
// the formula [ -log(rand) / hazard ] calculates an exponentially distributed waiting time
// based on a uniform distributed random number and a given hazard rate
dEventTime = WAIT(-log(RandUniform(1)) / dMortalityHazard);
}
// return the event time, if the maximum age is not reached at that point
if (dEventTime < time_of_birth + MAX(AGE_RANGE) + 1.0) return dEventTime;
// otherwise, return the moment, at which the maximum age is reached
else return time_of_birth + MAX(AGE_RANGE) + 0.9999;
}
Calculate the mortality trend adjustment factors in pre-simulation¶
This binary search algorithm is used to find calibration factors, which are copied into a model-generated trend parameter MortalityTrend[nYear][nSex] .
void PreSimulation()
{
double dLower, dUpper, dCenter, dResult, dTarget, dAlive, dDeaths;
int nIterations;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
dTarget = LifeExpectancy[nYear][nSex]; // Target: life expectancy
dResult = 0.0; // Search result: life expectancy
nIterations = 10000; // Maximum number of iterations in search
dLower = 0.1; // Lower limit of calibration factor (relative risk)
dUpper = 3.0; // Upper limit of calibration factor (relative risk)
while (abs(dResult - dTarget) > 0.0001 && nIterations > 0)
{
nIterations--;
dCenter = (dLower + dUpper) / 2.0; // New calibration factor for probing
dResult = 0.0;
dAlive = 1.0; // Proportion of people still alive
// Life expectancy calculated applying calibration factor
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
// proportion of deaths in year: survival = exp(-hazard)
dDeaths = dAlive * (1 - exp(-MortalityTable[nAge][nSex] * dCenter));
dAlive = dAlive - dDeaths;
// people dying in this year are assumed to die in th emiddle of the year
dResult = dResult + dDeaths * 0.5 + dAlive;
}
// Moving the search limits for next iteration
if (dTarget < dResult) dLower = dCenter;
else dUpper = dCenter;
}
// copying the best solution into the model-generated mortality trend parameter
MortalityTrend[nYear][nSex] = dCenter;
}
}
}
Changes in Tables.mpp¶
Because we added sex to mortality and mortality follows a time trend, we modify the table DeathRates to add the two new dimensions. In its default view (users can always select the ordering of dimensions in the user interface), the table now displays mortality measures by age and year; the other two dimensions of displayed measure (number, rates, probabilities) and sex (adding + also produces totals) can be selected from drop-down lists.
table Person DeathRates //EN Death rates and probabilities
[ in_projected_time ]
{
sex+ *
{
transitions(alive, TRUE, FALSE), //EN Number of persons dying at this age
transitions(alive, TRUE, FALSE) / duration(), //EN Death Rate decimals=4
transitions(alive, TRUE, FALSE) / unit //EN Death probability decimals=4
}
* integer_age
* sim_year
};
7.5.4 Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step of this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
No changes affecting model conversions are made in this step, compared to the previous step. Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.6. Step 6: Migration¶
7.6.1. Overview¶
This step adds internal migration to the model. It is based on age- and sex-specific transition matrices. Only one transition per year is allowed. The module has three parameters, one to switch migration on/off, the second for the probability to move away (by province, age, and sex), and the third to sample the destination province (by origin, age, and sex).
7.6.2. Concepts¶
- Transition matrices
- SPLIT
- Converting probabilities to rates
Transition matrices¶
Transition matrices are origin-destination matrices containing probabilities to move from one to another state in a given time period. They are typically divided into two parameters, one containing the probability to leave by origin, and the second the distribution of destinations by origin. The second parameter—using the Modgen type cumrate—allows sampling the destination when the event happens.
Example
parameters
{
double MigrationProbability[SEX][AGE5_PART][PROVINCE_NAT]; //EN Migration probability
cumrate MigrationDestination[SEX][PROVINCE_NAT][AGE5_PART][PROVINCE_NAT]; //EN Migration Destination
};
As such matrices contain probabilities to move within a period of time (usually years), there are four ways of implementing events:
- By a clock event creating an event at a specific point in each period, e.g., in the middle of the year. At that time, it is decided if an actor moves and, if so, the move to a sampled destination is done immediately. Following this approach, everybody moves at the same time if modeling moves by calendar year.
- Deciding at the beginning of each period if a person is supposed to move. If so, a random time within the period is drawn and the event is scheduled for this time. One drawback is that characteristics that may influence the probability to move may change within the period.
- Probabilities are converted to corresponding hazard rates [ rate = -ln(1-probability) ] and the event is implemented in continuous time. Using this approach, as here in the Migration.mpp module, one has to be sure that only one event can happen within the period.
- The transition matrix of probabilities is converted to a matrix of rates. Applying this approach, multiple events are possible within a year, but after each period, consistency with the original origin-destination transitions is maintained. There exists a Taylor series approach for this approach. The beauty of this approach is that transition matrices calculated from longer intervals, e.g., five-year inter-census intervals, can also be used without restricting moves to a single point in time per interval.
SPLIT(), split(), self_scheduling_split()¶
Split functions break up continuous variables into intervals.
split() can be used in tables or for derived states; it is dynamic, thus maintains itself:
Example
partition AGE5_PART { 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 }; //EN Age Groups
table Person TabNumberMoves //EN Internal migration rate
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
{
number_moves / duration() //EN Migration rate decimals=4
}
* split(integer_age, AGE5_PART) //EN Age group
};
SPLIT() is used within functions. It is static, returning the index at a point in time.
Example
void Person::MigrationEvent()
{
int nDestination;
int nAge5 = SPLIT(integer_age, AGE5_PART);
[...]
}
self_scheduling_split() is a self-scheduling function, so it not only updates itself if the underlying variable changes (e.g., integer_age changes) but also ensures that the underlying variable is updated at the scheduled moment. This is useful when using continuous variables like age or time, as there may not be another event happening at the time the split is supposed to happen. The above table could also be written as:
table Person TabNumberMoves //EN Internal migration rate
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
{
number_moves / duration() //EN Migration rate decimals=4
}
* self_schedling_split(age, AGE5_PART) //EN Age group
};
Note that by producing their own events, self-scheduling states are resource intensive and unnecessary use should be avoided, including multiple use of the same split function, e.g., in various tables.
Converting probabilities to rates¶
Probabilities and rates are linked through the concept of survival, which is the probability that no event happens within a period where an actor is at a constant risk of an event.
within one time interval:
survival = exp(-rate) the probability that nothing happens under a constant risk 'rate'
rate = -ln(survival) the rate, at which survival % of the population does not experience an event
Note that for a constant rate, the waiting time of an event is exponentially distributed, while the number of events follows the Poisson distribution.
7.6.3. How to reproduce this modeling step¶
All code, except for tables, is added at this step within the new module, Migration.mpp. The module has a typical organization:
- Documentation
- Types
- Parameters
- Actor definitions
- Event implementation
The module Migration.mpp¶
The following describes how to add a module Migration.mpp.
Documentation¶
//LABEL(Migration, EN) Migration
/* NOTE(Migration, EN)
Added at Step 5
This module implements internal migration. It is based on age and sex specific transition
matrices. Only one transition per year is allowed. The module has three parameters, one to
switch migration on/off, the second for the probability to move away (by province, age and
sex), the third to sample the destination province (by origin, age and sex).
*/
Types¶
The model uses five-year age groups, which can built by a partition.
partition AGE5_PART { 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 }; //EN Age Groups
Parameters¶
The age partition into five-year age groups is used as a dimension type of the two parameters handling migration: the probability to leave and the destination. As the destination is sampled, a cumrate parameter is used.
parameters
{
logical ModelMigration; //EN Migration switched on/off
double MigrationProbability[SEX][AGE5_PART][PROVINCE_NAT]; //EN Migration probability
cumrate MigrationDestination[SEX][PROVINCE_NAT][AGE5_PART][PROVINCE_NAT]; //EN Migration Destination
};
parameter_group PG_Migration //EN Internal migration
{
ModelMigration, MigrationProbability, MigrationDestination
};
Actor block¶
The module has two simple states: the first records the number of moves of a person, the second records the age at the last move. (Only one move is allowed by age year.) There is one event, the migration event, at which a new province is chosen.
actor Person
{
integer number_moves = { 0 }; //EN Number of interprovincial moves
logical age_at_last_move = { 999 }; //EN Age at last migration
event timeMigrationEvent, MigrationEvent; //EN Migration Event
};
Event implementation¶
The event time for an inter-provincial move is modeled in continuous time. To do so, the given probability is transformed into a hazard rate = -ln(1 - probability).
TIME Person::timeMigrationEvent()
{
// get the current age index using SPLIT()
int nAge5 = SPLIT(integer_age, AGE5_PART);
// get the probability to move
double dMoveProb = MigrationProbability[sex][nAge5][province_nat];
// Check if a person is at risk for moving
if (ModelMigration && in_projected_time && !has_moved_this_year && dMoveProb > 0.0)
{
if (dMoveProb >= 1.0) return WAIT(0); // if there is a 100% probability move immediately
else // there is o positive probability 100%
{
// calculate a random waiting time based on the given probability converted to a hazard
// rate = -log(1-probability)
return WAIT(-log(RandUniform(2)) / -log(1 - dMoveProb));
}
}
return TIME_INFINITE;
}
void Person::MigrationEvent()
{
int nDestination;
int nAge5 = SPLIT(integer_age, AGE5_PART);
// Sample the destination
Lookup_MigrationDestination(RandUniform(3), sex, province_nat, nAge5, &nDestination);
// move the actor to the destination
province_nat = (PROVINCE_NAT)nDestination;
// update indicators
has_moved_this_year = TRUE;
number_moves++;
}
New tables in Tables.mpp¶
We add two tables, one for the average population by sex, province, and projected year, and the other for simulated migration rates by sex, age group, and province of origin.
table Person PopulationTableProvAverage //EN Average Population by province
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
sex + *
{
duration() //EN Average Population
}
* province_nat +
* sim_year
};
table Person TabNumberMoves //EN Internal migration rate
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
sex + *
{
number_moves / duration() //EN Migration rate decimals=4
}
* split(integer_age, AGE5_PART) //EN Age group
* province_nat +
};
7.6.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
No changes affecting model conversions are made at this step, compared to the previous step. Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.7. Step 7: Emigration¶
7.7.1. Overview¶
This step adds emigration to the model. Emigration is based on emigration rates by age group, sex, and province of residence. The module can be switched on/off by the user.
7.7.2. Concepts¶
This module adds emigration to the model. It does not introduce any new Modgen concept, but can be seen as a typical Modgen module containing most basic concepts.
7.7.3. How to reproduce this modeling step¶
Add emigration to the model by using all new code (except for tables), which are within the new module Emigration.mpp, which follows a typical outline.
The Emigration.mpp module¶
Add a new module Emigration.mpp and its documentation.
//LABEL(Emigration, EN) Emigration
/* NOTE(Emigration, EN)
Added at Step 6
This module implements emigration based on emigration rates by age group, sex, and
province of residence. The module can be switched on/off by the user.
*/
Parameters¶
parameters
{
logical ModelEmigration; //EN Switch emigration on/off
double EmigrationRates[SEX][AGE5_PART][PROVINCE_NAT]; //EN Emigration Rates
};
parameter_group PG_Emigration //EN Emigration
{
ModelEmigration, EmigrationRates
};
Actor definitions¶
actor Person
{
logical has_emigrated = { FALSE }; //EN Person has emigrated
event timeEmigrationEvent, EmigrationEvent; //EN Emigration
};
Event implementation¶
TIME Person::timeEmigrationEvent()
{
if (ModelEmigration && calendar_year >= MIN(SIM_YEAR_RANGE) &&
EmigrationRates[sex][SPLIT(integer_age, AGE5_PART)][province_nat] > 0.0)
{
return WAIT(-log(RandUniform(10)) / EmigrationRates[sex][SPLIT(integer_age, AGE5_PART)][province_nat]);
}
else return TIME_INFINITE;
}
void Person::EmigrationEvent()
{
has_emigrated = TRUE;
Finish();
}
Tables¶
We add a validation table for emigration rates.
table Person EmigrationRatesTable //EN Emigration rates
[WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
sex + *
{
transitions(has_emigrated, FALSE, TRUE) / duration() //EN Emigration rates decimals=4
}
*split(integer_age, AGE5_PART) //EN Age group
* province_nat +
};
Once again, the table for remaining life expectancy failed, as emigrants are recorded only while they are in the country. To fix this, we produce the table at death, and therefore exclude all who emigrated and whose death is not recorded.
table Person RemainingLifeExpectancyAge //EN Remaining life expectancy by age at start
[trigger_entrances(alive,FALSE) && in_projected_time && time_of_birth < MIN(SIM_YEAR_RANGE)]
{
tab_age_start *
{
(value_in(age) - value_in(tab_age_start)) / unit //EN Remaining life expectancy in simulation decimals=4
}
};
7.7.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
No changes affecting model conversions are made at this step, compared to the previous step. Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.8. Step 8: Immigration¶
7.8.1. Overview¶
This step adds immigration to the model. Immigration is based on immigration numbers by age, sex, and destination province, and can be switched on and off. Model parameters are the number and sex of immigrants by year, the age distribution by sex, and the distribution of destination provinces by sex and age. Immigrants are created at the moment of immigration with the age at immigration.
This step also adds population scaling to the model. The number of persons by age, sex, and province in the distributional table for the starting population and the parameter for the number of future immigrants is used to calculate the “true” population size, to which the model output can be scaled automatically if “scale population” is selected in the model settings. The output then reflects the real population size, independent of the chosen sample size.
7.8.2. Concepts¶
This module adds immigration to the model. One new concept totals the values in population tables and copies them into a model-generated cumrate table in the pre-simulation phase. This is necessary, as cumrate parameters are internally optimized for sampling, but lose information about their original cell values and their sum.
Another concept introduced at this step is automatic population scaling. If the user clicks “population scaling” in the model settings, the model scales automatically to the population size recorded in the starting population and immigrant tables. This overwrites any other scaling target.
Copying parameters from double to cumrate tables in pre-simulation¶
The following code loops through a population table, adding the cell values and copying values to a cumrate parameter of the same shape.
// Count and copy Starting population
StartPopSize = 0.0;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_NAT); nProv++)
{
StartPopSize = StartPopSize + StartingPopulation[nSex][nAge][nProv];
StartingPopulationTable[nSex][nAge][nProv] = StartingPopulation[nSex][nAge][nProv];
}
}
}
Population scaling¶
Case-based models¶
Case-based models provide an option to scale model output to a target population size in the Scenario, where users can specify target population size. Target population size corresponds to the number of cases created by the simulation engine, i.e., the generation zero (who then can have children, grand-children, etc., who are not counted here). As the target population can include persons born in the future (e.g., immigrants), it is not a readily available number. Modgen allows overwriting this number with a number calculated in the simulation, e.g., adding together all values from the population table and the table of immigrants.
The following code gives three scaling options:
- No scaling: a parameter “scale population” and the scenario settings for scaling are not clicked. The model produces the number of actors, as in the scenario settings’ number of cases.
- Scaling as in scenario settings: a parameter “scale population” is not clicked, but scaling is switched on in the scenario settings. The population is scaled to the number in the settings.
- Automatic scaling to the “true” population size is determined in the simulation. The parameter for automatic population scaling is clicked, and the settings of the scenario options are overwritten.
The following code uses the following parameters, settings, and functions:
- ScalePopulation: a model parameter. The code is effective only if it is true; otherwise scaling is handled by scenario settings.
- SetPopulation(TotalPopSize) overwrites the value of the target size in the scenario settings. If scaling is clicked on in the scenario settings, scaling is handled automatically.
- GetPopulationScalingRequired() returns true if the user has clicked scaling in the scenario settings. If not, code for weighting the actors has to be added, as it will not be handled automatically.
- Set_actor_weight() and Set_actor_subsample_weight() are the functions used for setting weights.
- TotalPopSampleSize is a model_generated parameter identical to the number of simulated cases specified in the scenario settings.
- TotalPopSize is the total target population size calculated in the model from parameter tables.
void Person::Start(long PersonNr, Person *peMother)
{
if (ScalePopulation)
{
SetPopulation(TotalPopSize);
// Set the actor weight if population scaling not clicked in Scenario settings
if (!GetPopulationScalingRequired())
{
Set_actor_weight(TotalPopSize / TotalPopSampleSize);
Set_actor_subsample_weight(TotalPopSize / TotalPopSampleSize);
}
}
[...]
}
Note that, if desired, models are always scaled to TotalPopSize if population scaling is turned on in the scenario settings. No additional parameter for automatic scaling is required and the target population size is always overwritten by the calculated one. In this case, weighting can be done with one line of code, which can also be placed into the pre-simulation function after TotalPopSize is calculated there. This approach works only in case-based models.
if (GetPopulationScalingRequired()) SetPopulation(TotalPopSize);
Time-based models¶
There are no scenario settings for population size and scaling in time-based models, where the population size has to be determined by a parameter. Unlke case-based models, TotalPopSampleSize is a normal parameter that must be set by the user.
The code for time-based models is:
void Person::Start(Person *peMother)
{
if (ScalePopulation)
{
Set_actor_weight(TotalPopSize / TotalPopSampleSize);
Set_actor_subsample_weight(TotalPopSize / TotalPopSampleSize);
}
[...]
}
7.8.3. How to reproduce this modeling step¶
At this step, we add immigration to the model by adding a new module, Immigration.mpp, which has many similarities to the StartPopSampling module, as its only actor function is used to sample the characteristics of new immigrants. The sampling function is called at Start(), which has to be adapted to deal with the new type of persons besides those in the starting population and simulated births. Also, because two populations—the starting population and population of immigrants—are created by the simulation engine, we must know the relation between the two population sizes. Counting the population parameter tables is performed in the Pre-Simulation. This information will be used to automatically scale the model output.
The Immigration.mpp module¶
Create and document¶
Add a new module Immigration.mpp and its documentation:
//LABEL(Immigration, EN) Immigration
/* NOTE(Immigration, EN)
Added at Step 7
This module implements immigration. Immigration can be switched on and off.
Model parameters are the number and sex of immigrants by year, the age distribution by sex,
and the distribution of destination provinces by sex and age.
Immigrants are created at the moment of immigration with the age at immigration
*/
Parameters¶
parameters
{
logical ModelImmigration; //EN Switch immigration on/off
double NumberImmigrants[SIM_YEAR_RANGE][SEX]; //EN Number of immigrants
model_generated cumrate[2] NumberImmigrantsTable[SIM_YEAR_RANGE][SEX]; //EN Number of immigrants (sampling)
cumrate AgeImmigrants[SEX][AGE_RANGE]; //EN Age distribution of immigrants
cumrate DestinationImmigrants[SEX][AGE5_PART][PROVINCE_NAT]; //EN Destination of immigrants
};
parameter_group PG_Immigration //EN Immigration
{
ModelImmigration, NumberImmigrants, AgeImmigrants, DestinationImmigrants
};
Actor states and function¶
actor Person
{
TIME time_of_immigration = { TIME_INFINITE }; //EN Time of immigration
void GetImmigrantCharacteristics(); //EN Sample immigrant characteristics
};
Function implementation¶
::
void Person::GetImmigrantCharacteristics() {
// Sex and time of immigration int nSex, nYear; Lookup_NumberImmigrantsTable(RandUniform(11), &nYear, &nSex); sex = (SEX)nSex; time_of_immigration = MIN(SIM_YEAR_RANGE) + nYear + RandUniform(12);
// Time of birth int nAge; Lookup_AgeImmigrants(RandUniform(14), sex, &nAge); double dAge = nAge + RandUniform(13); time_of_birth = time_of_immigration - dAge;
// Province of immigration int nProvince; Lookup_DestinationImmigrants(RandUniform(9), sex, SPLIT(nAge, AGE5_PART), &nProvince); province_nat = (PROVINCE_NAT)nProvince;
}
Changes in PersonCore.mpp¶
New classification and states¶
By adding immigrants to the population, we now have three types of persons: those from the starting population, those born in the simulation, and those entering the country as immigrants during the simulation. We add a new state, recording this information.
classification PERSON_TYPE //EN Person Type
{
PT_START, //EN Person from Starting Population
PT_CHILD, //EN Person born in simulation
PT_IMMIGRANT //EN Immigrant
};
actor Person
{
PERSON_TYPE person_type = { PT_START }; //EN Person type
[...]
};
Changes in ModelSettings.mpp¶
Model settings are extended to include immigrants and count the population size of future immigrants.
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
model_generated double StartPopSize; //EN Total population size
model_generated long ImmiPopSampleSize; //EN Sample size of starting population
model_generated double ImmiPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
void PreSimulation()
{
// Starting population
StartPopSize = 0.0;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_NAT); nProv++)
{
StartPopSize = StartPopSize + StartingPopulation[nSex][nAge][nProv];
StartingPopulationTable[nSex][nAge][nProv] = StartingPopulation[nSex][nAge][nProv];
}
}
}
// Immigrant population
ImmiPopSize = 0.0;
if (ModelImmigration) // If immigration is switched on
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
ImmiPopSize = ImmiPopSize + NumberImmigrants[nYear][nSex];
NumberImmigrantsTable[nYear][nSex] = NumberImmigrants[nYear][nSex];;
}
}
}
//Total population
TotalPopSize = ImmiPopSize + StartPopSize;
TotalPopSampleSize = GetAllCases();
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
StartPopSampleSize = (long)(StartPopSize / ActorWeight);
ImmiPopSampleSize = TotalPopSampleSize - StartPopSampleSize;
//Scale Population
ScalePopulation = GetPopulationScalingRequired();
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
Changes in the Start() function - 3 types of persons and population scaling¶
The Start() function is now rearranged, to accout for the three types of persons. Note that new immigrants are created at the moment of immigration and not at their date of birth, so they are created at their age at immigration.
Also, information on total population size is used now to scale the model output to the true population size if requested. If a user selects population scaling in the parameters, the scenario settings are ignored and the SetPopulation() (over)writes the target value of population scaling in the user settings. As the scenario settings for time-based models do not include the scaling option, the code checks if the model is time or case-based. If case-based, it overwrites the scenario settings if the parameter for automatic scaling to the true population is switched on.
void Person::Start(long PersonNr, Person *peMother)
{
// Population scaling
if (ScalePopulation)
{
if (model_is_case_based)
{
SetPopulation(TotalPopSize);
// Set the actor weight if population scaling not clicked in Scenario settings
if (!GetPopulationScalingRequired())
{
Set_actor_weight(TotalPopSize / TotalPopSampleSize);
Set_actor_subsample_weight(TotalPopSize / TotalPopSampleSize);
}
}
else
{
Set_actor_weight(TotalPopSize / TotalPopSampleSize);
Set_actor_subsample_weight(TotalPopSize / TotalPopSampleSize);
}
}
// Determine the person type
if (peMother != NULL) // Born in simulation
{
person_type = PT_CHILD;
}
else if (PersonNr < TotalPopSampleSize * (StartPopSize / TotalPopSize)) // Person from start population
{
person_type = PT_START;
}
else // Immigrant
{
person_type = PT_IMMIGRANT;
}
// Initializations person from start population
if (person_type == PT_START)
{
GetStartCharacteristics();
age = 0;
time = time_of_birth;
}
// Initializations persons born in simulation
else if (person_type == PT_CHILD)
{
// assign a sex according to the sex ratio parameter
sex = MALE;
if (RandUniform(5) < 100.0 / (100.0 + SexRatio[RANGE_POS(SIM_YEAR_RANGE, calendar_year)])) sex = FEMALE;
// inherit characteristics from the mother
time_of_birth = peMother->time; // The time of creation
province_nat = peMother->province_nat; // The province at birth
age = 0;
time = time_of_birth;
}
// Initializations immigrants
else
{
GetImmigrantCharacteristics();
age = time_of_immigration - time_of_birth;
time = time_of_immigration;
}
}
New tables¶
We add three tables to the group of migration tables, which can also be used to validate the results against the parameters.
table_group TG_04_MigrationTables //EN Migration
{
TabNumberMoves, EmigrationRatesTable,
NumberImmigrantsTab, AgeImmigrantsTable,
DestinationImmigrantsTable
};
table Person NumberImmigrantsTab //EN Number of immigrants
[person_type == PT_IMMIGRANT && WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
{
entrances(set_alive, TRUE) //EN Immigrants
}
* sim_year
* sex +
};
table Person AgeImmigrantsTable //EN Age at immigration
[person_type == PT_IMMIGRANT && WITHIN(SIM_YEAR_RANGE, calendar_year) && trigger_entrances(set_alive, TRUE)]
{
{
unit //EN Immigrants
}
* integer_age
* sex +
};
table Person DestinationImmigrantsTable //EN Destination of immigrants
[person_type == PT_IMMIGRANT && WITHIN(SIM_YEAR_RANGE, calendar_year) && trigger_entrances(set_alive, TRUE)]
{
sex+ *
{
unit //EN Immigrants
}
* split(integer_age, AGE5_PART) //EN Age group
* province_nat +
};
Not that all new tables filter to immigrants. After adding immigrants, we have to revise the existing tables if they are affected by the new type of persons. This is the case where we count the number of births by counting “set_alive” transition: we must include immigrants, who are set alive at the age of immigration.
table Person NumberBirths //EN Number of births
[WITHIN(SIM_YEAR_RANGE, calendar_year) && person_type == PT_CHILD ]
{
{
entrances(set_alive, TRUE) //EN Births
}
* sim_year
* sex +
};
The second table affected by the changes is the remaining life expectancy of the people in the starting population. Filtering to this person type explicitely solves the problem.
table Person RemainingLifeExpectancyAge //EN Remaining life expectancy by age at start
[trigger_entrances(alive, FALSE) && person_type == PT_START]
{
tab_age_start *
{
(value_in(age) - value_in(tab_age_start)) / unit //EN Remaining life expectancy in simulation decimals=4
}
};
7.8.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
The module ModelSettings.mpp in alternative versions of Step 7¶
Sampling - time based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
model_generated double StartPopSize; //EN Total population size
long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated long ImmiPopSampleSize; //EN Sample size of starting population
model_generated double ImmiPopSize; //EN Total population size
logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG00_ModelSettings //EN Model Settings
{
TotalPopSampleSize, ScalePopulation
};
void PreSimulation()
{
// Starting population
StartPopSize = 0.0;
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nAge = 0; nAge < SIZE(AGE_RANGE); nAge++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_NAT); nProv++)
{
StartPopSize = StartPopSize + StartingPopulation[nSex][nAge][nProv];
StartingPopulationTable[nSex][nAge][nProv] = StartingPopulation[nSex][nAge][nProv];
}
}
}
// Immigrant population
ImmiPopSize = 0.0;
if (ModelImmigration) // If immigration is switched on
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
ImmiPopSize = ImmiPopSize + NumberImmigrants[nYear][nSex];
NumberImmigrantsTable[nYear][nSex] = NumberImmigrants[nYear][nSex];;
}
}
}
//Total population
TotalPopSize = StartPopSize + ImmiPopSize;
StartPopSampleSize = TotalPopSampleSize * (StartPopSize / TotalPopSize);
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
//Scale Population
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
Micro data file - case based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
model_generated long ImmiPopSampleSize; //EN Sample size of starting population
model_generated double ImmiPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
model_generated logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG_ModelSettings //EN Model Settings
{
StartPopSize
};
void PreSimulation()
{
// Immigrant population
ImmiPopSize = 0.0;
if (ModelImmigration) // If immigration is switched on
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
ImmiPopSize = ImmiPopSize + NumberImmigrants[nYear][nSex];
NumberImmigrantsTable[nYear][nSex] = NumberImmigrants[nYear][nSex];;
}
}
}
//Total population
TotalPopSize = ImmiPopSize + StartPopSize;
TotalPopSampleSize = GetAllCases();
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
StartPopSampleSize = (long)(StartPopSize / ActorWeight);
ImmiPopSampleSize = TotalPopSampleSize - StartPopSampleSize;
//Scale Population
ScalePopulation = GetPopulationScalingRequired();
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
Micro data file - time based¶
parameters
{
model_generated long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
model_generated long ImmiPopSampleSize; //EN Sample size of starting population
model_generated double ImmiPopSize; //EN Total population size
long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG_ModelSettings //EN Model Settings
{
StartPopSize, TotalPopSampleSize, ScalePopulation
};
void PreSimulation()
{
// Immigrant population
ImmiPopSize = 0.0;
if (ModelImmigration) // If immigration is switched on
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
ImmiPopSize = ImmiPopSize + NumberImmigrants[nYear][nSex];
NumberImmigrantsTable[nYear][nSex] = NumberImmigrants[nYear][nSex];;
}
}
}
//Total population
TotalPopSize = ImmiPopSize + StartPopSize;
// Weight
ActorWeight = TotalPopSize / TotalPopSampleSize;
StartPopSampleSize = (long)(StartPopSize / ActorWeight);
ImmiPopSampleSize = TotalPopSampleSize - StartPopSampleSize;
//Scale Population
if (ScalePopulation && model_is_case_based)
{
SetPopulation(TotalPopSize);
}
};
7.9. Step 9: Micro-Data Output¶
7.9.1. Overview¶
This step adds micro-data output to the model, allowing the user to set a time when a micro-data file is written. All code is within the new module, MicroDataOutput.mpp, which can be added at any step of model development.
This step completes the micro-simulation implementation of a typical macro population projection model.
7.9.2. Concepts¶
Modgen functionality for micro-data output¶
Modgen includes some functionality to produce micro-data output in the form of CSV files.
For CSV output files, the file has to be declared and a parameter for the file name must be created:
output_csv out_csv; //EN Microdata output csv object
parameters
{
file MicroRecordFileName; //EN File name micro-data output file
};
The output file has to be opened before and closed after the simulation. This can be at the beginning and the end of the Simulation() function, or in the pre- and post- simulation.
// Open the output file
out_csv.open(MicroRecordFileName);
// Close the output file
out_csv.close();
Output records can be produced within a function or event:
// Push the fields into the output record.
out_csv << time_of_birth;
out_csv << (int)sex;
out_csv << (int)province_nat;
// All fields have been pushed, now write the record.
out_csv.write_record();
7.9.3. How to reproduce this modeling step¶
The full code for micro-population is contained in one file, which can be added at any point in model development. Output variables can also be edited and added at any point in model development.
The MicroDataOutput.mpp module¶
The following code is the full module:
// LABEL(MicroDataOutput, EN) Micro data output
/* NOTE(MicroDataOutput, EN)
Added at Step 8: This module can be added at any step of model development
This module implements micro data output written to a csv file.
Users can specify the time at which a micro-data file is written out and choose a file name.
The module can be switched on and off.
*/
output_csv out_csv; //EN Microdata output csv object
parameters
{
logical WriteMicrodata; //EN Write micro-data output file Y/N
double TimeMicroOutput; //EN Time of micro-data output
file MicroRecordFileName; //EN File name micro-data output file
};
parameter_group PG05_Files //EN Microdata output
{
WriteMicrodata, MicroRecordFileName, TimeMicroOutput
};
actor Person
{
TIME time_microdata_output = { TIME_INFINITE }; //EN Time for microdata output
void WriteMicroRecord_Start(); //EN Initialization for microdata output event
hook WriteMicroRecord_Start, Start;
event timeWriteMicroRecord, WriteMicroRecord; //EN Write micro-data record event
};
void Person::WriteMicroRecord_Start()
{
if (WriteMicrodata && TimeMicroOutput >= time) time_microdata_output = TimeMicroOutput;
else time_microdata_output = TIME_INFINITE;
}
TIME Person::timeWriteMicroRecord()
{
return time_microdata_output;
}
void Person::WriteMicroRecord()
{
// Push the fields into the output record.
out_csv << time_of_birth;
out_csv << (int)sex;
out_csv << (int)province_nat;
// All fields have been pushed, now write the record.
out_csv.write_record();
// do only once
time_microdata_output = TIME_INFINITE;
}
void PreSimulation()
{
if (WriteMicrodata) if (WriteMicrodata) { out_csv.open(MicroRecordFileName); }
}
void PostSimulation()
{
if (WriteMicrodata) { out_csv.close(); }
}
7.9.4. Model versions¶
Converting the model to a time-based and/or micro-data-based version¶
As in any modeling step in this guide, the case-based model can be converted to time-based by replacing the DYNAMIS-POP-MRT.mpp module (see Appendix for code) and adapting its settings in ModelSettings.mpp.
As in any step starting from Step 1, the starting population type can be converted from a sampling table to a micro-data file by replacing the starting population file (see Appendix for code) and adapting the settings in ModelSettings.mpp.
No changes affecting model conversions are made at this step, compared to the previous step. Note that model conversion is discussed in detail in Step 11: Conversion to a Time-Based Model.
7.10. Step 10: Micro-Data Input¶
7.10.1. Overview¶
In this step, we convert the starting population type of the model. Instead of sampling characteristics from a distributional table, we read in a micro-data file.
7.10.2. Concepts¶
Modgen functionality for micro-data input¶
Modgen includes some functionality to produce micro-data input in form of CSV files.
For CSV input files, the file has to be declared and a parameter for the file name must be created:
input_csv in_csv; //EN Microdata input csv object
parameters
{
file MicroDataInputFile; //EN File name of starting population
};
The fields of a record can be addressed by an index starting at 0, which is best handled creating a classification of field names:
classification PERSON_MICRODATA_COLUMNS //EN fields in the microdata input file
{
PMC_BIRTH, //EN Time of birth
PMC_SEX, //EN Sex
PMC_PROVINCE //EN Province
};
The input file has to be opened before and closed after the simulation. This can be at the beginning and the end of the Simulation() function, or in the pre- and post- simulation.
// Open the input file
in_csv.open(MicroDataInputFile);
// Close the input file
in_csv.close();
Input records can be read within a read function or event by a function that takes one argument, which is the line number of the record:
in_csv.read_record(i_lLine);
Once a record is read, its fields can be accessed by the index as defined in our classification:
time_of_birth = in_csv[PMC_BIRTH]; // time of birth
if ((int)in_csv[PMC_SEX] == 1) sex = MALE; else sex = FEMALE; // sex
province_nat = (PROVINCE_NAT)(int)in_csv[PMC_PROVINCE]; // province
Note that all values of the CSV file are read in as double and have to be casted to other types if required. In C++, this is done by prompting the variable with (NewType).
7.10.3. How to reproduce this modeling step¶
The full code for micro-population input is contained in one file—StartPopFile.mpp—which replaces the file StartPopSampling.mpp. This replacement of files and model type can also be performed at any previous step of model development. Besides this file replacement, a few adaptations in ModelSettings.mpp have to be made.
The StartPopFile.mpp module¶
The following code is the full module:
//LABEL(StartPopFile, EN) Starting population File
/* NOTE(StartPopFile, EN)
Added at step 9 replacing StartPopSampling.mpp
This replacement can be made at any previouse modeling step
This file replaces StartPopSampling.mpp and converts the model from a model that samples
starting characteristics to a model that reads in a micro-population file.
Varaibles are time of birth, sex, and region
*/
input_csv in_csv; //EN Microdata input csv object
classification PERSON_MICRODATA_COLUMNS //EN fields in the microdata input file
{
PMC_BIRTH, //EN Time of birth
PMC_SEX, //EN Sex
PMC_PROVINCE //EN Province
};
parameters
{
file MicroDataInputFile; //EN File name of starting population
};
parameter_group PG00_MicroDataInputFile //EN Starting Population file
{
MicroDataInputFile
};
actor Person
{
void GetStartCharacteristics(long i_lLine); //EN Read charcteristics at birth
};
void Person::GetStartCharacteristics(long i_lLine)
{
// Read record
in_csv.read_record(i_lLine);
// Assigns values from record
time_of_birth = in_csv[PMC_BIRTH]; // time of birth
if ((int)in_csv[PMC_SEX] == 1) sex = MALE; else sex = FEMALE; // sex
province_nat = (PROVINCE_NAT)(int)in_csv[PMC_PROVINCE]; // province
}
void PreSimulation()
{
in_csv.open(MicroDataInputFile);
}
void PostSimulation()
{
in_csv.close();
}
New settings in ModelSettings.mpp¶
Parameters: The only change in parameters is that the size of the starting population is changed from a model-generated parameter to a normal double parameter, as the model now cannot calculate the size of the starting population automatically.
parameters
{
[...]
double StartPopSize; //EN Starting population size
[...]
};
parameter_group PG00_ModelSettings //EN Model Settings
{
StartPopSize
};
In the PreSimulation() function, we accordingly have to remove the loop, which previously calculated the starting population size. This is the only required change in settings.
Below the full ModelSettings.mpp file.
The microdata file¶
At this point, we need a micro-data file in CSV format with the fields.
- Birth date: e.g. 1966.158
- Sex: 0 - Female, 1 - Male
- Province: 0 - 12
Such a file can be generated in the previous model version, which allows writing a micro-data file of this format. As model development moves beyond these three characteristics, such a file can be produced by statistical software.
7.11. Step 11: Conversion to a Time-Based Model¶
7.11.1. Overview¶
In this step, we convert to a time-based model, which will be the starting point for the second stage of model development, allowing for a broad range of additional features, like actor interactions and automatic model alignment to external targets.
7.11.2. Concepts¶
Time-Based Models¶
In time-based models, all persons are simulated simultaneously through time. While more resource intensive, such models allow for communication between all persons or any other entities in the simulation.
Technically, any case-based model can be implemented as a time-based model. Such a conversion can be done easily by replacing the simulation engine file—in our case the DYNAMIS-POP-MRT.mpp file—by an engine for time-based models. A template for such a file is provided here. Developers typically do not touch the code of the simulation engine file, so the same file can be used in all modeling steps.
Time-Based models have different scenario-setting parameters in the graphical user interface.
- A new parameter for the end of the simulation time. At this point in time the simulation is stopped automatically.
- Three parameters used so far are missing: the sample size, the total population size, and the box to check for automatic population scaling. If required, these parameters must be added as normal model parameters.
- Time-Based models do not allow for sub-samples, as all actors have to be able to communicate with each other. Instead, for the calculation of monte carlo variability in outputs, the user can define a number of model replicates created and Modgen automatically averages over the outcome results.
At this modeling step, we do not add any functionality to the model and accordingly do not make use yet of the added modeling possibilities.
7.11.3. How to reproduce this modeling step¶
To convert the model from case based to time based, the code in the simulation engine file has to be replaced. The code for a time-based model can be found in the Appendix.
New settings in ModelSettings.mpp¶
Parameters: As the user interface of the scenario settings of time-based models do not include parameters for sample and population sizes as well as population scaling, these parameters are changed into normal model parameters. This gives us added flexibility as to which population size parameters can be set by the user, and which to calculate based on these settings. We will use this flexibility, to let the user choose the size of the starting population file and the corresponding real size of the starting population. Based on the sampling table for immigrants, the sample and total size of the immigrant population, and also of the total population can be calculated automatically.
parameters
{
long StartPopSampleSize; //EN Sample size of starting population
double StartPopSize; //EN Total population size
model_generated long ImmiPopSampleSize; //EN Sample size of starting population
model_generated double ImmiPopSize; //EN Total population size
model_generated long TotalPopSampleSize; //EN Sample size of starting population
model_generated double TotalPopSize; //EN Total population size
logical ScalePopulation; //EN Scale population
model_generated double ActorWeight; //EN Actor Weight
};
parameter_group PG_ModelSettings //EN Model Settings
{
StartPopSize, StartPopSampleSize, ScalePopulation
};
In the PreSimulation() function, we accordingly have to change how and which parameters are calculated. The actor weight can now be calculated from the starting population information. Given the new parameters, the total population sample size can now be calculated automatically and becomes a model-generated parameter.
void PreSimulation() { // Weight ActorWeight = StartPopSize / StartPopSampleSize;
// Total Immigrant population
ImmiPopSize = 0.0;
if (ModelImmigration) // If immigration is switched on
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nYear = 0; nYear < SIZE(SIM_YEAR_RANGE); nYear++)
{
ImmiPopSize = ImmiPopSize + NumberImmigrants[nYear][nSex];
NumberImmigrantsTable[nYear][nSex] = NumberImmigrants[nYear][nSex];;
}
}
}
//Total population
TotalPopSize = ImmiPopSize + StartPopSize;
TotalPopSampleSize = (long)(TotalPopSize / ActorWeight);
ImmiPopSampleSize = TotalPopSampleSize - StartPopSampleSize;
};
7.11.4. Discussion: Model type conversions¶
This discussion contains the code of alternative model versions.
At any step 0-8, the model can be converted from case based to time based by replacing the model engine (the code in DYNAMIS-POP-MRT.mpp).
At any step 1-9, the model can be converted to a model reading in a starting population file instead of sampling starting characteristics from a distributional table. This is done by replacing the starting population module.
Note that some settings in ModelSettings.mpp have to be modified when changing the model type. The model settings file changes by modeling step and its alternative versions are documented at each step.
The DYNAMIS-POP-MRT.mpp file for time-based models (all steps)¶
The full code of DYNAMIS-POP-MRT.mpp for time-based models is detailed below. Note that this code will not be changed in the following modeling steps. Model developers do not need to modify or understand this code.
//LABEL(DYNAMIS-POP-MRT, EN) Simulation Engine for a time-based model
/* NOTE(DYNAMIS-POP-MRT, EN)
This module is part of the time-based Template at step 0
This module is the simulation engine for a time-based model.
It contains core simulation functions and definitions.
When starting from a template (or using the Modgen wizard), model developers typically do
not have to modify (or understand) the code of this file.
The file contains the definition of the model type: in our case a time-based continuouse time model
The second part consists of the functions handling the simulation
Case-based versions of DYNAMIS-POP-MRT can be changed into a time-based model by exchanging this file resp.
by copy-pasting the code of this file into the DYNAMIS-POP-MRT.mpp file replacing the code.
The code contains one parameter - StartingPopulationSize - which is necessary in time-based models
(as it and not part of scenario-settings as in case-based models).
Note that this parameter has to be added to the .dat file when changing a case-based model into a time-based one
by adding: int StartingPopulationSize = 1000;
*/
// Definition of basic model characteristics
version 1, 0, 0, 0; // The model version number
model_type time_based, just_in_time; // The model type
time_type double; // The data type used to represent time
options packing_level = 2; // An option reducing memory use at the expense of speed
// Other data types
real_type float;
counter_type ushort;
integer_type short;
index_type ulong;
// Supported languages
languages
{
EN // English
};
// Model strings
string S_MODEL_FINISH; //EN Finish
string S_MODEL_REPLICATE; //EN Replicate
string S_MODEL_SIMULATION; //EN Simulation
string S_MODEL_START; //EN Start
// Parameters
parameters
{
model_generated logical model_is_case_based; //EN Model is case based
};
void PreSimulation()
{
model_is_case_based = FALSE;
};
// The Simulation function is called by Modgen to simulate a replicate.
void Simulation()
{
// Buffer for reporting progress
const size_t nBufSize = 255;
TCHAR szBuffer[nBufSize];
// Note replicate number for progress reporting
int nReplicate = GetReplicate();
// Create the starting population
_stprintf_s(szBuffer, nBufSize, _T("%s %d: %s"), ModelString("S_MODEL_REPLICATE"), nReplicate, ModelString("S_MODEL_START"));
ProgressMessage( szBuffer );
for ( long nJ = 0; nJ < TotalPopSampleSize; nJ++ )
{
Person *paPerson = new Person();
paPerson->Start(nJ,NULL );
}
_stprintf_s(szBuffer, nBufSize, _T("%s %d: %s"), ModelString("S_MODEL_REPLICATE"), nReplicate, ModelString("S_MODEL_SIMULATION"));
ProgressMessage( szBuffer );
// event loop
double dCurrentTime = TIME_INFINITE;
double dStartTime = TIME_INFINITE;
int nLastProgressPercent = -1;
int nThisProgressPercent = -1;
while ( !gpoEventQueue->Empty() )
{
// get the time of next event, verify against the simulation end
dCurrentTime = gpoEventQueue->NextEvent();
// Note the start time (time of first event) for progress indicator
if ( dStartTime == TIME_INFINITE )
{
dStartTime = dCurrentTime;
}
if ( dCurrentTime > SIMULATION_END() || gbInterrupted || gbCancelled || gbErrors )
{
if (dCurrentTime > SIMULATION_END())
{
// age all actors to the simulation end time
gpoEventQueue->WaitUntilAllActors( SIMULATION_END() );
}
_stprintf_s(szBuffer, nBufSize, _T("%s %d: %s"), ModelString("S_MODEL_REPLICATE"), nReplicate, ModelString("S_MODEL_FINISH"));
ProgressMessage( szBuffer );
gpoEventQueue->FinishAllActors();
}
else
{
// age all actors to the time of next event
gpoEventQueue->WaitUntil( dCurrentTime );
// implement the next event
gpoEventQueue->Implement();
}
// Update progress indicator only if the integer percentage complete changes
// (updates to the progress bar at every event are expensive).
nThisProgressPercent = (int)( 100 * ( dCurrentTime - dStartTime ) /
( SIMULATION_END() - dStartTime ) );
if ( nThisProgressPercent > nLastProgressPercent )
{
TimeReport( dCurrentTime ); // update simulation progress
nLastProgressPercent = nThisProgressPercent;
}
}
}
The StartPopFile.mpp module for reading in a starting file¶
The file StartPopFile.mpp replaces the file StartPopSampling.mpp (which has to be removed). The code of this file is discussed at Step 9 when preparing the base model for model extensions requiring a time-based model with a starting population file.
//LABEL(StartPopFile, EN) Starting population File
/* NOTE(StartPopFile, EN)
Added at step 9 replacing StartPopSampling.mpp
This replacement can be made at any previouse modeling step
This file replaces StartPopSampling.mpp and converts the model from a model that samples
starting characteristics to a model that reads in a micro-population file.
Varaibles are time of birth, sex, and region
*/
input_csv in_csv; //EN Microdata input csv object
classification PERSON_MICRODATA_COLUMNS //EN fields in the microdata input file
{
PMC_BIRTH, //EN Time of birth
PMC_SEX, //EN Sex
PMC_PROVINCE //EN Province
};
parameters
{
file MicroDataInputFile; //EN File name of starting population
};
parameter_group PG00_MicroDataInputFile //EN Starting Population file
{
MicroDataInputFile
};
actor Person
{
void GetStartCharacteristics(long i_lLine); //EN Read charcteristics at birth
};
void Person::GetStartCharacteristics(long i_lLine)
{
// Read record
in_csv.read_record(i_lLine);
// Assigns values from record
time_of_birth = in_csv[PMC_BIRTH]; // time of birth
if ((int)in_csv[PMC_SEX] == 1) sex = MALE; else sex = FEMALE; // sex
province_nat = (PROVINCE_NAT)(int)in_csv[PMC_PROVINCE]; // province
}
void PreSimulation()
{
in_csv.open(MicroDataInputFile);
}
void PostSimulation()
{
in_csv.close();
}
7.12. Step 12: Sampling from a detailed starting population¶
7.12.1. Overview¶
In this step, we replace the starting population file with its final version. The file has more variables and also includes a weight variable. File size and sample size do not have to correspond; if the file size is lager than the selected sample size, records are sampled. If the sample size exceeds the file size, observations are cloned.
7.12.2. Concepts¶
In this step, we add a new actor type, “Observation.” Observations correspond with (sampled) file records and have integer weights. Some of the added functionality of time-based models is introduced.
Adding a new actor type¶
Models can have any number of actor types. New actors are introduced simply by an actor statement block, containing actor states and at least the two functions, Start() and Finish(). For example, the minimum code for adding an actor Observation is:
actor Observation //EN Actor Observations
{
void Start(); //EN Starts the actor
void Finish(); //EN Destroys the actor
};
// Implementation
void Observation::Start()
{
// code can be added here
}
void Observation::Finish()
{
// code can be added here
}
New actors can be created by other actors in events (e.g., births are created by mothers) or can be created by the simulation engine at the start of the simulation.
Code example from a Simulation() function, reading in a file, and creating an actor for each record, thereby passing the current record object as a parameter:
// Create observations
for (long nJ = 0; nJ < MicroDataInputFileSize; nJ++)
{
in_csv.read_record(nJ);
Observation *paObservation = new Observation();
paObservation->Start(in_csv);
}
Actor Sets¶
One of the most powerful concepts available in time-based modes are record sets, a collection of actors of one type (e.g., persons) that is maintained automatically. Record sets can have dimensions, e.g., age or province of residence. At any change of individual characteristics (e.g., birthdays, migration), the change in group membership is updated automatically. Actor sets can contain filters, making membership dependent on individual charactersitics. When characteristics change, the membership is updated automatically. Actor sets can be ordered.
//EN All Observation actors
actor_set Observation asAllObservations;
//EN Persons by age and sex
actor_set Person asPersonsByAgeAndSex[integer_age][sex]
//EN 8 year old persons by education of mother
actor_set Person asPersonsAge08[educ_mother] filter integer_age == 8;
//EN Persons eligible for marriage by sex, ordered by income
actor_set Person asEligibleOrdered[sex] filter wants_to_marry order income;
Actor sets are very useful to find out how many actors (with specific characteristics) exist, and to find a specific (e.g. the first) or a random actor of the set.
// How many observations are available?
int nNumberObservations = asAllObservations->Count();
// Create a pointer to a random observation:
Observation * prRandomObservation = asAllObservations->GetRandom(RandUniform(2));
// Create a pointer to the first observation:
Observation * prFirstObservation = asAllObservations->Item(0);
// Use the pointer to access a state of the actor
time_of_birth = prRandomObservation->obs_birth;
// Kill the first actor of the set
prFirstObservation->Finish();
7.12.3. How to reproduce this modeling step¶
File modifications and extensions are made in three files, most importantly in StartPopFile.mpp, where the new actor Observation is implemented and used. The simulation engine DYNAMIS-POP-MRT.mpp is changed to create observation actors from the input file. We also change one line in the Start() function in PersonCore.mpp.
Changes to file operations for allowing multi-threading¶
So far, all file input operations (i.e., creating the file object, opening the input file, reading records, and closing the file) were contained in the StartPopFile.mpp module. To allow multi-threading, or running multiple replicas of the same model simultaneousely, we move this code into the Simulation() function, which automatically handles these issues.
void Simulation()
{
// Create and open the input data file
input_csv in_csv; // Microdata input csv object
in_csv.open(MicroDataInputFile); // Open the microdata input file
[...]
// Close teh microdata input file
in_csv.close();
}
Input file variables and file lenght (StartPopFile.mpp)¶
We extend the classification to contain the full set of variables of the model.
classification PERSON_MICRODATA_COLUMNS //EN fields in the microdata input file
{
PMC_WEIGHT, //EN Weight
PMC_BIRTH, //EN Time of birth
PMC_SEX, //EN Sex
PMC_PROVINCE, //EN Province
PMC_EDUC, //EN Education
PMC_POB, //EN Province of birth
PMC_UNION, //EN Union formation time
PMC_PARITY, //EN Number of births
PMC_LASTBIR //EN Time of last birth
};
Also, as we allow for weighting and do not require correspondence between sample size and input file length, we add two parameters:
parameters
{
file MicroDataInputFile; //EN File name of starting population
long MicroDataInputFileSize; //EN File size of starting population
logical UseWeightsInFile; //EN Use weights in starting population file
};
parameter_group PG00_MicroDataInputFile //EN Starting Population file
{
MicroDataInputFile, MicroDataInputFileSize,
UseWeightsInFile
};
Accordingly, this code has to be removed from the StartPopFile.mpp module, and the whole PreSimulation() and PostSimulation() functions become obsolete and can be removed.
The new actor type Observation (StartPopFile.mpp)¶
“Observation” can be created just by an actor code block of its name, containing its states and functions. States correspond to the fields of the input file. The Start() function takes a record from the file as input parameter. Additionally, we create a state for the integer weight, which will contain information as to how many Person actors corresponding with an observation should be created. The Function WeightOrKill() determines the integer weight and removes the observation if the weight is 0, i.e., the observation is not needed.
actor Observation //EN Actor Observations
{
// values from file
double pmc[PERSON_MICRODATA_COLUMNS]; //EN Person micro record columns
// states
integer int_weight = { 1.0 }; //EN Integer Weight
// functions
void Start(const input_csv& input); //EN Starts the actor
void Finish(); //EN Destroys the actor
void WeightOrKill(); //EN Decides if an observation stays in sample and how often
};
The Start() function simply initializes the states of the Observation; the implementation of Finish() is empty.
void Observation::Start(const input_csv& in_csv)
{
for (int nJ = 0; nJ < SIZE(PERSON_MICRODATA_COLUMNS); nJ++)
{
pmc[nJ] = in_csv[nJ];
}
};
void Observation::Finish(){};
The function WeightOrKill() calculates integer weights for each record in the file. Based on the non-integer part of the weight, a random number is used to decide if weights are rounded up or down. Actors with a resulting integer weight 0 are removed. Weights add up to the sample size parameter.
void Observation::WeightOrKill()
{
double dWeight = 0.0;
int nWeight = 0;
if (!UseWeightsInFile) dWeight = (double)StartPopSampleSize / (double)MicroDataInputFileSize;
else dWeight = pmc[PMC_WEIGHT] * (double)StartPopSampleSize / StartPopSize;
nWeight = (int)dWeight;
dWeight = dWeight - nWeight;
if (RandUniform(3) < dWeight) nWeight++;
int_weight = nWeight;
if (int_weight == 0) Finish();
}
To be able to sample from the available observations, we create an actor set of all observations.
actor_set Observation asAllObservations; //EN All Observations
Using the new Observation actor for sampling starting characteristics (StartPopFile.mpp)¶
As the final change in the module StartPopFile.mpp, we modify the existing GetStartCharacteristics() function, which no longer takes a parameter. Instead of reading a record with a given number, we now take an observation from the actor set asAllObservations.
The starting characteristics of the Person actor are now initialized using the selected Observation actor.
Once the Observation is “used,” its integer weight is reduced by one, or, if the weight is one, the observation is removed automatically from the Actor Set by calling its Finish() function. Note that as the weighted number of records corresponds with the sample size, we could have taken the first Observation actor of the set instead of sampling.
actor Person
{
void GetStartCharacteristics(); //EN Get charcteristics at birth
};
void Person::GetStartCharacteristics()
{
// Sample an observation
Observation * prObservation = asAllObservations->GetRandom(RandUniform(2));
// Assigns values from observation
time_of_birth = prObservation->pmc[PMC_BIRTH];
if ((int)prObservation->pmc[PMC_SEX] == 1) sex = MALE; else sex = FEMALE;
province_nat = (PROVINCE_NAT)(int)prObservation->pmc[PMC_PROVINCE];
// Decrement the integer weight of the observation actor and kill it if not needed anymore
prObservation->int_weight--;
if (prObservation->int_weight <= 0 ) prObservation->Finish();
}
Changes in the Simulation() function (DYNAMIS-POP-MRT.mpp)¶
After opening the input file, and before creating the Person actors, we create the Observation actors by looping through the micro-data input file. After creating an observation, we decide if and how often it is to be used by setting an integer weight. This is done by calling WeightOrKill(), which also removes an actor and releases its memory space immediately if it is not used.
// Create observations
for (long nJ = 0; nJ < MicroDataInputFileSize; nJ++)
{
in_csv.read_record(nJ);
Observation *paObservation = new Observation();
paObservation->Start(in_csv);
paObservation->WeightOrKill();
}
Changes in the Start() function¶
Because a random element is contained in the decision if and how often each observation in the starting population file is used, the resulting starting population size based on integer-weighted observations becomes a random variable that is very close, but not necessarily equal to, the StartPopSampleSize Parameter. To decide if an actor stems from the starting population, we replace the condition “PersonNr < StartPopSampleSize” with “asAllObservations->Count() > 1” to ensure we did not run out of observations.
else if (asAllObservations->Count() > 1) // Person from start population
{
person_type = PT_START;
}
Note that the PersonNr as a parameter of the Start() function becomes obsolete and can be removed. Also, as an actor set is used, it is not possible to convert back to a case-based model and the code could be cleaned up for removing case-based specific code.
7.13. Step 13: Primary Education¶
7.13.1. Overview¶
This module implements primary education. Users can specify entry and graduation ages, as well as probabilities to enter and to graduate by year of birth, sex, and province of birth. Optionally, proportional factors can be specified accounting for mother’s education. If these factors are used, the outcome calibrates itself for a selected year to the initial outcome.
The primary education system is implemented as a “PrimarySchool” actor, who at the change of each school year selects potential students for entering school for graduation.
7.13.2. Concepts¶
This step provides a rich set of examples as to the use of actor sets and the communication between two types of actors: individual persons and an actor representing the primary school system.
- Efficiency: Avoiding large numbers of individual level events happening at the same time.
- Data imputation: Sampling characteristics from a donor population.
- Alignment of aggregated results respecting relative differences in individual probabilities.
Gaining efficiency by avoiding large numbers of individual level events happening at the same time¶
In this step, we add a new actor type, “PrimarySchool.” This actor is predominantly introduced for efficiency gains. Instead of each person individually scheduling school entry and graduation events all happening at the same time each year, one single central actor calls a school year change event. The PrimarySchool actor loops through the population of all people eligible for school entry and graduation and calls functions on the individual level to decide school transitions. The eligible population is automatically maintained by actor sets.
The following is a simplified example of how school entry can be modeled in such a way:
//EN Primary education entry age cohort
actor_set Person asPrimaryEntryCohort filter integer_age == AgeEnterPrimary;
// A function to be called at each school year change
void PrimarySchool::EnterPrimary()
{
// loop through all eligible actors
for (long nJ = 0; nJ < asPrimaryEntryCohort->Count(); nJ++)
{
Person * prPerson = new Person();
prPerson = asAllPersons->Item(nJ);
prPerson->AnIndividualLevelFunctionToDecideIndividualSchoolEntry();
}
}
Sampling characteristics from a donor population¶
Actor sets are a very powerful and straightforward mechanism for sampling characteristics from a donor population, for example, for imputing missing information by sampling from a population of people with similar characteristics. An example from this modeling step is the imputation of education of new immigrants if, at the moment of immigration, they are beyond graduation age and born before the year of birth range for which education is modeled in the application.
[...]
// (c) sample from other foreign born of same age, sex and province of residence
// if no foreignborn with same characteristics available, sample from all
// residents of same age and sex if no donor is found, assign education randomly
else
{
Person * prPerson = new Person;
int nPopSize = asForeignersAgeSexProv[integer_age][sex][province_nat]
->Count();
if (nPopSize > 0)
{
prPerson = asForeignersAgeSexProv[integer_age][sex][province_nat]
->GetRandom(RandUniform(19));
}
else
{
nPopSize = asResidentsAgeSex[integer_age][sex]->Count();
if (nPopSize > 0) prPerson = asResidentsAgeSex[integer_age][sex]
->GetRandom(RandUniform(20));
}
// a donor was found: assign donor's primary entry level
if (nPopSize > 0 && prPerson->primary_level != PL_NO) primary_level = PL_ENTER;
// no donor was found; depending on samlpe size this should never or rarely happen
else if (RandUniform(21) < 0.5) primary_level = PL_ENTER;
}
Aligning model results¶
One of the strengths of time-based models is the ability to align aggregated output to external targets, while respecting relative differences in probabilities or risks. This is used at this step to allow the introduction of additional relative factors—log odds for the influence of mother’s education—without changing the aggregated output for the year of introduction. By aligning results this way, a calibration factor is derived and applied for the following years.
The following illustration is the PrimarySchoolEntry() function introduced at this modeling step. This function is called by the Primary School Actor every year to select children to enroll in primary school. The user can choose two model variants, one with and one without accounting for mother’s education. In the latter case, the outcome is aligned to the initial outcome for the first year in which mother’s education is introduced:
void PrimarySchool::PrimarySchoolEntry()
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_INT); nProv++)
{
// Size of relevant sub-populations
int nMotherEd0 = asPrimaryEntryCohort[nSex][nProv][PL_NO]->Count();
int nMotherEd1 = asPrimaryEntryCohort[nSex][nProv][PL_ENTER]->Count();
int nMotherEd2 = asPrimaryEntryCohort[nSex][nProv][PL_GRAD]->Count();
int nPerson = nMotherEd0 + nMotherEd1 + nMotherEd2;
// Calculate probabilities with all adjustments
if (nPerson > 0)
{
double dCalibrationFactor = 0.0;
double dMotherFactorEduc0 = 0.0;
double dMotherFactorEduc1 = 0.0;
double dMotherFactorEduc2 = 0.0;
double dCenter;
// overall probability if not accounting for mother seducation
double dProb =
StartPrimary[nSex][RANGE_POS(YOB_START_PRIMARY, school_year - AgeEnterPrimary)][nProv];
// account for mothers education and calibrate results in year in which introduced
if (UseMothersEducation && school_year >= CalibratedYear + AgeEnterPrimary)
{
dMotherFactorEduc0 = log(OddsMotherEnterPrimary[nSex][PL_NO]);
dMotherFactorEduc1 = log(OddsMotherEnterPrimary[nSex][PL_ENTER]);
dMotherFactorEduc2 = log(OddsMotherEnterPrimary[nSex][PL_GRAD]);
// calibration year
if (school_year == CalibratedYear + AgeEnterPrimary)
{
double dWeight0 = (double)nMotherEd0 / (double)nPerson;
double dWeight1 = (double)nMotherEd1 / (double)nPerson;
double dWeight2 = (double)nMotherEd2 / (double)nPerson;
int nIterations = 0;
double dResult = 500.0;
double dTarget = dProb;
double dLower = -10.0;
double dUpper = 10.0;
while (abs(dResult - dTarget) > 0.0001 && nIterations < 10000)
{
nIterations++;
dCenter = (dLower + dUpper) / 2.0;
dResult = dWeight0 * AdjustedProbability(dProb, dMotherFactorEduc0, dCenter)
+ dWeight1 * AdjustedProbability(dProb, dMotherFactorEduc1, dCenter)
+ dWeight2 * AdjustedProbability(dProb, dMotherFactorEduc2, dCenter);
if (dTarget > dResult) dLower = dCenter;
else dUpper = dCenter;
}
dCalibrationFactor = dCenter;
LastProbabilityPrimaryEntry[nSex][nProv] = dProb;
CalibrationFactorPrimaryEntry[nSex][nProv] = dCalibrationFactor;
}
// after calibrated year
else
{
dCalibrationFactor = CalibrationFactorPrimaryEntry[nSex][nProv];
dProb = LastProbabilityPrimaryEntry[nSex][nProv];
}
}
// decide for each person in entry cohort, if entering school
for (int nJ = 0; nJ < nMotherEd0; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_NO]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc0, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd1; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_ENTER]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc1, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd2; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_GRAD]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc2, dCalibrationFactor));
}
}
}
}
7.13.3. How to reproduce this modeling step¶
At this step, we add a module PrimaryEducation.mpp. Changes in other modules are very minor.
The PrimaryEducation.mpp module¶
Actor set and type definitions¶
//EN Primary education entry age cohort
actor_set Person asPrimaryEntryCohort[sex][province_birth][educ_mother]
filter set_alive && integer_age == AgeEnterPrimary;
//EN Primary education graduation age cohort
actor_set Person asPrimaryGraduationCohort[sex][province_birth][educ_mother]
filter set_alive && integer_age == AgeGradPrimary && primary_level != PL_NO;
//EN All Persons
actor_set Person asAllPersons filter set_alive;
//EN Foreign born by age, sex and province
actor_set Person asForeignersAgeSexProv[integer_age][sex][province_nat]
filter set_alive && province_birth == PI_PROV13;
//EN Residents by age, sex and province
actor_set Person asResidentsAgeSex[integer_age][sex] filter set_alive;
//EN Foreign born who at least entered primary education by age, sex and province
actor_set Person asForeignersPrimAgeSexProv[integer_age][sex][province_nat]
filter set_alive && province_birth == PI_PROV13 && primary_level != PL_NO;
// EN Residents who at least entered primary education
actor_set Person asResidentsPrimAgeSex[integer_age][sex] filter set_alive && primary_level != PL_NO;
classification PRIMARY_LEVEL //EN Primary Education level
{
PL_NO, //EN Never entered primary school
PL_ENTER, //EN Entered primary school
PL_GRAD //EN Graduated from primary school
};
range YOB_START_PRIMARY { 2001, 2063 }; //EN Year of birth
range YOB_GRAD_PRIMARY { 1997, 2063 }; //EN Year of birth
Model parameters¶
The primary school system is parameterized by a school entry age, a graduation age, and the time in the calendar year, a new school year starts. School entry and graduation is decided according to probabilities by year of birth, province of birth, and sex. Additionally, odds ratios by mothers’ education can be specified. Their use is optional, when used, results are aligned to the results without mothers’ education for a selected year at which this additional variable is introduced.
parameters
{
int AgeEnterPrimary; //EN School entry age primary
int AgeGradPrimary; //EN Graduation age primary
double StartOfSchoolYear; //EN Start at school year (e.g. 0.5 = July)
double StartPrimary[SEX][YOB_START_PRIMARY][PROVINCE_INT]; //EN Probability to start primary school
double GradPrimary[SEX][YOB_GRAD_PRIMARY][PROVINCE_INT]; //EN Probability to graduate from primary school
double OddsMotherEnterPrimary[SEX][PRIMARY_LEVEL]; //EN Odds by mother's education: primary
double OddsMotherGradPrimary[SEX][PRIMARY_LEVEL]; //EN Odds by mother's education: secondary
int CalibratedYear; //EN Calibrated year introducing mothers education
logical UseMothersEducation; //EN Use mother's education
};
Personal level states and functions¶
The key state modeled in this module is the primary school level. There are functions to decide school entry and graduation for given probabilities and a function to initialize education of immigrants at the moment they enter the country.
actor Person
{
PRIMARY_LEVEL primary_level = { PL_NO }; //EN Primary school status
PRIMARY_LEVEL educ_mother = { PL_NO }; //EN Mothers primary school status
void DecidePrimaryEntry(double dProb); //EN Decides & sets primary school entry
void DecidePrimaryGrad(double dProb); //EN Decides & sets primary graduation
void SetImmigrantPrimaryEducationStatus(); //EN Assign primary education at immigration
hook SetImmigrantPrimaryEducationStatus, SetAliveEvent;
};
The actor Primary School definitions and basics: Start, Finish, School year change¶
As all actors, the primary school actor is declared in an actor code block containing its states and functions. Mandatory functions are Start() and Finish(). The actor is created at the start of the simulation. The actor has one single event—the change of school year—in which two key functions of this module are called PrimarySchoolEntry() and PrimarySchoolGraduation().
actor PrimarySchool //EN Primary School Actor
{
int school_year = { 0 }; //EN Current school year
TIME next_school_year = { TIME_INFINITE }; //EN Next school year
TIME assign_primary_time = { TIME_INFINITE }; //EN Initial assignment of primary status
double CalibrationFactorPrimaryEntry[SEX][PROVINCE_INT]; //EN Calibration factor primary entry
double LastProbabilityPrimaryEntry[SEX][PROVINCE_INT]; //EN Last primary entry probability
double CalibrationFactorPrimaryGrad[SEX][PROVINCE_INT]; //EN Calibration factor primary graduation
double LastProbabilityPrimaryGrad[SEX][PROVINCE_INT]; //EN Last primary entry probability
event timeNextSchoolYearEvent, NextSchoolYearEvent; //EN Next school year event
event timeAssignInitialPrimary, AssignInitialPrimary; //EN Assigns initial school level at start
void PrimarySchoolEntry(); //EN Decides who enters primary education
void PrimarySchoolGraduation(); //EN Decides who graduates from primary education
//EN Calculate adjusted probability adding relative factors
double AdjustedProbability(double dProb, double dLogOddEduc, double dLogOddAdjust);
void Start(); //EN Start the primary school actor
void Finish(); //EN Destroy the primary school actor
};
void PrimarySchool::Start()
{
time = MIN(SIM_YEAR_RANGE); // Create at start of simulation
school_year = MIN(SIM_YEAR_RANGE) - 1; // Still 'old' school year
next_school_year = WAIT(0.5); // Next school year starts mid year
assign_primary_time = WAIT(0.0); // Assign initial proiary status in population
};
void PrimarySchool::Finish(){}
TIME PrimarySchool::timeNextSchoolYearEvent()
{
return next_school_year;
}
/* NOTE(PrimarySchool.NextSchoolYearEvent, EN)
The primary school actor has just this one event, the change of school year.
At each school year change, a selection is made which potential students enter
primary education, and which students graduate.
*/
void PrimarySchool::NextSchoolYearEvent()
{
school_year++; // update school year
PrimarySchoolEntry(); // decide who enters primary school
PrimarySchoolGraduation(); // decide who graduates from primary school
next_school_year = WAIT(1.0); // set clock for next school year
}
Initialization of the primary education status in the starting population¶
This event assigns the initial primary education status at the beginning of the simulated time period. For given parameters of school entry and graduation age, values in the file are ignored and reset (if the person is too young for having entered/graduated from primary school), reassigned by models (if the recorded outcome of schooling is not necessarily the final outcome), or kept.
void PrimarySchool::AssignInitialPrimary()
{
// loop through all actors
for (long nJ = 0; nJ < asAllPersons->Count(); nJ++)
{
Person * prPerson = new Person();
prPerson = asAllPersons->Item(nJ);
// Entry to primary education
// (a) persons below school entry age reset education on file to non
if (prPerson->age < AgeEnterPrimary + StartOfSchoolYear)
{
prPerson->primary_level = PL_NO;
}
// (b) primary education: modeled if yob in parameterized range
else if (prPerson->year_of_birth >= MIN(YOB_START_PRIMARY))
{
prPerson->primary_level = PL_NO; // reset
if (RandUniform(8) < StartPrimary[prPerson->sex]
[RANGE_POS(YOB_START_PRIMARY, prPerson->year_of_birth)][prPerson->province_birth])
prPerson->primary_level = PL_ENTER;
}
// (c) old enought: keep primary education from file (do nothing)
else {}
// Graduate from primary education
// (a) persons below school graduation age reset graduations on record
if (prPerson->age < AgeGradPrimary + StartOfSchoolYear && prPerson->primary_level == PL_GRAD)
{
prPerson->primary_level = PL_ENTER;
}
// (b) primary education graduation: modeled if yob in parameterized range
else if (prPerson->age >= AgeGradPrimary + StartOfSchoolYear
&& prPerson->year_of_birth >= MIN(YOB_GRAD_PRIMARY) && prPerson->primary_level != PL_NO)
{
prPerson->primary_level = PL_ENTER; // reset
if (RandUniform(15) < GradPrimary[prPerson->sex]
[RANGE_POS(YOB_GRAD_PRIMARY, prPerson->year_of_birth)][prPerson->province_birth])
prPerson->primary_level = PL_GRAD;
}
// (c) old enought: keep primary education from file (do nothing)
else {}
}
assign_primary_time = TIME_INFINITE; //do just once
}
Primary school entry¶
This function is called by the Primary School Actor every year to select children to enroll in primary school. The user can choose two model variants, one with and one without accounting for mother’s education. In the latter case, the outcome is aligned to the initial outcome for the first year in which mother’s education is introduced.
void PrimarySchool::PrimarySchoolEntry()
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_INT); nProv++)
{
// Size of relevant sub-populations
int nMotherEd0 = asPrimaryEntryCohort[nSex][nProv][PL_NO]->Count();
int nMotherEd1 = asPrimaryEntryCohort[nSex][nProv][PL_ENTER]->Count();
int nMotherEd2 = asPrimaryEntryCohort[nSex][nProv][PL_GRAD]->Count();
int nPerson = nMotherEd0 + nMotherEd1 + nMotherEd2;
// Calculate probabilities with all adjustments
if (nPerson > 0)
{
double dCalibrationFactor = 0.0;
double dMotherFactorEduc0 = 0.0;
double dMotherFactorEduc1 = 0.0;
double dMotherFactorEduc2 = 0.0;
double dCenter;
// overall probability if not accounting for mother seducation
double dProb =
StartPrimary[nSex][RANGE_POS(YOB_START_PRIMARY, school_year - AgeEnterPrimary)][nProv];
// account for mothers education and calibrate results in year in which introduced
if (UseMothersEducation && school_year >= CalibratedYear + AgeEnterPrimary)
{
dMotherFactorEduc0 = log(OddsMotherEnterPrimary[nSex][PL_NO]);
dMotherFactorEduc1 = log(OddsMotherEnterPrimary[nSex][PL_ENTER]);
dMotherFactorEduc2 = log(OddsMotherEnterPrimary[nSex][PL_GRAD]);
// calibration year
if (school_year == CalibratedYear + AgeEnterPrimary)
{
double dWeight0 = (double)nMotherEd0 / (double)nPerson;
double dWeight1 = (double)nMotherEd1 / (double)nPerson;
double dWeight2 = (double)nMotherEd2 / (double)nPerson;
int nIterations = 0;
double dResult = 500.0;
double dTarget = dProb;
double dLower = -10.0;
double dUpper = 10.0;
while (abs(dResult - dTarget) > 0.0001 && nIterations < 10000)
{
nIterations++;
dCenter = (dLower + dUpper) / 2.0;
dResult = dWeight0 * AdjustedProbability(dProb, dMotherFactorEduc0, dCenter)
+ dWeight1 * AdjustedProbability(dProb, dMotherFactorEduc1, dCenter)
+ dWeight2 * AdjustedProbability(dProb, dMotherFactorEduc2, dCenter);
if (dTarget > dResult) dLower = dCenter;
else dUpper = dCenter;
}
dCalibrationFactor = dCenter;
LastProbabilityPrimaryEntry[nSex][nProv] = dProb;
CalibrationFactorPrimaryEntry[nSex][nProv] = dCalibrationFactor;
}
// after calibrated year
else
{
dCalibrationFactor = CalibrationFactorPrimaryEntry[nSex][nProv];
dProb = LastProbabilityPrimaryEntry[nSex][nProv];
}
}
// decide for each person in entry cohort, if entering school
for (int nJ = 0; nJ < nMotherEd0; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_NO]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc0, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd1; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_ENTER]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc1, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd2; nJ++) asPrimaryEntryCohort[nSex][nProv][PL_GRAD]->Item(nJ)->
DecidePrimaryEntry(AdjustedProbability(dProb, dMotherFactorEduc2, dCalibrationFactor));
}
}
}
}
Primary school graduation¶
This function is called by the Primary School Actor every year to select current primary students to graduate in primary school. If accounting for mother’s education, the outcome is aligned to the initial outcome for the first year in which mother’s education is introduced.
void PrimarySchool::PrimarySchoolGraduation()
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
for (int nProv = 0; nProv < SIZE(PROVINCE_INT); nProv++)
{
// Size of relevant sub-populations
int nMotherEd0 = asPrimaryGraduationCohort[nSex][nProv][PL_NO]->Count();
int nMotherEd1 = asPrimaryGraduationCohort[nSex][nProv][PL_ENTER]->Count();
int nMotherEd2 = asPrimaryGraduationCohort[nSex][nProv][PL_GRAD]->Count();
int nPerson = nMotherEd0 + nMotherEd1 + nMotherEd2;
// Calculate probabilities with all adjustments
if (nPerson > 0)
{
double dCalibrationFactor = 0.0;
double dMotherFactorEduc0 = 0.0;
double dMotherFactorEduc1 = 0.0;
double dMotherFactorEduc2 = 0.0;
double dCenter;
// overall probability if not accounting for mother seducation
double dProb =
GradPrimary[nSex][RANGE_POS(YOB_GRAD_PRIMARY, school_year - AgeGradPrimary)][nProv];
if (UseMothersEducation && school_year >= CalibratedYear + AgeGradPrimary)
{
dMotherFactorEduc0 = log(OddsMotherGradPrimary[nSex][PL_NO]);
dMotherFactorEduc1 = log(OddsMotherGradPrimary[nSex][PL_ENTER]);
dMotherFactorEduc2 = log(OddsMotherGradPrimary[nSex][PL_GRAD]);
// calibration year
if (school_year == CalibratedYear + AgeGradPrimary)
{
double dWeight0 = (double)nMotherEd0 / (double)nPerson;
double dWeight1 = (double)nMotherEd1 / (double)nPerson;
double dWeight2 = (double)nMotherEd2 / (double)nPerson;
int nIterations = 0;
double dResult = 500.0;
double dTarget = dProb;
double dLower = -10.0;
double dUpper = 10.0;
while (abs(dResult - dTarget) > 0.0001 && nIterations < 10000)
{
nIterations++;
dCenter = (dLower + dUpper) / 2.0;
dResult = dWeight0 * AdjustedProbability(dProb, dMotherFactorEduc0, dCenter)
+ dWeight1 * AdjustedProbability(dProb, dMotherFactorEduc1, dCenter)
+ dWeight2 * AdjustedProbability(dProb, dMotherFactorEduc2, dCenter);
if (dTarget > dResult) dLower = dCenter;
else dUpper = dCenter;
}
dCalibrationFactor = dCenter;
LastProbabilityPrimaryGrad[nSex][nProv] = dProb;
CalibrationFactorPrimaryGrad[nSex][nProv] = dCalibrationFactor;
}
// after calibrated year
else
{
dCalibrationFactor = CalibrationFactorPrimaryGrad[nSex][nProv];
dProb = LastProbabilityPrimaryGrad[nSex][nProv];
}
}
// decide for each primary student if graduating
for (int nJ = 0; nJ < nMotherEd0; nJ++) asPrimaryGraduationCohort[nSex][nProv][PL_NO]->
Item(nJ)->DecidePrimaryGrad(AdjustedProbability(dProb, dMotherFactorEduc0, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd1; nJ++) asPrimaryGraduationCohort[nSex][nProv][PL_ENTER]->
Item(nJ)->DecidePrimaryGrad(AdjustedProbability(dProb, dMotherFactorEduc1, dCalibrationFactor));
for (int nJ = 0; nJ < nMotherEd2; nJ++) asPrimaryGraduationCohort[nSex][nProv][PL_GRAD]->
Item(nJ)->DecidePrimaryGrad(AdjustedProbability(dProb, dMotherFactorEduc2, dCalibrationFactor));
}
}
}
}
Primary education status of immigrants¶
This function is called at the moment of immigration to assign a education status to the new immigrant. Depending on year of birth and age, there are three possibilities:
- The person’s graduation age is younger than school entry .and currently has not entered primary.
- For the birth date, parameters are available; education status is modeled
- The person was born too far in the past and no parameters are available; characteristics are sampled from the current immigrant population of the same age, sex, and province of residence. If no donor is found, the sampling is extended to the national population of the same age and sex.
void Person::SetImmigrantPrimaryEducationStatus()
{
if (person_type == PT_IMMIGRANT)
{
// ENTRY TO PRIMARY
double dTimeToNextSchoolyear = StartOfSchoolYear - (time - calendar_year);
if (dTimeToNextSchoolyear < 0) dTimeToNextSchoolyear = dTimeToNextSchoolyear + 1.0;
// (a) younger than entry age at next school year change:
if ((int)(age + dTimeToNextSchoolyear) <= AgeEnterPrimary) { /*do nothing*/ }
// (b) parameters available for given year of birth
else if (year_of_birth >= MIN(YOB_START_PRIMARY))
{
if (RandUniform(18) < StartPrimary[sex][RANGE_POS(YOB_START_PRIMARY, year_of_birth)][province_birth])
primary_level = PL_ENTER;
}
// (c) sample from other foreign born of same age, sex and province of residence
// if no foreignborn with same characteristics available, sample from all residents of same age and sex
else
{
Person * prPerson = new Person;
int nPopSize = asForeignersAgeSexProv[integer_age][sex][province_nat]->Count();
if (nPopSize > 0) prPerson = asForeignersAgeSexProv[integer_age][sex][province_nat]
->GetRandom(RandUniform(19));
else
{
nPopSize = asResidentsAgeSex[integer_age][sex]->Count();
if (nPopSize > 0) prPerson = asResidentsAgeSex[integer_age][sex]
->GetRandom(RandUniform(20));
}
// a donor was found: assign donor's primary entry level
if (nPopSize > 0 && prPerson->primary_level != PL_NO) primary_level = PL_ENTER;
// no donor was found; depending on samlpe size this should never or rarely happen
else if (RandUniform(21) < 0.5) primary_level = PL_ENTER;
}
// GRADUATION FROM PRIMARY
if (primary_level == PL_ENTER)
{
double dTimeToNextSchoolyear = StartOfSchoolYear - (time - calendar_year);
if (dTimeToNextSchoolyear < 0) dTimeToNextSchoolyear = dTimeToNextSchoolyear + 1.0;
// (a) younger than graduation age at next school year change:
if ((int)(age + dTimeToNextSchoolyear) <= AgeGradPrimary) { /* do nothing */ }
// (b) parameters available for given year of birth
else if (year_of_birth >= MIN(YOB_GRAD_PRIMARY))
{
if (RandUniform(22) < GradPrimary[sex][RANGE_POS(YOB_GRAD_PRIMARY, year_of_birth)][province_birth])
primary_level = PL_GRAD;
}
// (c) sample from other foreign born who entered primary of same age, sex and province of residence
// if no foreignborn with same characteristics available, sample from all residents of same age and sex
else
{
Person * prPerson = new Person;
int nPopSize = asForeignersPrimAgeSexProv[integer_age][sex][province_nat]->Count();
if (nPopSize > 0) prPerson = asForeignersPrimAgeSexProv[integer_age][sex][province_nat]
->GetRandom(RandUniform(23));
else
{
nPopSize = asResidentsPrimAgeSex[integer_age][sex]->Count();
if (nPopSize > 0) prPerson = asResidentsPrimAgeSex[integer_age][sex]
->GetRandom(RandUniform(24));
}
// a donor was found: assign donor's primary entry level
if (nPopSize > 0 && prPerson->primary_level == PL_GRAD) primary_level = PL_GRAD;
// no donor was found; depending on samlpe size this should never or rarely happen
else if (RandUniform(25) < 0.5) primary_level = PL_GRAD;
}
}
}
}
Supporting functions¶
/* NOTE(PrimarySchool.AdjustedProbability,EN)
This function modifies a base probability by two log odds factors. It is used to apply
log odds by mothers education and a calibration factor to the base probability
*/
double PrimarySchool::AdjustedProbability(double dProb, double dLogOddEduc, double dLogOddAdjust)
{
if (dProb >= 0.9999) dProb = 0.9999;
double dExp = exp(log(dProb / (1 - dProb)) + dLogOddEduc + dLogOddAdjust);
return dExp / (1 + dExp);
}
/* NOTE(Person.DecidePrimaryEntry,EN)
A simple function randomly setting the primary education entry status for a given probability
*/
void Person::DecidePrimaryEntry(double dProb)
{
if (RandUniform(16) < dProb) primary_level = PL_ENTER;
}
/* NOTE(Person.DecidePrimaryGrad,EN)
A simple function randomly setting the primary graduation status for a given probability
*/
void Person::DecidePrimaryGrad(double dProb)
{
if (primary_level == PL_ENTER && RandUniform(17) < dProb) primary_level = PL_GRAD;
}
7.13.4. Table Output¶
table Person TabEducByYobAge15 //EN Education at age 15
[trigger_entrances(integer_age,15) ]
{
sex + *
province_birth+ *
{
unit
}
* year_of_birth
* primary_level +
};
Changes in the Simulation() function in DYNAMIS-POP-MRT.mpp¶
The simulation engine now also has to create the primary school actor. The code is introduced after the person actors are created.
void Simulation()
{
[...]
for (long nJ = 0; nJ < TotalPopSampleSize; nJ++)
{
Person *paPerson = new Person();
paPerson->Start(nJ, NULL);
}
// Create the Primary School actor
PrimarySchool *paPrimarySchool = new PrimarySchool();
paPrimarySchool->Start();
[...]
}
Changes in PersonCore.mpp¶
We add a state for province of birth and a classification of provinces, including “abroad.”
classification PROVINCE_INT //EN Province incl abroad
{
PI_PROV00, //EN Hodh-Charghy
PI_PROV01, //EN Hodh-Gharby
PI_PROV02, //EN Assaba
PI_PROV03, //EN Gorgol
PI_PROV04, //EN Brakna
PI_PROV05, //EN Trarza
PI_PROV06, //EN Adrar
PI_PROV07, //EN Dakhlett-Nouadibou
PI_PROV08, //EN Tagant
PI_PROV09, //EN Guidimagha
PI_PROV10, //EN Tirs-Ezemour
PI_PROV11, //EN Inchiri
PI_PROV12, //EN Nouakchott
PI_PROV13 //EN Abroad
};
actor Person
{
[...]
PROVINCE_INT province_birth = { PI_PROV00 }; //EN Province of birth
};
In the Start() function, we initialize province of birth of newborns by the province of residence of the mother.
void Person::Start(long PersonNr, Person *peMother)
{
[...]
province_nat = peMother->province_nat;
province_birth = (PROVINCE_INT)peMother->province_nat;
age = 0;
time = time_of_birth;
[...]
}
Changes in SatrtPopFile.mpp¶
We add the two new characteristics, province of birth and primary school level, from the starting population used at this step:
void Person::GetStartCharacteristics()
{
[...]
province_birth = (PROVINCE_INT)(int)prObservation->pmc[PMC_POB];
primary_level = (PRIMARY_LEVEL)(int)prObservation->pmc[PMC_EDUC];
[...]
}
Changes in the Immigration.mpp module¶
The province of birth is added as a characteristic at immigration:
void Person::GetImmigrantCharacteristics()
{
[...]
province_birth = (PROVINCE_INT)(SIZE(PROVINCE_INT) - 1); // abroad is last category
}
7.14. Step 14: First Union Formation¶
7.14.1. Overview¶
This module implements first union formation (marriage) of women. It allows the user to choose between two approaches. The first option is applying a Coale & McNeil model. This allows parameterizing union formation by three parameters:
- Youngest age
- Average age of union formation
- Proportion of the population ever entering a union
Alternatively, users can specify period rates of age-specific union formation risks.
7.14.2. Concepts¶
This step provides an example of allowing users to choose the model to be applied to the modeled process.
Allowing users to choose between models¶
The union formation module allows the user to choose between two modeling options. Based on this choice, parameters are calculated and copied into a model-generated parameter, which is then applied independent of the user’s choice for the rest of the code.
classification UNION_MODEL_SELECTION //EN Model selection
{
UMS_COALE, //EN Coale & McNeil
UMS_RATES //EN Rates by age, period and education
};
parameters
{
//EN Model selection for union formation
UNION_MODEL_SELECTION UnionModelSelection;
//EN Union formation (Option A): Coale and McNeil
double UnionParameters[PRIMARY_LEVEL][YOB_UNION][UNION_PARA];
//EN Union formation (Option B): Hazards
double UnionFormationHazardParameter[PRIMARY_LEVEL][AGE_UNION][SIM_YEAR_RANGE];
//EN Union formation hazards
model_generated double UnionFormationHazard[PRIMARY_LEVEL][SIM_YEAR_RANGE][AGE_UNION];
};
/*
In the presimulation of the union formation module, age specific hazards for union formation
are calculated and copied into a model-generated parameter. Depending on the model selected by the user,
values are calculated using a Coale McNeil model, or come directly from a parameter table.
*/
void PreSimulation()
{
for (int nEduc = 0; nEduc < SIZE(PRIMARY_LEVEL); nEduc++)
{
for (int nYear = MIN(SIM_YEAR_RANGE); nYear <= MAX(SIM_YEAR_RANGE); nYear++)
{
for (int nAge = MIN(AGE_UNION); nAge <= MAX(AGE_UNION); nAge++)
{
if (UnionModelSelection == UMS_COALE)
{
UnionFormationHazard[nEduc][RANGE_POS(SIM_YEAR_RANGE,nYear)][RANGE_POS(AGE_UNION,nAge)]
= GetUnionFormationHazard(nEduc,nYear,nAge);
}
else
{
UnionFormationHazard[nEduc][RANGE_POS(SIM_YEAR_RANGE,nYear)][RANGE_POS(AGE_UNION,nAge)]
= UnionFormationHazardParameter[nEduc][RANGE_POS(AGE_UNION,nAge)]
[RANGE_POS(SIM_YEAR_RANGE,nYear)];
}
}
}
}
}
7.14.3. How to reproduce this modeling step¶
At this step, we add a module UnionFormation.mpp. Changes in other modules are very minor.
The UnionFormation.mpp module¶
Actor set and type definitions¶
The module uses one actor set for females by age and education; it is used to sample union formation dates from the female population and this information is input for immigrants. The user selection between two model types is implemented via a classification of possible choices.
// Females by age and education
actor_set Person asFemaleAgeEduc[integer_age][primary_level] filter set_alive && sex==FEMALE;
range YOB_UNION { 1964, 2050 }; //EN Year of birth
range AGE_UNION { 10, 60 }; //EN Age
classification UNION_PARA //EN Union formation parameters
{
UP_MINAGE, //EN Age at which first 1% enter a union
UP_AVERAGE, //EN Average age at first union formation
UP_EVER //EN Proportion ow femailes ever entering a union
};
classification UNION_MODEL_SELECTION //EN Model selection
{
UMS_COALE, //EN Coale & McNeil
UMS_RATES //EN Rates by age, period and education
};
Model parameters¶
There are four parameters: the first for choosing the model to be used, one for each model, and a model-generated parameter in which the final parameters calculated from the chosen model parameters are copied in the pre-simulation.
parameters
{
//EN Model selection for union formation
UNION_MODEL_SELECTION UnionModelSelection;
//EN Union formation (Option A): Coale and McNeil
double UnionParameters[PRIMARY_LEVEL][YOB_UNION][UNION_PARA];
//EN Union formation (Option B): Hazards
double UnionFormationHazardParameter[PRIMARY_LEVEL][AGE_UNION][SIM_YEAR_RANGE];
//EN Union formation hazards
model_generated double UnionFormationHazard[PRIMARY_LEVEL][SIM_YEAR_RANGE][AGE_UNION];
};
parameter_group PG_UnionFormation //EN Union formation
{
UnionModelSelection, UnionParameters,
UnionFormationHazardParameter
};
Actor states and functions¶
The key state modeled in this module is in_union, indicating that a woman has entered a union. The date can be recorded in the starting pulation file, modeled by the selected modeling choice, or input, in the case of immigrants.
actor Person
{
AGE_UNION age_union = COERCE(AGE_UNION, integer_age); //EN Age
//EN Union formation hazard
double union_formation_hazard = (WITHIN(AGE_UNION, integer_age) && sex==FEMALE) ?
UnionFormationHazard[primary_level][RANGE_POS(SIM_YEAR_RANGE, calendar_year)]
[RANGE_POS(AGE_UNION, integer_age)] : 0.0;
TIME time_of_union_formation = { TIME_INFINITE }; //EN Time of union formation
logical in_union = { FALSE }; //EN Ever in union
event timeUnionFormationEvent, UnionFormationEvent; //EN First union formation event
void SetImmigrantUnionFormation(); //EN Union formation date for immigrants
};
Calculating hazards using a Coale McNeil model¶
GetUnionFormationHazard() returns the union formation hazard for given education, age, and period based on a Coale McNeil model. This is a parametric model with three parameters: youngest age, average age of union formation, and proportion of the population ever entering a union.
This information is provided by a model parameter “UnionParameters.”
double GetUnionFormationHazard(int nEduc, int nYear, int nAge);
double GetUnionFormationHazard(int nEduc, int nYear, int nAge)
{
double dReturnValue = 0.0;
double g[SIZE(AGE_UNION)];
double a0 = UnionParameters[nEduc][RANGE_POS(YOB_UNION, nYear - nAge)][UP_MINAGE];
double my = UnionParameters[nEduc][RANGE_POS(YOB_UNION, nYear - nAge)][UP_AVERAGE];
double C = UnionParameters[nEduc][RANGE_POS(YOB_UNION, nYear - nAge)][UP_EVER];
double k = (my - a0) / 11.36;
double dThisYearCumulative = 0.0;
if (nAge < a0) dReturnValue = 0.0;
else if ( nAge >= a0 )
{
for (int nX = 0; nX < SIZE(AGE_UNION); nX++)
{
g[nX] = 0.19465*exp(-0.174*(nX - 6.06) - exp(-0.288*(nX - 6.06)));
}
double index = ((double)nAge - a0) / k;
int nIndex = int(index); // integer part
double dIndex = index - nIndex; // after comma part
double G = 0.0;
for (int nI = 0; nI <= nIndex; nI++)
{
if (nI < SIZE(AGE_UNION)) G = G + g[nI];
}
if (nIndex < SIZE(AGE_UNION) - 1) G = G + dIndex*g[nIndex + 1];
dThisYearCumulative = G * C;
}
if (nAge == a0) dReturnValue = dThisYearCumulative;
else if ( nAge > a0 )
{
double index = ((double)nAge - a0 -1) / k;
int nIndex = int(index); // integer part
double dIndex = index - nIndex; // after comma part
double G = 0.0;
for (int nI = 0; nI <= nIndex; nI++)
{
if (nI < SIZE(AGE_UNION)) G = G + g[nI];
}
if (nIndex < SIZE(AGE_UNION) - 1) G = G + dIndex*g[nIndex + 1];
if (1.0 - G * C > 0.0001) dReturnValue = (dThisYearCumulative - G * C) / (1 - G * C);
else dReturnValue = 0.0;
}
// convert probability to hazard
if (dReturnValue < 1.0) dReturnValue = -log(1 - dReturnValue);
return dReturnValue;
}
The union formation event¶
TIME Person::timeUnionFormationEvent()
{
if (calendar_year >= MIN(SIM_YEAR_RANGE) & sex == FEMALE && !in_union && WITHIN(AGE_UNION, integer_age)
&& union_formation_hazard > 0.0)
{
return WAIT(-TIME(log(RandUniform(26)) / union_formation_hazard));
}
else if (time_of_union_formation < MIN(SIM_YEAR_RANGE) & sex == FEMALE
&& !in_union & calendar_year < MIN(SIM_YEAR_RANGE))
{
return time_of_union_formation;
}
else return TIME_INFINITE;
}
void Person::UnionFormationEvent()
{
in_union = TRUE;
time_of_union_formation = time;
}
Sampling the union formation age for immigrants¶
The function to sample a union formation age for immigrants based on age and primary education level is called at immigration. The host population is the total population of the same age and education.
void Person::SetImmigrantUnionFormation()
{
if (person_type == PT_IMMIGRANT && sex==FEMALE && asFemaleAgeEduc[integer_age][primary_level]->Count()>0)
{
Person * prPerson = new Person;
prPerson = asFemaleAgeEduc[integer_age][primary_level]->GetRandom(RandUniform(27));
time_of_union_formation = prPerson->time_of_union_formation;
if (time_of_union_formation != TIME_INFINITE) in_union = TRUE;
}
}
Presimulation¶
In the presimulation of the union formation module, age-specific hazards for union formation are calculated and copied into a model-generated parameter. Depending on the model selected by the user, values are calculated using a Coale McNeil model, or come directly from a parameter table.
void PreSimulation()
{
for (int nEduc = 0; nEduc < SIZE(PRIMARY_LEVEL); nEduc++)
{
for (int nYear = MIN(SIM_YEAR_RANGE); nYear <= MAX(SIM_YEAR_RANGE); nYear++)
{
for (int nAge = MIN(AGE_UNION); nAge <= MAX(AGE_UNION); nAge++)
{
if (UnionModelSelection == UMS_COALE)
{
UnionFormationHazard[nEduc][RANGE_POS(SIM_YEAR_RANGE,nYear)][RANGE_POS(AGE_UNION,nAge)]
= GetUnionFormationHazard(nEduc,nYear,nAge);
}
else
{
UnionFormationHazard[nEduc][RANGE_POS(SIM_YEAR_RANGE,nYear)][RANGE_POS(AGE_UNION,nAge)]
= UnionFormationHazardParameter[nEduc][RANGE_POS(AGE_UNION,nAge)]
[RANGE_POS(SIM_YEAR_RANGE,nYear)];
}
}
}
}
}
Tables¶
At this step, we add one table, which enables display of the calculated hazards and compares them with the simulated hazards. Besides hazards, the table also displays the proportion of women who ever entered a union. The table is by calendar year, age, and education.
table_group TG_05_Union_Tables //EN Union Formation
{
UnionFormationTab
};
actor Person
{
SIM_YEAR_RANGE tab_sim_year = COERCE(SIM_YEAR_RANGE,calendar_year); //EN Year
};
table Person UnionFormationTab //EN Union formation
[WITHIN(AGE_UNION, integer_age) && sex == FEMALE && WITHIN(SIM_YEAR_RANGE, calendar_year)]
{
{
//EN Hazard from parameters decimals=4
max_value_in(union_formation_hazard),
//EN Simulated hazard decimals=4
transitions(in_union, FALSE, TRUE) / duration(in_union, FALSE),
//EN Proportion of women who ever entered a union decimals=4
duration(in_union,TRUE)/duration()
}
* primary_level+
* age_union
* tab_sim_year
};
Changes in SetAliveEvent()¶
The sampling function for inputting a union formation date for immigrants at the moment of immigration is called in the “SetAliveEvent” and is called immediately after the actor is created (which, for immigrants, is the moment of immigration). To use education as a characteristic, SetImmigrantPrimaryEducationStatus() is explicitly called before, and the function hook used so far has to be removed.
void Person::SetAliveEvent()
{
set_alive = TRUE;
if (person_type == PT_IMMIGRANT)
{
SetImmigrantPrimaryEducationStatus();
SetImmigrantUnionFormation();
}
}
Changes in GetStartCharacteristics()¶
When reading the starting population file, the initialization of the union formation date has to be added:
void Person::GetStartCharacteristics()
{
[...]
if (prObservation->pmc[PMC_UNION] < MIN(SIM_YEAR_RANGE))
{
time_of_union_formation = prObservation->pmc[PMC_UNION];
}
[...]
}
7.15. Step 15: Fertility refined¶
7.15.1. Overview¶
This step refines the fertility module by adding an alternative way of modeling births and four options for which model to use and if and how to align it. Basically, fertility is implemented in two ways:
- The macro approach: rates by age only. Fertility is based on age-specific fertility rates calculated from two parameters: an age distribution of fertility and the TFR for future years. Another parameter is the sex ratio.
- The micro approach: fertility is modeled by age, parity, time since last birth, education, and union status. In this implementation, fertility is modeled based on a more detailed model estimated from micro-data. Users can choose to align the outcome to the macro outcomes, either for total outcome (number of births) or for births by age.
7.15.2. Concepts¶
This step provides an example of using the self_scheduling split() function to determine the time interval since an event. It also provides an example for model alignment, where the number of events is determined by one model (the “macro” model), producing the alignment targets, while the selection to whom the event applies is based on another model (a refined “micro” model to find an appropriate person with the shortest waiting time).
State duration: Time intervals since an event¶
In this example, the time interval since the previous birth event is used for calculating the duration baseline risk for higher-order births. Cut-offs for time intervals are the first year since the previous birth, then 3, 6, 9, and 12 years. A logical indicator state is set to first FALSE and then TRUE in each birth event to reset the duration clock. This indicator is then used to split up the duration since the last event using self_scheduling_split. To calculate the most recent duration in a state, the function active_spell_duration() is used to determine the time a state has been in a given state since the last state change.
partition DUR_TIME_IN_PARITY{ 1, 3, 6, 9, 12 }; //EN Duration episodes since last birth
actor Person
{
//EN Indicator set to FALSE and then again TRUE in the birth event
logical in_this_parity = { FALSE };
//EN Time index since the last birth event
int time_in_parity
= self_scheduling_split(active_spell_duration(in_this_parity, TRUE), DUR_TIME_IN_PARITY);
};
void Person::BirthEvent()
{
in_this_parity = FALSE; // reset indicator: parity will increase
parity++; // increment parity
in_this_parity = TRUE; // set indicator true again
}
Model Alignment: Using one model for scheduling events happening to persons selected by another model¶
It must be determined at each birth event if the birth applies to the actor herself (unaligned models) or if an appropriate mother for the birth still has to be found. The latter is used for aligned modes, in which the birth events are produced by the macro model to which output is aligned, while the birth itself is given by the potential mother with the shortest waiting time determined by the micro model.
In this example, the time function is used in two ways: the “normal” way, to schedule events, and by directly calling it, with an indicator state “override_mode” set to TRUE, to find the actor with the shortest waiting time.
//EN Actor set of potential mothers
actor_set Person asPotentialMothers filter is_potential_mother;
TIME Person::timeBirthEvent()
{
double dEventTime = TIME_INFINITE;
double dHazard = 0.0;
if (is_potential_mother)
{
// Timing comes from macro model (incl. models aligned to macro numbers)
if ( use_macro_model && !override_mode)
{
dHazard = [.. Calculation of macro model hazard ];
}
// Timing comes from micro model or waiting time needed from micro model
else
{
dHazard = [.. Calculation of micro model hazard ]
}
if (dHazard > 0.0) dEventTime = WAIT(-TIME(log(RandUniform(28)) / dHazard));
}
return dEventTime;
}
void Person::BirthEvent()
{
// event applies to individual without alignment
if ( not_an_aligned_model )
{
// increase parity of this actor and create a baby linked to this actor
parity++; // increment parity
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1, this); // Call the Start() function of the new actor
}
// aligned to macro-model: find women with shortest waing time to birth
else
{
Person *peMother = NULL;
Person *pePotentialMother = NULL;
double dShortestWaitTime = TIME_INFINITE;
// find an appropriate mother for this birth event
// loop through all potential mothers and find the one with the shortest
// waiting time by explicitely calling the waiting time function
double nNumber = asPotentialMothers->Count();
for (double nJ = 0; nJ < nNumber; nJ++)
{
pePotentialMother = asPotentialMothers->Item(nJ);
// Set override_mode in order to get waiting time based on micro model
// from time function timeBirthEvent()
pePotentialMother->override_mode = TRUE;
// calculate waiting time and store shortest
double dCurrentWaitTime = pePotentialMother->timeBirthEvent() - time;
if (dCurrentWaitTime < dShortestWaitTime)
{
peMother = pePotentialMother;
dShortestWaitTime = dCurrentWaitTime;
}
// reset override_mode
pePotentialMother->override_mode = FALSE;
}
// increase parity of the selected mother and create a baby linked to her mother
peMother->parity++; // increment parity
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1, peMother); // Call the Start() function of the new actor
}
}
7.15.3. How to reproduce this modeling step¶
- Most changes and additions made in this step affect the module Fertility.mpp.
- In StartPopFile.mpp, code has to be added to set the state’s parity and time since last birth.
- For immigrants, parity and time since last birth have to be sampled. This code can be added after sampling the time of union formation using the same donor (function SetImmigrantUnionFormation() in UnionFormation.mpp).
The new Fertility.mpp module¶
Actor sets¶
Actor sets are used to loop through potential mothers to find the one with the shortest waiting time. This is used if model alignment is chosen. Because the user can choose if a model is aligned by number of births only or by the number of births by age, two actor sets are provided.
//EN Potential mothers
actor_set Person asPotentialMothers filter is_potential_mother;
//EN Potential mothers by age
actor_set Person asPotentialMothersByAge[fertile_age] filter is_potential_mother;
Dimensions¶
Most dimensions used in this module are added at this step. The following is the complete code of declarations of dimensions.
// Dimensions
range FERTILE_AGE_RANGE { 10, 49 }; //EN Fertile age range
range PARITY_RANGE { 0, 15 }; //EN Parity range
range PARITY_RANGE1{ 1, 15 }; //EN Parity range 1+
range PARITY_RANGE2 { 2, 15 }; //EN Parity range 2+
classification SELECTED_FERTILITY_MODEL //EN Fertility model options
{
SFM_MACRO, //EN Macro model (age only)
SFM_MICRO, //EN Micro model un-aligned
SFM_ALIGNE_BIRTHS, //EN Micro model with aligned number of births
SFM_ALIGNE_AGE //EN Micro model with aligned number of births by age
};
classification UNION_STATUS //EN Union Status
{
US_NO, //EN Never entered a union
US_YES //EN Entered a union
};
classification HIGHER_BIRTHS_PARA //EN Parameters for higher order births
{
HBP_PERIOD1, //EN 0-1 years after previous birth
HBP_PERIOD2, //EN 1-3 years after previous birth
HBP_PERIOD3, //EN 3-6 years after previous birth
HBP_PERIOD4, //EN 6-9 years after previous birth
HBP_PERIOD5, //EN 9-12 years after previous birth
HBP_PERIOD6, //EN 12+ years after previous birth
HBP_AGE35, //EN Age 35-39
HBP_AGE40, //EN Age 40-44
HBP_AGE45, //EN Age 45+
HBP_EDUC1, //EN Entered primary school but dropped out
HBP_EDUC2 //EN Graduated from primary school
};
partition BIRTH_AGE_PART{ 35, 40, 45 }; //EN Age Groups
partition DUR_TIME_IN_PARITY{ 1, 3, 6, 9, 12 }; //EN Duration episodes since last birth
Parameters¶
New parameters include a parameter to select between the four model options, as well as all the parameters for the second model type introduced; this type models fertility by parity and various other characteristics. For easier use, the parameters are hierarchically grouped. The following is the complete list of parameters including the parameters already used in the previous model steps.
parameters
{
// Model selection
SELECTED_FERTILITY_MODEL selected_fertility_model; //EN Fertility model selection
// Macro model parameters
double AgeSpecificFertility[FERTILE_AGE_RANGE][SIM_YEAR_RANGE]; //EN Age distribution of fertility
double TotalFertilityRate[SIM_YEAR_RANGE]; //EN Total fertility rate
double SexRatio[SIM_YEAR_RANGE]; //EN Sex ratio (male per 100 female)
// Micro model parameters
//EN First Births
double FirstBirthRates[PRIMARY_LEVEL][UNION_STATUS][FERTILE_AGE_RANGE][PROVINCE_NAT];
//EN Higher order births
double HigherOrderBirthsPara[HIGHER_BIRTHS_PARA][PARITY_RANGE2]; //EN Higher order births
//EN Birth Trends
double BirthTrends[PARITY_RANGE1][SIM_YEAR_RANGE];
// Model-generated parameters
//EN Age specific fertility rate
model_generated double AgeSpecificFertilityRate[FERTILE_AGE_RANGE][SIM_YEAR_RANGE];
};
//EN Fertility
parameter_group PG03_Fertility_Top
{
selected_fertility_model, PG03a_Fertility_Model_A, PG03b_Fertility_Model_B
};
//EN Fertility Model A: Macro approach
parameter_group PG03a_Fertility_Model_A
{
AgeSpecificFertility, TotalFertilityRate, SexRatio
};
//EN Fertility Model B: Micro approach
parameter_group PG03b_Fertility_Model_B
{
FirstBirthRates, HigherOrderBirthsPara, BirthTrends
};
Actor Person declarations¶
A number of states is added at this step. The refined module allows the choice of a more detailed way of modeling fertility by accounting for parity, union status, and the duration since a previous birth. The way to get an automatically updated index of the time interval since last birth using the functions self_scheduling_split() and active_spell_duration() were explained in the section “Concepts” above. Unlike the example given there, in our model, we must also take care of the time since last birth recorded in the starting population file, which has to be added to the simulated time. As self_scheduling_split() only allows a set of functions as arguments (duration, weighted_duration, active_spell_duration, and active_spell_weighted_duration), we use the function split() instead. This function does not update its own event; instead, we ensure yearly updates by using an integer index of full years since last birth, which itself is a self-scheduling state.
The following code is the complete code of the actor Person block including states introduced at previous steps already.
actor Person
{
FERTILE_AGE_RANGE fertile_age = COERCE(FERTILE_AGE_RANGE, integer_age); //EN Age
int birth_age_part = split(integer_age, BIRTH_AGE_PART); //EN Age group
PARITY_RANGE parity = { 0 }; //EN Parity
UNION_STATUS union_status = (in_union) ? US_YES : US_NO; //EN Union Status
//EN Switch for calculating waiting times from one model while using another
logical override_mode = { FALSE };
//EN Indicator re-set at birth events for calculation of time since last birth
logical in_this_parity = { TRUE };
//EN Indicator that current year is in simulated time
logical in_simulation_time = ( calendar_year >= MIN(SIM_YEAR_RANGE)) ? TRUE : FALSE;
//EN Indicator re-set at birt events calculating time since last birth in simulation time
logical in_this_parity_in_simulation = in_simulation_time && in_this_parity;
//EN Time since last birth in the starting population or at creation (immigrants)
double time_since_last_birth_start = { 0.0 };
//EN Time since last birth in full years
int time_since_last_birth = self_scheduling_int(active_spell_duration(in_this_parity_in_simulation, TRUE))
+ round(time_since_last_birth_start);
//EN Time index since the last birth event
int time_in_parity = split(time_since_last_birth, DUR_TIME_IN_PARITY);
//EN Indicator that perion is a potential mother
logical is_potential_mother = (set_alive && sex == FEMALE && WITHIN(FERTILE_AGE_RANGE, integer_age)
&& parity < MAX(PARITY_RANGE) && WITHIN(SIM_YEAR_RANGE, calendar_year)) ? TRUE : FALSE;
logical set_alive = { FALSE }; //EN Person is set to be alive already
event timeSetAliveEvent, SetAliveEvent; //EN Event to set set_alive to true
event timeBirthEvent, BirthEvent; //EN Birth event
};
The birth event¶
The time function now contains two ways of calculating waiting times, one following the previous “macro” model, and the second, a refined “micro” model introduced at this step. The time function is used in two ways: first, the “normal” way to schedule events, and second, explicitly for getting waiting times from the “micro” model (in override_mode) for finding the potential mother with the shortest waiting time. If the user chooses to align the model, the macro model is used for scheduling the event, but the birth happens to another person, i.e., the person with the shortest waiting time based on the micro model. Thus at each birth event it has to be determined if the birth applies to the actor herself (unaligned models) or if an appropriate mother for the birth still has to be found. The logic of this alignment method is explained in more detail above in the “Concepts” section.
At each birth we reset the indicator in_this_parity (setting it first FALSE and later TRUE again), which is used to calculate the state duration since last birth (using active_spell_duration(in_this_parity,TRUE)). Also the time since last birth (time_since_last_birth_start) from the starting population is reset to 0.
TIME Person::timeBirthEvent()
{
double dEventTime = TIME_INFINITE;
double dHazard = 0.0;
if (is_potential_mother)
{
// Timing comes from macro model (incl. models aligned to macro numbers)
if (selected_fertility_model != SFM_MICRO && !override_mode)
{
dHazard = AgeSpecificFertilityRate[RANGE_POS(FERTILE_AGE_RANGE, integer_age)]
[RANGE_POS(SIM_YEAR_RANGE, calendar_year)];
}
// Timing comes from micro model or waiting time needed from micro model
else
{
// first birth
if (parity == 0)
{
dHazard = FirstBirthRates[primary_level][union_status][RANGE_POS(FERTILE_AGE_RANGE, integer_age)][province_nat]
* BirthTrends[RANGE_POS(PARITY_RANGE1, parity+1)][RANGE_POS(SIM_YEAR_RANGE, calendar_year)];
}
// higher order births
else
{
double emil = time_since_last_birth;
double hugo = 1.1;
dHazard = HigherOrderBirthsPara[time_in_parity][RANGE_POS(PARITY_RANGE2, parity+1)] //Baseline
* BirthTrends[RANGE_POS(PARITY_RANGE1, parity+1)][RANGE_POS(SIM_YEAR_RANGE, calendar_year)]; //Trend
integer gustav = time_in_parity;
// relative risk for older age
if (birth_age_part > 0) //not the baseline age group
{
dHazard = dHazard * HigherOrderBirthsPara[HBP_AGE35 - 1 + birth_age_part][RANGE_POS(PARITY_RANGE2, parity+1)];
}
// relative risk for primary dropouts and graduates
if (primary_level == PL_ENTER)
{
dHazard = dHazard * HigherOrderBirthsPara[HBP_EDUC1][RANGE_POS(PARITY_RANGE2, parity+1)];
}
else if (primary_level == PL_GRAD)
{
dHazard = dHazard * HigherOrderBirthsPara[HBP_EDUC2][RANGE_POS(PARITY_RANGE2, parity+1)];
}
}
}
// schedule event
if (dHazard > 0.0) dEventTime = WAIT(-TIME(log(RandUniform(28)) / dHazard));
}
return dEventTime;
}
void Person::BirthEvent()
{
// event applies to individual without alignment
if (selected_fertility_model == SFM_MICRO || selected_fertility_model == SFM_MACRO)
{
// increase parity of this actor and create a baby linked to this actor
in_this_parity = FALSE; // initialize indicator
time_since_last_birth_start = 0.0; // reset time before start of simulation
parity++; // increment parity
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1, this); // Call the Start() function of the new actor and pass own address
in_this_parity = TRUE; // reset indicator
}
// aligned to macro-model: find women with shortest waing time to birth
else
{
Person *peMother = NULL;
Person *pePotentialMother = NULL;
double dShortestWaitTime = TIME_INFINITE;
// align number of births only
if ( selected_fertility_model == SFM_ALIGNE_BIRTHS )
{
double nNumber = asPotentialMothers->Count();
// find an appropriate mother for this birth event
for (double nJ = 0; nJ < nNumber; nJ++)
{
pePotentialMother = asPotentialMothers->Item(nJ);
pePotentialMother->override_mode = TRUE;
double dCurrentWaitTime = pePotentialMother->timeBirthEvent() - time;
if (dCurrentWaitTime < dShortestWaitTime)
{
peMother = pePotentialMother;
dShortestWaitTime = dCurrentWaitTime;
}
pePotentialMother->override_mode = FALSE;
}
}
// align number of birth by age
else
{
double nNumber = asPotentialMothersByAge[RANGE_POS(FERTILE_AGE_RANGE, fertile_age)]->Count();
// find an appropriate mother for this birth event
for (double nJ = 0; nJ < nNumber; nJ++)
{
pePotentialMother = asPotentialMothersByAge[RANGE_POS(FERTILE_AGE_RANGE, fertile_age)]->Item(nJ);
pePotentialMother->override_mode = TRUE;
double dCurrentWaitTime = pePotentialMother->timeBirthEvent() - time;
if (dCurrentWaitTime < dShortestWaitTime)
{
peMother = pePotentialMother;
dShortestWaitTime = dCurrentWaitTime;
}
pePotentialMother->override_mode = FALSE;
}
}
// increase parity of the selected mother and create a baby linked to her mother
peMother->in_this_parity = FALSE; // initialize indicator
peMother->time_since_last_birth_start = 0.0; // reset time before start of simulation
peMother->parity++; // increment parity
Person *peChild = new Person; // Create and point to a new actor
peChild->Start(-1, peMother); // Call the Start() function of the new actor and pass own address
peMother->in_this_parity = TRUE; // reset indicator
}
}
Changes in StartPopFile.mpp¶
The following code for assigning values to the state’s parity and time_since_last_birth is added in the function GetStartCharacteristics()
if ( sex == FEMALE )
{
parity = (int)prObservation->pmc[PMC_PARITY];
if ( parity > 0 && MIN(SIM_YEAR_RANGE) > prObservation->pmc[PMC_LASTBIR] )
{
time_since_last_birth_start = MIN(SIM_YEAR_RANGE) - prObservation->pmc[PMC_LASTBIR];
}
}
Changes in UnionFormation.mpp to assign values to immigrants¶
In the function SetImmigrantUnionFormation(), we add the following code to assign values to the state parity and time_since_last_birth_start using the same donor as for the date of first union formation.
parity = prPerson->parity;
time_since_last_birth_start = prPerson->time_since_last_birth;
7.16. Step 16: Infant mortality¶
7.16.1. Overview¶
This module implements a specific model for infant and child mortality and replaces the general mortality model for ages 0-4. The child mortality module is based on a proportional model consisting of:
- Mortality baseline hazards by age and sex
- Relative risks by age for mother’s characteristics: age at birth (three groups), primary education (three groups)
- A time trend by age
The module is optional, and the user can choose between various options:
- Disable the child mortality model; the same model is used for all ages.
- Child mortality model without alignment: the child mortality module replaces the overall mortality module for ages 0-4. Note that the overall life expectancy may be altered by this choice.
- Child mortality model is calibrated for an initial year, then trends as for other ages: In this choice, life expectancy is the same as in the overall mortality for the given year, but as the composition of the population by mothers’ characteristics changes over time, the number of deaths (and therefore life expectancy) will be different for the following years, allowing for scenario comparisons.
- Child mortality model calibrated for an initial year, then specific trends from the child mortality module: In this choice, life expectancy is the same as in the overall mortality for the given year, but as the composition of the population by mothers’ characteristics changes over time, and as a result of potentially different trends, the number of deaths (and therefore life expectancy) will be different for the following years, allowing for scenario comparisons.
If activated, the model for child mortality starts replacing the overall mortality model five calendar years after the starting year of the simulation, ensuring that all children “know” their mother’s characteristics (are born in the simulation). As mother’s characteristics are not available for children born outside the country, immigrants are excluded from the model and handled by the general mortality module.
7.16.2. Concepts¶
From an implementation point of view, the main complication of this module lies in calibrating a model during execution. Five years into the simulation, all children subject to infant and child mortality are born in the simulation and thus know their mother’s characteristics. At this point, a calibration routine is performed, searching for baseline mortality rates by age and sex which, when applying to the relative risks by mothers’ characteristics, result in the same overall target mortality as the model without relative risks.
Calibrating a model during execution¶
The main problem of calibrating a model during execution is that model-generated parameters can only be set in the pre-simulation phase and not during execution. Calibration results thus cannot be stored as parameters, but have to be handled as states. To avoid storing these “parameters” as individual characteristics, a specific calibration actor can be defined and calibration results can be made states of this single actor. Individuals then can link to this actor and access the “parameter” values without having to store them individually.
The calibration actor
actor Calibrator
{
double calibrated_value; //EN Calibrated value
event timeCalibrationEvent, CalibrationEvent; //EN Calibration event
};
TIME Calibrator::timeCalibrationEvent()
{
.... return the time, when the calibration should be performed;
}
void Calibrator::CalibrationEvent()
{
calibrated_value = calculate the value...
}
Accessing the calibrated value: A simple way to access the states of a single instance actor is by referencing it as the first item of an actor set:
actor_set Calibrator asCalibrator;
//... wherever the state is needed, it can be accessed as:
dLocalVariable = asCalibrator->Item(0)->calibrated_value;
Actors can also be permanently linked to the Calibrator and then access its states more directly:
// Links have to be defined, e.g.:
link Person.lCalibrator Calibrator.mlPerson[]; //EN Link between 1 calibrator and n persons
//.. actors then can easily be linked, e.g. at the person's creation at Start()
lCalibrator = Calibrator->Item(0);
//.. to access states of the Calibrator, now the link can be used
dLocalVariable = lCalibrator->calibrated_value;
Actor linkages¶
Actors of the same or of different types can be linked, allowing access to the states of linked actors and easily interchanging information. Links can be of type 1:1 (e.g., between spouses), 1:n (e.g., children to mothers), and n:n (e.g., persons to siblings). Links have to be declared first. Once a link is established, it is maintained automatically and symmetrically, e.g., a child establishing a link to her mother automatically adds the link to her to the mother.
Declaration
// 1:1 of same actor type: link ActorName.LinkName
link Person.lSpouse;
// 1:1 of different actor types: link ActorOneName.LinkNameX ActorTwoName.LinkNameY
link Person.lSocialInsuranceAccount SocialInsuranceAccount.lPerson ;
// 1:n
link Person.lCalibrator Calibrator.mlPersons[];
link Person.lMother Person.mlChildren[];
// n:n
link Person.mlSportClubs[] SportClubs.mlMembers[];
Establishing a link A typical place to establish a link is in the Start() function, e.g., linking to a mother, if her address is passed over at birth through Start(Person *prMother).
// Declaration
link Person.lMother Person.mlChildren[]; // Link between mother and her children
// the mother creates a baby and calls the baby's start function passing over her own address 'this'
...
prChild->Start(this);
// the baby permanently establishes a link
void Person::Start(Person *prMother)
{
...
lMother = prMother;
}
// States of a linked actor can be accessed anywhere except for time functions of events.
// It is also possible to use links in derived states, for example:
logical mother_is_alive = ( lMother != NULL ) ? TRUE : FALSE;
double mothers_wealth = ( mother_is_alive ) ? lMother->wealth : 0.0;
Links are automatically maintained, so whenever a characteristic of an actor changes, this change becomes visible to the linked actor. Also, establishing or removing a link leads to the symmetric action on the other side of the linkage. A link is removed by setting it to NULL. For example, at divorce, the code lSpouse=NULL will remove the link on both ends of the previously married couple. Links are automatically removed at the death of an actor.
7.16.3. How to reproduce this step¶
Most of the changes are contained in the new module, ChildMortality.mpp, which is added at this step. Minor changes are required in the simulation engine, general mortality module, and PersonCore module.
The new module ChildMortality.mpp¶
Classifications and ranges¶
classification SELECTED_CHILD_MORTALITY_MODEL //EN Child Mortality Model Options
{
SCMM_MACRO, //EN Disable child mortality model
SCMM_MICRO_NOT_ALIGNED, //EN Child mortality model without alignment
SCMM_MICRO_ALIGNED_MACRO_TRENDS, //EN Aligned, trends as for other ages
SCMM_MICRO_ALIGNED_MICRO_TRENDS //EN Aligned, trends from child mortality model
};
classification CHILD_MORTALITY_RISKS //EN Relative risks for child mortality
{
CMR_AGE14, //EN Mothers age at birth below 15
CMR_AGE16, //EN Mothers age at birth below 17
CMR_NOPRIM, //EN Mother never entered primary education
CMR_PRIMDROP //EN Mother dropped out of primary education
};
classification MOTHER_AGE_CLASS //EN Mother age group
{
MAC_14, //EN 14 and below
MAC_16, //EN 15 or 16
MAC_17P //EN 17 and above
};
range CHILD_MORTALITY_AGE { 0, 4 }; //EN Age range for child mortality
range CHILD_MORTALITY_YEARS{ 2018, 2113 }; //EN Year range for which child mortality is modeled
Parameters¶
parameters
{
//EN Child mortality model selection
SELECTED_CHILD_MORTALITY_MODEL SelectedChildMortalityModel;
//EN Child mortality baseline risk
double ChildMortalityBaseRisk[CHILD_MORTALITY_AGE][SEX];
//EN Child mortality relative risk
double ChildMortalityRelativeRisks[CHILD_MORTALITY_AGE][CHILD_MORTALITY_RISKS];
//EN Child mortality time trend
double ChildMortalityTrend[CHILD_MORTALITY_AGE][CHILD_MORTALITY_YEARS];
};
parameter_group PG_ChildMortality //EN Child mortality
{
SelectedChildMortalityModel, ChildMortalityBaseRisk,
ChildMortalityRelativeRisks, ChildMortalityTrend
};
The ChildMortalityCalibrator actor declaration¶
To allow calibration of the baseline hazard of child mortality, we introduce a single instance calibration actor. This actor has a single event: the calibration of the model by finding baseline hazards that, when applying relative risks and for a given population composition determined at this moment of time, leads to the same number of deaths as using the base model. As parameters cannot be changed during simulation, the calibrated values are stored in states of the calibrator.
actor_set ChildMortalityCalibrator asTheChildMortalityCalibrator;
link Person.lChildMortalityCalibrator ChildMortalityCalibrator.mlBabies[];
actor ChildMortalityCalibrator
{
double mort_male_0; //EN Child mortality risk male age 0
double mort_male_1; //EN Child mortality risk male age 1
double mort_male_2; //EN Child mortality risk male age 2
double mort_male_3; //EN Child mortality risk male age 3
double mort_male_4; //EN Child mortality risk male age 4
double mort_female_0; //EN Child mortality risk female age 0
double mort_female_1; //EN Child mortality risk female age 1
double mort_female_2; //EN Child mortality risk female age 2
double mort_female_3; //EN Child mortality risk female age 3
double mort_female_4; //EN Child mortality risk female age 4
logical calibration_is_done = { FALSE }; //EN Calibration is done
void Start(); //EN Starts the calibrator actor
//EN Child mortality calibration event
event timeChildMortalityCalibrationEvent, ChildMortalityCalibrationEvent;
integer int_age = self_scheduling_int(age);
};
void ChildMortalityCalibrator::Start()
{
time = MIN(SIM_YEAR_RANGE);
}
Implementation of the calibration event¶
The event is called five years into projected time, when all children below five were born in the simulation and know their mother’s characteristics, which are used to calculate relative risks in the model. The calibration is performed by a binary search algorithm. As part of the calibration, the population composition by age, sex, and the selected mother’s characteristics is determined.
TIME ChildMortalityCalibrator::timeChildMortalityCalibrationEvent()
{
// if not done yet and a model with alignment is chosen
if (!calibration_is_done
&& ( SelectedChildMortalityModel == SCMM_MICRO_ALIGNED_MACRO_TRENDS
|| SelectedChildMortalityModel == SCMM_MICRO_ALIGNED_MICRO_TRENDS))
// return the moment in which the calibration is to be performed
{
return MIN(CHILD_MORTALITY_YEARS);
}
// else never call the calibration event
else return TIME_INFINITE;
}
void ChildMortalityCalibrator::ChildMortalityCalibrationEvent()
{
// local matrices
double dDeaths[SIZE(CHILD_MORTALITY_AGE)][SIZE(SEX)]; // Number expected deaths
double dProb[SIZE(CHILD_MORTALITY_AGE)][SIZE(SEX)]; // Death probability
double dBase[SIZE(CHILD_MORTALITY_AGE)][SIZE(SEX)]; // Baseline Hazard found in simulation
double dRelRisks[SIZE(CHILD_MORTALITY_AGE)][SIZE(PRIMARY_LEVEL)][SIZE(MOTHER_AGE_CLASS)]; // Relative risks
long nPop[SIZE(CHILD_MORTALITY_AGE)][SIZE(SEX)][SIZE(PRIMARY_LEVEL)][SIZE(MOTHER_AGE_CLASS)];// Pop sizes
// Initialize matrices
// set population sizes nPop and expected deaths nDeaths to 0
// sets death probabilities dProb by age and sex for the year in which the model is calibrated
for (int nAge = 0; nAge < SIZE(CHILD_MORTALITY_AGE); nAge++)
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
dProb[nAge][nSex] = 1.0 - exp(-MortalityTable[nAge][nSex]
* MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, MIN(CHILD_MORTALITY_YEARS))][nSex]);
dDeaths[nAge][nSex] = 0.0;
for (int nEduc = 0; nEduc < SIZE(PRIMARY_LEVEL); nEduc++)
{
for (int nMotherAge = 0; nMotherAge < SIZE(MOTHER_AGE_CLASS); nMotherAge++)
{
nPop[nAge][nSex][nEduc][nMotherAge] = 0.0;
}
}
}
}
// Initialize relative risks dRelRisks by age, mothers education, and mothers age
for (int nAge = 0; nAge < SIZE(CHILD_MORTALITY_AGE); nAge++)
{
dRelRisks[nAge][PL_NO][MAC_14] = ChildMortalityRelativeRisks[nAge][CMR_NOPRIM]
* ChildMortalityRelativeRisks[nAge][CMR_AGE14];
dRelRisks[nAge][PL_ENTER][MAC_14] = ChildMortalityRelativeRisks[nAge][CMR_PRIMDROP]
* ChildMortalityRelativeRisks[nAge][CMR_AGE14];
dRelRisks[nAge][PL_GRAD][MAC_14] = ChildMortalityRelativeRisks[nAge][CMR_AGE14];
dRelRisks[nAge][PL_NO][MAC_16] = ChildMortalityRelativeRisks[nAge][CMR_NOPRIM]
* ChildMortalityRelativeRisks[nAge][CMR_AGE16];
dRelRisks[nAge][PL_ENTER][MAC_16] = ChildMortalityRelativeRisks[nAge][CMR_PRIMDROP]
* ChildMortalityRelativeRisks[nAge][CMR_AGE16];
dRelRisks[nAge][PL_GRAD][MAC_16] = ChildMortalityRelativeRisks[nAge][CMR_AGE16];
dRelRisks[nAge][PL_NO][MAC_17P] = ChildMortalityRelativeRisks[nAge][CMR_NOPRIM];
dRelRisks[nAge][PL_ENTER][MAC_17P] = ChildMortalityRelativeRisks[nAge][CMR_PRIMDROP];
dRelRisks[nAge][PL_GRAD][MAC_17P] = 1.0;
}
// Population Count
// calculates population sizes nPop by age, sex, mothers education, and mothers age group
// calculates expected deaths dDeaths by age and sex
for (long nJ = 0; nJ < asAllPersons->Count(); nJ++)
{
Person * prPerson = new Person();
prPerson = asAllPersons->Item(nJ);
if (prPerson->integer_age <= MAX(CHILD_MORTALITY_AGE) && prPerson->mother_known)
{
for (int nAge = 0; nAge < SIZE(CHILD_MORTALITY_AGE); nAge++)
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
if (nSex == prPerson->sex && nAge == prPerson->integer_age)
{
dDeaths[nAge][nSex] = dDeaths[nAge][nSex] + dProb[nAge][nSex];
if (prPerson->mother_educ_0 == TRUE)
{
if (prPerson->mother_max_14 == TRUE) nPop[nAge][nSex][PL_NO][MAC_14]++;
else if (prPerson->mother_max_16 == TRUE) nPop[nAge][nSex][PL_NO][MAC_16]++;
else nPop[nAge][nSex][PL_NO][MAC_17P]++;
}
else if (prPerson->mother_educ_1 == TRUE)
{
if (prPerson->mother_max_14 == TRUE) nPop[nAge][nSex][PL_ENTER][MAC_14]++;
else if (prPerson->mother_max_16 == TRUE) nPop[nAge][nSex][PL_ENTER][MAC_16]++;
else nPop[nAge][nSex][PL_ENTER][MAC_17P]++;
}
else
{
if (prPerson->mother_max_14 == TRUE) nPop[nAge][nSex][PL_GRAD][MAC_14]++;
else if (prPerson->mother_max_16 == TRUE) nPop[nAge][nSex][PL_GRAD][MAC_16]++;
else nPop[nAge][nSex][PL_GRAD][MAC_17P]++;
}
}
}
}
}
}
// find calibrated baselines
for (int nAge = 0; nAge < SIZE(CHILD_MORTALITY_AGE); nAge++)
{
for (int nSex = 0; nSex < SIZE(SEX); nSex++)
{
double dUpper = 2.0;
double dLower = 0.0;
double dCenter = 1.0;
double dNumberDeaths = 0.0;
int nIterations = 10000;
while ( abs(dNumberDeaths - dDeaths[nAge][nSex]) > 0.0001 && nIterations > 0 )
{
nIterations--;
dBase[nAge][nSex] = ( dLower + dUpper) / 2.0;
dNumberDeaths = 0.0;
//Celculate numer of deaths for given dBase
for (int nEduc = 0; nEduc < SIZE(PRIMARY_LEVEL); nEduc++)
{
for (int nMotherAge = 0; nMotherAge < SIZE(MOTHER_AGE_CLASS); nMotherAge++)
{
dNumberDeaths = dNumberDeaths + nPop[nAge][nSex][nEduc][nMotherAge]
* (1-exp(-dBase[nAge][nSex] * dRelRisks[nAge][nEduc][nMotherAge]));
double hugo = nPop[nAge][nSex][nEduc][nMotherAge];
double emil = dBase[nAge][nSex];
double gustav = dRelRisks[nAge][nEduc][nMotherAge];
double hannelore = dDeaths[nAge][nSex];
double anton = 0;
}
}
// shrink search interval
if ( dNumberDeaths > dDeaths[nAge][nSex]) dUpper = dBase[nAge][nSex];
else dLower = dBase[nAge][nSex];
}
int hugo = nIterations;
}
}
// set states
mort_male_0 = dBase[0][MALE]; mort_female_0 = dBase[0][FEMALE];
mort_male_1 = dBase[1][MALE]; mort_female_1 = dBase[1][FEMALE];
mort_male_2 = dBase[2][MALE]; mort_female_2 = dBase[2][FEMALE];
mort_male_3 = dBase[3][MALE]; mort_female_3 = dBase[3][FEMALE];
mort_male_4 = dBase[4][MALE]; mort_female_4 = dBase[4][FEMALE];
calibration_is_done = TRUE;
}
The child mortality event of the actor Person¶
Individual child mortality rates are implemented as derived states, accessing the “parameters” found by the calibrator.
actor Person
{
//EN Child mortality overrides general mortality module
logical child_mortality_overrides = calendar_year >= MIN(CHILD_MORTALITY_YEARS)
&& SelectedChildMortalityModel != SCMM_MACRO
&& integer_age <= MAX(CHILD_MORTALITY_AGE)
&& mother_known;
logical mother_known = { FALSE }; //EN Mother is known
logical mother_educ_0 = { FALSE }; //EN Mother never entered primary school
logical mother_educ_1 = { FALSE }; //EN Mother dropped out of primary school
logical mother_max_14 = { FALSE }; //EN Mother was 14 or below at birth
logical mother_max_16 = { FALSE }; //EN Mother was 15 or 16 at birth
//EN Baseline mortality
double child_mortality = (lChildMortalityCalibrator == NULL) ? 0.0 :
(integer_age == 0 && sex == MALE) ? lChildMortalityCalibrator->mort_male_0 :
(integer_age == 1 && sex == MALE) ? lChildMortalityCalibrator->mort_male_1 :
(integer_age == 2 && sex == MALE) ? lChildMortalityCalibrator->mort_male_2 :
(integer_age == 3 && sex == MALE) ? lChildMortalityCalibrator->mort_male_3 :
(integer_age == 4 && sex == MALE) ? lChildMortalityCalibrator->mort_male_4 :
(integer_age == 0 && sex == FEMALE) ? lChildMortalityCalibrator->mort_female_0 :
(integer_age == 1 && sex == FEMALE) ? lChildMortalityCalibrator->mort_female_1 :
(integer_age == 2 && sex == FEMALE) ? lChildMortalityCalibrator->mort_female_2 :
(integer_age == 3 && sex == FEMALE) ? lChildMortalityCalibrator->mort_female_3 :
(integer_age == 4 && sex == FEMALE) ? lChildMortalityCalibrator->mort_female_4 : 0.0;
//EN Child mortality event
event timeChildMortalityEvent, ChildMortalityEvent;
};
According to the user’s model choices, the time function of the child mortality event determines waiting times for three alternative options. Note that the module can be deactivated, in which case no events are scheduled and the general model (in mortality.mpp) handles mortality.
TIME Person::timeChildMortalityEvent()
{
double dEventTime = TIME_INFINITE;
double dHazard = 0.0;
if (child_mortality_overrides) // at risk (the model is used and overrides the overall mortality)
{
// base risk * trend
// unaligned model
if (SelectedChildMortalityModel == SCMM_MICRO_NOT_ALIGNED)
{
dHazard = ChildMortalityBaseRisk[integer_age][sex]
* ChildMortalityTrend[integer_age][RANGE_POS(CHILD_MORTALITY_YEARS, calendar_year)];
}
// aligned with overall trend
else if (SelectedChildMortalityModel == SCMM_MICRO_ALIGNED_MACRO_TRENDS)
{
dHazard = child_mortality
* MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, calendar_year)][sex]
/ MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, MIN(CHILD_MORTALITY_YEARS))][sex];
}
// aligned with specific child mortality trend
else
{
dHazard = child_mortality
* ChildMortalityTrend[integer_age][RANGE_POS(CHILD_MORTALITY_YEARS, calendar_year)];
}
// relativ risks
if (mother_educ_0) dHazard = dHazard * ChildMortalityRelativeRisks[integer_age][CMR_NOPRIM];
else if (mother_educ_1) dHazard = dHazard * ChildMortalityRelativeRisks[integer_age][CMR_PRIMDROP];
if (mother_max_14) dHazard = dHazard * ChildMortalityRelativeRisks[integer_age][CMR_AGE14];
else if (mother_max_16) dHazard = dHazard * ChildMortalityRelativeRisks[integer_age][CMR_AGE16];
// if hazard is positive calculate event time
if (dHazard > 0) dEventTime = WAIT(-log(RandUniform(4)) / dHazard);;
}
return dEventTime;
}
void Person::ChildMortalityEvent()
{
MortalityEvent();
}
Changes in the simulation engine DYNAMIS-POP-MRT.mpp¶
The only change in the simulation engine is the creation of the calibration actor in the Simulation() function; the code can be added, e.g., after the primary school actor is created:
// Create the child mortality calibration actor
ChildMortalityCalibrator *paChildMortalityCalibrator = new ChildMortalityCalibrator();
paChildMortalityCalibrator->Start();
Changes in the general mortality module Mortality.mpp¶
Assessing whether a person is at risk now also includes assessing whether mortality is overridden by the child mortality module.
TIME Person::timeMortalityEvent()
{
...
// check if a person is at risk
if (dMortalityHazard > 0.0 && in_projected_time && !child_mortality_overrides)
...
7.16.4. Changes in the PersonCore.mpp module¶
In the Start() function in PersonCore, additional code is needed to store mother’s characteristics at birth. Also, a link is established to the calibration actor.
....
else if (person_type == PT_CHILD)
{
...
// Mother's characteristics
mother_known = TRUE; //Mother is known
if (peMother->primary_level == PL_NO) mother_educ_0 = TRUE;
else if (peMother->primary_level == PL_ENTER) mother_educ_1 = TRUE;
if (peMother->integer_age <= 14) mother_max_14 = TRUE;
else if (peMother->integer_age <= 16) mother_max_16 = TRUE;
// establish a link to the calibrator actor
lChildMortalityCalibrator = asTheChildMortalityCalibrator->Item(0);
}
....
7.17. Step 17: Migration refined¶
7.17.1. Overview¶
At this step, we add an alternative parameter for migration probabilities, adding the level of education as a dimension. The user can choose between the base and refined versions of the model.
7.17.2. Concepts¶
This is a simple step in which just two additional parameters and their roles in the time function of the migration event are added.
7.17.3. How to reproduce this step¶
At this step, two parameters are added, the first allowing the user to choose which migration parameters to use (i.e., with or without education as a dimension), and the second for migration probabilities by province, age group, and as the added dimension, education.
//EN Use parameters by education
logical ModelByEducation;
//EN Migration probability by education
double MigrationProbabilityEduc[PRIMARY_LEVEL][SEX][AGE5_PART][PROVINCE_NAT];
The time function of the migration event now checks which parameter to use:
// get the probability to move
double dMoveProb = 0.0;
if (ModelByEducation) dMoveProb = MigrationProbabilityEduc[primary_level][sex][nAge5][province_nat];
else dMoveProb = MigrationProbability[sex][nAge5][province_nat];
7.18. Step 18: Extending Table and Micro-Data Output¶
7.18.1. Overview¶
At this step, we extend the micro-data output options and the list of variables that can be written to the output CSV file. Users can now choose which variables to include in the output, and output for various points in time. Users can choose the timeframe of outputs (e.g., 2015 - 2065) as well as the time interval between output events (e.g., every five years).
The second addition concerns the list of output tables. We add a broad variety of output tables whose output allows for a rich analysis of simulation results.
7.18.2. Concepts¶
The following paragraphs address adding a header line to CSV micro-data input and output files.
Adding a header line with variable names to csv files¶
Immediately after opening a CSV file for output, a header fline can be written using a write_header() function. The string variable has to be of type stc::string or a compatible type like CStringA (the latter allowing for easy concatenating using the + operator; see example):
CStringA myString = "ID,"; // Actor ID
myString = myString + "TIME,"; // Time
...
out_csv.open(MicroRecordFileName);
out_csv.write_header(myString.GetString());
For reading a header file, the function is read_header():
in_csv.open(PersonMicroDataFile);
std::string headerLine = in_csv.read_header();
7.18.3. How to reproduce this step¶
Extending the Micro-Data Output Options¶
Classifications and Parameters¶
New classifications are declared for the list of variables and for the extended output options. They are used to add or modify corresponding parameters.
classification OUTPUT_VARIABLES //EN List of output variables
{
OV_TIME, //EN Time
OV_WEIGHT, //EN Weight
OV_BIRTH, //EN Time of birth
OV_SEX, //EN Sex
OV_PROVINCE, //EN Province
OV_EDUC, //EN Education
OV_POB, //EN Province of birth
OV_UNION, //EN Union formation time
OV_PARITY, //EN Number of births
OV_LASTBIR //EN Time of last birth
};
classification OUTPUT_TIMES //EN Micro-data output times
{
OT_FIRST, //EN Time of first output
OT_LAST, //EN Time of last output
OT_INTERVAL //EN Time interval (0 for no repetition)
};
parameters
{
...
double TimeMicroOutput[OUTPUT_TIMES]; //EN Time(s) of micro-data output
logical SelectedOutput[OUTPUT_VARIABLES]; //EN Output variable selection
};
Presimulation¶
In presimulation, the output file is opened and a header line is written, containing the variable names:
void PreSimulation()
{
if (WriteMicrodata)
{
CStringA myString = "ID,"; // Actor ID
if (SelectedOutput[OV_TIME]) myString = myString + "TIME,"; // Time
if (SelectedOutput[OV_WEIGHT]) myString = myString + "WEIGHT,"; // Weight
if (SelectedOutput[OV_BIRTH]) myString = myString + "BIRTH,"; // Time of birth
if (SelectedOutput[OV_SEX]) myString = myString + "MALE,"; // Sex
if (SelectedOutput[OV_PROVINCE]) myString = myString + "PROVINCE,"; // Province
if (SelectedOutput[OV_EDUC]) myString = myString + "EDUCATION,"; // Education
if (SelectedOutput[OV_POB]) myString = myString + "PROV_BIRTH,";// Province of birth
if (SelectedOutput[OV_UNION]) myString = myString + "UNION,"; // Union formation time
if (SelectedOutput[OV_PARITY]) myString = myString + "PARITY,"; // Number of births
if (SelectedOutput[OV_LASTBIR]) myString = myString + "LAST_BIRTH,";// time of last birth
out_csv.open(MicroRecordFileName);
out_csv.write_header(myString.GetString());
}
}
The output event¶
The output event is modified to allow output at various points in time (the timeinterval being a parameter) and to output the variables chosen by the user from the list of variables.
actor Person
{
TIME time_microdata_output = { TIME_INFINITE }; //EN Time for microdata output
void WriteMicroRecord_Start(); //EN Initialization for microdata output event
hook WriteMicroRecord_Start, Start;
event timeWriteMicroRecord, WriteMicroRecord; //EN Write micro-data record event
};
void Person::WriteMicroRecord_Start()
{
if (WriteMicrodata && TimeMicroOutput[OT_FIRST] >= time)
{
time_microdata_output = TimeMicroOutput[OT_FIRST];
}
else time_microdata_output = TIME_INFINITE;
}
TIME Person::timeWriteMicroRecord()
{
if (GetReplicate()==0) return time_microdata_output;
else return TIME_INFINITE;
}
void Person::WriteMicroRecord()
{
// calculate variables
double dUnionFormation = -99;
if (sex == FEMALE && time_of_union_formation < time) dUnionFormation = time_of_union_formation;
double dTimeLastBirth = -99;
if (sex == FEMALE && parity > 0) dTimeLastBirth = time_of_last_birth;
// Push the fields into the output record.
out_csv << actor_id; // Actor ID
if ( SelectedOutput[OV_TIME]) out_csv << time; // Time
if ( SelectedOutput[OV_WEIGHT]) out_csv << ActorWeight; // Weight
if ( SelectedOutput[OV_BIRTH]) out_csv << time_of_birth; // Time of birth
if ( SelectedOutput[OV_SEX]) out_csv << (int)sex; // Sex
if ( SelectedOutput[OV_PROVINCE]) out_csv << (int)province_nat; // Province
if ( SelectedOutput[OV_EDUC]) out_csv << (int)primary_level; // Education
if ( SelectedOutput[OV_POB]) out_csv << (int)province_birth; // Province of birth
if ( SelectedOutput[OV_UNION]) out_csv << dUnionFormation; // Union formation time
if ( SelectedOutput[OV_PARITY]) out_csv << parity; // Number of births
if (SelectedOutput[OV_LASTBIR]) out_csv << dTimeLastBirth; // time of last birth
// All fields have been pushed, now write the record.
out_csv.write_record();
// set next output
if (time_microdata_output + TimeMicroOutput[OT_INTERVAL] > time &&
time_microdata_output + TimeMicroOutput[OT_INTERVAL] <= TimeMicroOutput[OT_LAST])
{
time_microdata_output = time_microdata_output + TimeMicroOutput[OT_INTERVAL];
}
else
{
time_microdata_output = TIME_INFINITE;
}
}
Tables¶
At this step, we re-organize the whole table output of the model. The model produces a series of tables organized in seven table groups. All code is in the module Tables.mpp. The following discussion covers the entire code of the module and replaces the previous code.
Table groups¶
Population
- Total population by projected year, sex, and province
- Total population by projected year, age group, sex, and province
- Simulated starting population by age, sex, and province
Fertility
- Age-specific fertility rates by projected year
- Number of births by sex and projected year
- Number of births and first births by age group of mother and projected year
- Average age at birth and at first birth by education and projected year
Mortality
- Death rates and number of deaths by sex, age, and projected year
- Death rates and number of deaths by age group and projected year
- Child mortality by birth cohort and single year of age 0-4
- Mortality trends by sex (the trend factors calculated internally to scale the standard life table to meet the given scenario of future period life expectancy)
Migration
- Internal migration rates by age group and sex
- Number of immigrants by sex, age group, and province of destination by simulated year
- Emigration rates and number of emigrants by age group, sex, and province by projected year
Union
- First union formation by education, age, and simulated year: rates and proportion of women ever in a union
- Average age at first union formation by education and projected year
Education
- Population by province, age group, sex, and education by projected year
- Education composition of 15-year-old by sex, province of birth, and projected year
- Education composition of 15-year-old by sex, province of residence, and projected year
Female life-course experiences: cohort measures on own survival, union formation, education, fertility, and child deaths
- Average age at union formation
- Average age at first birth
- Proportion surviving until age 10
- Proportion surviving until age 20
- Proportion graduating from primary school
- Cohort fertility (subject to mortality)
- Cohort fertility of mothers (subject to mortality)
- Average number of children dying < age 5 per woman
- Average number of children dying < age 5 per mother
/////////////////////////////////////////////////////////////////////////////////////////
// Table groups for organizing tables in the user interface //
/////////////////////////////////////////////////////////////////////////////////////////
table_group TG01_PopulationTables //EN Population
{
TabTotalPopulationSexProvince,
TabPopulationAgeSexProvince,
TabSimulatedStartingPopulation
};
table_group TG_02_FertilityTables //EN Fertility
{
TabFertilityRates,
TabNumberBirths,
TabBirthsByAgeGroup,
TabAgeAtBirth
};
table_group TG_03_MortalityTables //EN Mortality
{
TabDeathRates,
TabDeathsByAgeGroup,
TabCohortChildMortality,
TabMortalityTrendFactor
};
table_group TG_04_MigrationTables //EN Migration
{
TabInternalMigrationRate,
TabImmigrationSexProvinceAge,
TabEmigration
};
table_group TG_05_UnionTables //EN First union formation
{
TabUnionFormation,
TabAgeAtFirstUnionFormation
};
table_group TG_06_EducationTables //EN Education
{
TabPopProvAgeEducSex,
TabEduc15ByPob,
TabEduc15ByPor
};
table_group TG_07_CohortTables //EN Cohort measures
{
TabMothersExperience
};
States and dimensions used in tables¶
Some states are used exclusively in tables and are therefore defined here. If the tables are not used, these states can be deleted with the table code to free memory space. Most states (and also tables) belong to the actor Person. The exceptions are states and a table belonging to the actor ChildMortalityCalibrator. The child mortality calibrator actor has only one instance and thus can more efficiently implement a table with values that do not vary between persons (calibration results in our example).
The logical state count_parity is an indicator introduced to exclude the number of children assigned to newly created immigrants at immigration from birth counts. It becomes true almost immediately after an actor is created and prevents inaccurate birth counts.
/////////////////////////////////////////////////////////////////////////////////////////
// States and dimensions used in tables only (can be removed with tables) //
/////////////////////////////////////////////////////////////////////////////////////////
partition LITTLE_TIME{ 0.001 }; //EN A very small time period
partition AGE_15_17{ 15,17 }; //EN Age groups
partition AGE_MORT{ 1,5,15,65 }; //EN Age groups
range SIM_YOB_RANGE{ 2013,2060 }; //EN Year of birth
range CHILD_MORT_RANGE{ 0,14 }; //EN Child mortality age range
actor Person
{
logical is_child = (integer_age < 15); //EN Child < 15
logical is_old = (integer_age >= 60); //EN Person 60+
logical is_aded0 = (!is_child && !is_old && primary_level == PL_NO); //EN Person 15-59 never enterd primary
logical is_aded1 = (!is_child && !is_old && primary_level == PL_ENTER); //EN Person 15-59 uncompleted primary
logical is_aded2 = (!is_child && !is_old && primary_level == PL_GRAD); //EN Person 15-59 primary graduate
FERTILE_AGE_RANGE fert_age = COERCE(FERTILE_AGE_RANGE, integer_age); //EN Age
SIM_YEAR_RANGE sim_yob = COERCE(SIM_YOB_RANGE, year_of_birth); //EN Year of birth
CHILD_MORT_RANGE sim_cmr = COERCE(CHILD_MORT_RANGE,integer_age); //EN Age
//EN Indicator to exclude the number of children assigned at immigration from birth counts
logical count_parity = self_scheduling_split(duration(set_alive, TRUE), LITTLE_TIME);
};
actor ChildMortalityCalibrator
{
//EN Calendar Year
SIM_YEAR_RANGE c_year = COERCE(SIM_YEAR_RANGE,self_scheduling_int(time));
//EN Mortality trend calibration factor - male
double MaleMortalityTrend = MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, c_year)][MALE];
//EN Mortality trend calibration factor - female
double FemaleMortalityTrend = MortalityTrend[RANGE_POS(SIM_YEAR_RANGE, c_year)][FEMALE];
};
General Population Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// General Population Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabTotalPopulationSexProvince //EN Total population by sex and province
[in_projected_time]
{
sex + *
{
duration() //EN Average Population
}
* province_nat +
* sim_year
};
table Person TabPopulationAgeSexProvince //EN Total population by age group, sex and province
[in_projected_time]
{
sex+ *
province_nat+ *
{
duration() //EN Average Population
}
* split(integer_age, AGE5_PART) //EN Age group
* sim_year //EN Calendar Year
};
table Person TabSimulatedStartingPopulation //EN Simulated starting population by age, sex and province
[trigger_entrances(in_projected_time, TRUE)]
{
sex + *
{
unit //EN Persons
}
* integer_age +
* province_nat +
};
Fertility Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Fertility Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabFertilityRates //EN Fertility rates
[ in_projected_time && WITHIN(FERTILE_AGE_RANGE, integer_age)
&& sex == FEMALE && count_parity ]
{
{
parity / duration() //EN Fertility rates decimals=4
}
* fert_age
* sim_year
};
table Person TabNumberBirths //EN Number of births
[in_projected_time && person_type == PT_CHILD]
{
{
entrances(set_alive, TRUE) //EN Births
}
* sim_year
* sex +
};
table Person TabBirthsByAgeGroup //EN Births by age of mother
[in_projected_time && count_parity]
{
{
parity, //EN Births
transitions(parity,0,1) //EN First births
}
* split(integer_age,AGE_15_17) + //EN Age group
* sim_year //EN Calendar Year
};
table Person TabAgeAtBirth //EN Age at birth
[in_projected_time && sex == FEMALE && count_parity]
{
primary_level + *
{
value_at_transitions(parity,0,1,age) / transitions(parity,0,1), //EN Average age at first birth decimals=2
value_at_changes(parity,age) / changes(parity) //EN Average age at birth decimals=2
}
*sim_year //EN Calendar Year
};
Mortality Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Mortality Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabDeathRates //EN Death rates and events by age
[in_projected_time]
{
sex + *
{
transitions(alive, TRUE, FALSE), //EN Number of persons dying at this age
transitions(alive, TRUE, FALSE) / duration() //EN Death Rate decimals=4
}
* integer_age
* sim_year
};
table Person TabDeathsByAgeGroup //EN Deaths by age group
[in_projected_time]
{
{
transitions(alive, TRUE, FALSE), //EN Number of persons dying at this age
transitions(alive, TRUE, FALSE) / duration() //EN Death Rate decimals=4
}
* split(integer_age, AGE_MORT) + //EN Age group
* sim_year //EN Calendar Year
};
table Person TabCohortChildMortality //EN Cohort child mortality rates
[in_projected_time && WITHIN(SIM_YOB_RANGE, year_of_birth) && WITHIN(CHILD_MORT_RANGE, integer_age)]
{
{
transitions(alive, TRUE, FALSE) / duration() //EN Mortality decimals=4
}
* sim_cmr //EN Age
* sim_yob //EN Year of birth
};
table ChildMortalityCalibrator TabMortalityTrendFactor //EN Mortality calibration factor
{
c_year *
{
value_out(MaleMortalityTrend), //EN Male Mortality Trend decimals=4
max_value_out(FemaleMortalityTrend) //EN Female Mortality Trend decimals=4
}
};
Migration Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Migration Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabInternalMigrationRate //EN Internal migration rate
[ in_projected_time ]
{
sex + *
{
number_moves / duration() //EN Migration rate decimals=4
}
* split(integer_age, AGE5_PART) //EN Age group
* province_nat +
};
table Person TabImmigrationSexProvinceAge //EN Immigration by sex, province and age group
[person_type == PT_IMMIGRANT && in_projected_time && trigger_entrances(set_alive, TRUE)]
{
sex + *
province_nat + *
{
unit //EN Immigrants
}
* split(integer_age, AGE5_PART)+ //EN Age group
* sim_year
};
table Person TabEmigration //EN Emigration rates and numbers
[in_projected_time]
{
sex + *
province_nat + *
{
transitions(has_emigrated, FALSE, TRUE) / duration(), //EN Emigration rates decimals=4
transitions(has_emigrated, FALSE, TRUE) //EN Emigrants
}
* split(integer_age, AGE5_PART)+ //EN Age group
* sim_year
};
Education Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Education Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabPopProvAgeEducSex //EN Population by province, age group, sex and education
[in\_projected\_time]
{
province\_nat + \*
sex + \*
{
duration(is\_child,TRUE), //EN Children < 15
duration(is\_aded0, TRUE), //EN Persons 15-59 never entered primary school
duration(is\_aded1, TRUE), //EN Persons 15-59 primary school non-completer
duration(is\_aded2, TRUE), //EN Persons 15-59 primary school graduate
duration(is\_old,TRUE) //EN Persons 60+
}
\* sim\_year //EN Calendar Year
};
table Person TabEduc15ByPob //EN Education composition of 15 year old by province of birth
[ integer\_age==15 && in\_projected\_time ]
{
province\_birth + \*
sex + \*
{
duration(primary\_level,PL\_NO) / duration(), //EN Never entered primary school decimals=4
duration(primary\_level,PL\_ENTER) / duration(), //EN Primary school non-completer decimals=4
duration(primary\_level,PL\_GRAD) / duration() //EN Primary school graduate decimals=4
}
\* sim\_year
};
table Person TabEduc15ByPor //EN Education composition of 15 year old by province of residence
[integer\_age == 15 && in\_projected\_time]
{
province\_nat + \*
sex + \*
{
duration(primary\_level,PL\_NO) / duration(), //EN Never entered primary school decimals=4
duration(primary\_level,PL\_ENTER) / duration(), //EN Primary school non-completer decimals=4
duration(primary\_level,PL\_GRAD) / duration() //EN Primary school graduate decimals=4
}
\* sim\_year
};
table Person TabEduc2015 //EN Population by education 2015
[calendar\_year == 2015]
{
in\_capital + \*
sex + \*
{
duration(primary\_level,PL\_NO), //EN Never entered primary school
duration(primary\_level,PL\_ENTER), //EN Primary school non-completer
duration(primary\_level,PL\_GRAD) //EN Primary school graduate
}
\*split(integer\_age, AGE\_FIVE) //EN Age group
};
table Person TabEduc2040 //EN Population by education 2040
[calendar\_year == 2040]
{
in\_capital + \*
sex + \*
{
duration(primary\_level,PL\_NO), //EN Never entered primary school
duration(primary\_level,PL\_ENTER), //EN Primary school non-completer
duration(primary\_level,PL\_GRAD) //EN Primary school graduate
}
\*split(integer\_age, AGE\_FIVE) //EN Age group
};
table Person TabEduc2065 //EN Population by education 2065
[calendar\_year == 2065]
{
in\_capital + \*
sex + \*
{
duration(primary\_level,PL\_NO), //EN Never entered primary school
duration(primary\_level,PL\_ENTER), //EN Primary school non-completer
duration(primary\_level,PL\_GRAD) //EN Primary school graduate
}
\*split(integer\_age, AGE\_FIVE) //EN Age group
};
Union Formation Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Union Tables //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabUnionFormation //EN First union formation by education and age
[WITHIN(AGE_UNION, integer_age) && sex == FEMALE && in_projected_time && count_parity]
{
{
//EN Hazard calculated from parameters decimals=4
max_value_in(union_formation_hazard),
//EN Simulated hazard decimals=4
transitions(in_union, FALSE, TRUE) / duration(in_union, FALSE),
//EN Proportion of women who ever entered a union decimals=4
duration(in_union,TRUE) / duration()
}
* primary_level +
* age_union
* sim_year
};
table Person TabAgeAtFirstUnionFormation //EN Age at first union formation
[ sex == FEMALE && in_projected_time && count_parity ]
{
{
//EN Average age at first union formation decimals=2
value_at_transitions(in_union, FALSE, TRUE,age) / transitions(in_union, FALSE, TRUE)
}
* primary_level +
* sim_year
};
Female Life-Course Experience Tables¶
/////////////////////////////////////////////////////////////////////////////////////////
// Female lifecourse experiences //
/////////////////////////////////////////////////////////////////////////////////////////
table Person TabMothersExperience //EN Cohort measures of fertility and child deaths
[ in_projected_time && WITHIN(SIM_YOB_RANGE, year_of_birth) && person_type == PT_CHILD && sex==FEMALE]
{
{
//EN Average age at union formation decimals=4
value_at_transitions(in_union, FALSE, TRUE,age) / transitions(in_union, FALSE, TRUE),
//EN Average age at first birth decimals=4
value_at_transitions(parity,0,1,age) / transitions(parity,0,1),
//EN Proportion surviving until age 10 decimals=4
transitions(integer_age, 9,10) / unit,
//EN Proportion surviving until age 20 decimals=4
transitions(integer_age, 14,15) / unit,
//EN Proportion surviving until age 20 decimals=4
transitions(integer_age, 19,20) / unit,
//EN Proportion graduating from primary school until age 20 decimals=4
entrances(primary_level, PL_GRAD) / unit,
//EN Cohort Fertility (subject to mortality) decimals=4
parity / unit,
//EN Cohort Fertility of mothers (subject to mortality) decimals=4
parity / transitions(parity,0,1),
//EN Average number of children dying < age 5 per woman decimals=4
infant_deaths / unit,
//EN Average number of children dying < age 5 per mother decimals=4
infant_deaths / transitions(parity,0,1)
}
* sim_yob //EN Year of birth
};