5.13. Step 13: Refined Fertility

5.13.1. Model Description

At this step we add a refined module for fertility which goes beyond age-specific rates by accounting for education, union formation, and parity. The new modules can replace or compliment the base model, in the latter case the aggregate outcomes of the refined model are aligned to the outcome of the base model.

5.13.2. The new FertilityRefinedEducUnionParity.mpp Module

This module implements detailed fertility models accounting for parity, geographical location, time since previous birth, partnership status, and an extendable list of other characteristics (currently grouping women by primary education outcome). Users can choose to align the aggregated results to the base model, either for total outcome (number of births) or births by age.

The module has four parameters:

  • Model selection: 4 options, base model, refined model, refined model aligned to number of births of base model, refined model aligned to number of births by age of base model.
  • First Births Rates by population group (currently: education), location (currently: region), partnership status, age.
  • Higher order births: hazard regression parameters with a duration baseline (age; time since last birth) and relative risks for education and age group.
  • Birth trends: an additional relative risk by calendar year and parity.

Simulated birth events only occure to residents in the projected time, as biographic information on parity and the most recent time of birth are contained in the starting population. As for immigrants no biographic information is available, immigrant women sample parity and timing of last birth from the female resident population of same education and partnership status and district. The function GetImmigrantParityTimeBirth() is hooked to the ImmigrationEvent().

This module can be added or removed from the model without any code change to other modules. It just requires the initialization of the states parity and time_since_last_birth_start based on the starting population file; this is done in the Start() function.



////////////////////////////////////////////////////////////////////////////////////////////////////
// Actor Sets
////////////////////////////////////////////////////////////////////////////////////////////////////

//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;

//EN Female residents by age, district of residence and education
actor_set Person asWomenAgeEducDist[integer_age][educ_one_fate][district_nat]
    filter is_alive && is_resident && sex==FEMALE;

////////////////////////////////////////////////////////////////////////////////////////////////////
// Types
////////////////////////////////////////////////////////////////////////////////////////////////////

classification BIRTH1_GROUP                         //EN Population Groups
{
    B1G_00,                                         //EN Never entered school
    B1G_01,                                         //EN Primary dropout
    B1G_02                                          //EN Primary graduate
};

classification BIRTH1_LOC                           //EN Geographical Location
{
    B1L_00,                                         //EN Far-West
    B1L_01,                                         //EN West
    B1L_02,                                         //EN Central
    B1L_03,                                         //EN The Capital
    B1L_04                                          //EN East
};

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 and 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
////////////////////////////////////////////////////////////////////////////////////////////////////

parameters
{
    //EN Fertility model selection
    SELECTED_FERTILITY_MODEL selected_fertility_model;

    //EN First Births
    double  FirstBirthRates[BIRTH1_GROUP][UNION_STATUS][FERTILE_AGE_RANGE][BIRTH1_LOC];

    //EN Higher order births
    double  HigherOrderBirthsPara[HIGHER_BIRTHS_PARA][PARITY_RANGE2];

    //EN Birth Trends
    double  BirthTrends[PARITY_RANGE1][SIM_YEAR_RANGE];
};

parameter_group PG03_Fertility_Top              //EN Fertility
{
    selected_fertility_model,
    PG03a_Fertility_Model_A,
    PG03b_Fertility_Model_B
};

parameter_group PG03b_Fertility_Model_B         //EN Fertility: Refined Version
{
    FirstBirthRates, HigherOrderBirthsPara,
    BirthTrends
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// Actor States and Functions
////////////////////////////////////////////////////////////////////////////////////////////////////

actor Person
{
    //EN Population group for modeling of births
    BIRTH1_GROUP birth1_group  = (educ_one_fate == EOL_LOW ) ? B1G_00 :
                        (educ_one_fate == EOL_MEDIUM ) ? B1G_01 : B1G_02;

    //EN Geographical location for modeling of first births
    BIRTH1_LOC birth1_loc = aggregate( district_nat, BIRTH1_LOC);

    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
    UNION_STATUS        union_status = (in_union) ? US_YES : US_NO;             //EN Union Status

    //EN Indicator re-set at birth events for calculation of time since last birth
    logical in_this_parity = { TRUE };

    //EN Indicator re-set at birt events calculating time since last birth in simulation time
    logical in_this_parity_in_simulation = in_projected_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);

    void SetFertilityModelSwitches();                       //EN Initializing model choices
    hook SetFertilityModelSwitches, Start;                  //EN hooked to Start function

    void GetImmigrantParityTimeBirth();                     //EN Sample immigrants characteristics
    hook GetImmigrantParityTimeBirth, ImmigrationEvent;     //EN hook to ImmigrationEvent

    TIME time_of_last_birth = { TIME_INFINITE };            //EN Time of last birth
    void MakeBaby();                                        //EN Function creating a baby
    event timeRefineModelBirthEvent, RefineModelBirthEvent; //EN Birth event
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation
////////////////////////////////////////////////////////////////////////////////////////////////////

void Person::GetImmigrantParityTimeBirth()
{
    if (sex == FEMALE )
    {
        if ( asWomenAgeEducDist[integer_age][educ_one_fate][district_immi]->Count() > 0 )
        {
            auto prHost =  asWomenAgeEducDist[integer_age][educ_one_fate][district_immi]
                ->GetRandom(RandUniform(30));
            parity = prHost-> parity;
            if (parity > 0)
            {
                in_this_parity = FALSE;
                time_since_last_birth_start = prHost->time_since_last_birth;
                in_this_parity = TRUE;
            }
        }
        else parity = 0; // no donor found
    }
}

void Person::SetFertilityModelSwitches()
{
    if (selected_fertility_model == SFM_MACRO) // Macro model (age only)
    {
        use_base_fertility_model =  TRUE ;            // Use the base model
        use_base_fertility_for_alignment =  FALSE ;   // Base model not used for alignment
    }
    else if (selected_fertility_model == SFM_MICRO )  // Use the refined model un-aligned
    {
        use_base_fertility_model =  FALSE ;           // Do not use the base model
        use_base_fertility_for_alignment =  FALSE ;   // Base model not used for alignment
    }
    else                                              // Refined model with aligned births
    {
        use_base_fertility_model =  FALSE ;           // Do not use the base model
        use_base_fertility_for_alignment = TRUE ;     // Base model used for alignment
    }
}

void Person::MakeBaby()
{
    auto peChild = new Person;    // Create and point to a new actor
    peChild->Start(NULL, this);   // Call Start() function of the new actor and pass own address
}

TIME Person::timeRefineModelBirthEvent()
{
    double dEventTime = TIME_INFINITE;
    double dHazard = 0.0;

    // The base model to which output is aligned already produced a baby
    if (baby_looking_for_mother)
    {
        dEventTime = WAIT(0);
    }
    // The refined model version is used
    else if (is_potential_mother && !use_base_fertility_model)
    {
        // first birth
        if (parity == 0)
        {
            dHazard = FirstBirthRates[birth1_group][union_status]
                [RANGE_POS(FERTILE_AGE_RANGE, integer_age)][birth1_loc]
                * BirthTrends[RANGE_POS(PARITY_RANGE1, parity+1)][RANGE_POS(SIM_YEAR_RANGE, calendar_year)];
        }
        // higher order births
        else
        {
            // Baseline hazard and trend
            dHazard = HigherOrderBirthsPara[time_in_parity][RANGE_POS(PARITY_RANGE2, parity+1)]
                * BirthTrends[RANGE_POS(PARITY_RANGE1, parity+1)][RANGE_POS(SIM_YEAR_RANGE, calendar_year)];

            // 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 (educ_one_fate == EOL_MEDIUM)
            {
                dHazard = dHazard * HigherOrderBirthsPara[HBP_EDUC1][RANGE_POS(PARITY_RANGE2, parity+1)];
            }
            else if (educ_one_fate == EOL_HIGH)
            {
                dHazard = dHazard * HigherOrderBirthsPara[HBP_EDUC2][RANGE_POS(PARITY_RANGE2, parity+1)];
            }
        }

        // schedule event
        if (dHazard > 0.0) dEventTime = WAIT(-TIME(log(RandUniform(29)) / dHazard));
    }
    return dEventTime;
}

/*      NOTE(Person.BirthEvent, EN)
At each birth event it has to be determined if the birth applies to the actor herself (unaligned model)
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.
*/

void Person::RefineModelBirthEvent()
{
    // event applies to individual without alignment
    if (!baby_looking_for_mother && !use_base_fertility_for_alignment)
    {
        // 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;  // time isnce last birth in starting population
        parity++;                                                       // increment parity
        auto peChild = new Person;                  // Create and point to a new actor
        peChild->Start(NULL, this);                     // Call the Start() function of the new actor and pass own address
        in_this_parity = TRUE;              // reset indicator
        time_of_last_birth = time;          // record time
    }
    // aligned to macro-model: find women with shortest waing time to birth
    else if (baby_looking_for_mother)
    {
        baby_looking_for_mother = FALSE;
        auto peMother = asPotentialMothers->Item(0);
        auto pePotentialMother = asPotentialMothers->Item(0);

        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);
                double dCurrentWaitTime = pePotentialMother->timeRefineModelBirthEvent() - time;
                if (dCurrentWaitTime < dShortestWaitTime)
                {
                    peMother = pePotentialMother;
                    dShortestWaitTime = dCurrentWaitTime;
                }
            }
        }
        // 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);
                double dCurrentWaitTime = pePotentialMother->timeRefineModelBirthEvent() - time;
                if (dCurrentWaitTime < dShortestWaitTime)
                {
                    peMother = pePotentialMother;
                    dShortestWaitTime = dCurrentWaitTime;
                }
            }
        }

        // 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
        peMother->MakeBaby();
        peMother->in_this_parity = TRUE;                // reset indicator
        peMother->time_of_last_birth = time;
    }
}

table Person TabNumberBirthsByAge                                               //EN Births by age
[in_projected_time]
{
    {
        parity                                  //EN Births
    }
    * sim_year
    * integer_age+
};

5.13.3. Initializations in the Start() Function

The states parity and time_since_last_birth_start are initiated in the Start() Function.


    // Initialize states
    if (person_type == PT_START) // Person comes from starting population
    {
        // (A) States from Starting population file
        time                = peObservation->pmc[PMC_BIRTH];
        sex                 = (SEX)(int)peObservation->pmc[PMC_SEX];
        family_role         = peObservation->fam_role;
        district_int        = (DISTRICT_INT)(int)peObservation->pmc[PMC_DISTRICT];
        district_birth      = (DISTRICT_INT)(int)peObservation->pmc[PMC_DOB];
        educ_one_startpop   = (EDUC_ONE_LEVEL)(int)peObservation->pmc[PMC_EDUC];
        ethnicity           = (ETHNICITY)(int)peObservation->pmc[PMC_ETHNO];

        if (peObservation->pmc[PMC_UNION] > time && peObservation->pmc[PMC_UNION] < MIN(SIM_YEAR_RANGE))
        {
            union1_startpop = peObservation->pmc[PMC_UNION];
        }

        if ( sex == FEMALE )
        {
            parity = (int)peObservation->pmc[PMC_PARITY];
            if ( parity > 0 && MIN(SIM_YEAR_RANGE) >  peObservation->pmc[PMC_LASTBIR] )
            {
                time_since_last_birth_start = MIN(SIM_YEAR_RANGE) -  peObservation->pmc[PMC_LASTBIR];
            }
        }