Global Health Policy Simulation model
| Home | Quick Start | User Guide | Schemas | Models | Architecture | Data Model | Developer Guide | Technical docs | API |
| Related: Height CSV quintile plan | Income quintile factor means plan | FINCH guide | Technical index | Documentation index |
Title: Weight Quintile Quantiles
Overview: Add per-stratum weight quantile CSVs under WeightQuantiles (nested Quintile1..N), keep the legacy single-file shape, pick curves by income_adjustment_stratum when assigning weight, and print stratum/final-income summary tables on generate and update-same pattern as height.
| Status | Item |
|---|---|
| Done | Extend v1/v2 kevinhall.json WeightQuantiles Female/Male oneOf: legacy csv_file or Quintile1..N object |
| Done | Implement load_weight_quantiles_by_gender in model_parser.cpp with broadcast, count validation vs adjustment_income_stratum_count, sorted Quintile keys |
| Done | Change KevinHallModel/Definition to per-stratum quantile vectors + resolve_weight_quantiles_for_person + update initialise_weight/get_weight_quantile |
| Done | Add print_weight_stratum_assignment_table, print_weight_by_final_income_category_table, print_weight_summary_tables; call from generate and update paths (mirror height gating) |
| Done | Add parser/runtime tests for legacy, N files, broadcast, mismatch errors, and console table output where applicable |
| Done | Update FINCH dynamic_model.json when quintile CSVs exist; add weight_quantiles_quintile_plan.md |
WeightQuantiles.Female / Male as a single csv_file (e.g. weight_quantiles_NCDRisk_female.csv).Female.Quintile1 -> weight_quantiles_NCDRisk_female_quintile1.csv, and the same for Male.person.income_adjustment_stratum (from adjustment_income_stratum_count rank buckets) to choose which quantile curve to use-the same idea as height via resolve_height_params_for_person in kevin_hall_model.cpp.project_requirements.income.categories ("3" or "4") on person.income; that does not change.Example: 5 adjustment strata for factors-mean / weight curves, 4 categories in ProjectRequirements for output.
RiskFactorModule always runs static then Kevin Hall. So when initialise_weight runs, the static model has already assigned income_adjustment_stratum and remapped person.income to 3 or 4 categories (static_linear_model.cpp ~786-905).
generate_risk_factors)Applies on run start (first simulation year). Static completes stratum assignment and final income categories before Kevin Hall touches weight.
flowchart TD
subgraph staticGen [StaticLinearModel.generate]
s1[Init sector income RF PA]
s2[Factors-mean adjustment optional per stratum]
s3[assign_income_adjustment_strata_equal_split N]
s4[assign_income_categories_equal_split 3 or 4]
end
subgraph dynGen [KevinHallModel.generate]
d1[Init nutrient and energy intake]
d2[initialise_weight uses stratum quantile curve]
d3[Adjust weight mean to expected]
d4[Init height using stratum height params]
d5[Init Kevin Hall state and BMI]
d6[Print weight summary tables]
end
staticGen --> dynGen
Weight-specific step: initialise_weight resolves the quantile vector for (gender, income_adjustment_stratum) (or index 0 if no stratum flag), then applies the existing E/PA -> weight quantile logic (~873-908).
update_risk_factors)Runs each simulation year. Static runs first again (strata can refresh from updated continuous income); then Kevin Hall updates weight/height.
flowchart TD
subgraph staticUpd [StaticLinearModel.update each year]
u1[Init or update newborns vs others]
u2[Factors-mean adjustment optional per stratum]
u3[assign_income_adjustment_strata_equal_split N]
u4[assign_income_categories_equal_split 3 or 4]
end
subgraph dynUpd [KevinHallModel.update]
v0{Path}
v0 -->|Newborns age 0| n1[Init nutrient energy weight]
n1 --> n2[Newborn weight adjustment]
n2 --> n3[Init height and KH state]
n3 --> n4[Print weight summary tables phase update-newborns]
v0 -->|Non-newborns| c1[Update nutrient and energy]
c1 --> c2{Age less than 19}
c2 -->|Yes| c3[initialise_weight stratum quantiles]
c2 -->|No| c4[kevin_hall_run adult weight]
c3 --> c5[Scenario weight adjustments]
c4 --> c5
c5 --> c6[Update height if child]
c6 --> c7[Print weight summary tables phase update-children]
end
staticUpd --> dynUpd
Note: Adults (age >= 19) do not re-run initialise_weight each year; stratum-based quantiles matter most at init and for children. Console tables still run on update paths so we can confirm strata and weights wherever weight was set in that step.
Files: schemas/v2/config/models/kevinhall.json, schemas/v1/config/models/kevinhall.json
For WeightQuantiles.Female and .Male, use oneOf:
$ref to csv_file.jsonQuintile1, Quintile2, … each a csv_file blockNew format example:
"WeightQuantiles": {
"Female": {
"Quintile1": { "name": "weight_quantiles_NCDRisk_female_quintile1.csv", "format": "csv", ... },
"Quintile2": { ... }
},
"Male": { ... }
}
Legacy (unchanged):
"Female": { "name": "weight_quantiles_NCDRisk_female.csv", "format": "csv", ... }
This is the approach to use unless requirements change later.
| Approach | Config effort | Tradeoff |
|---|---|---|
Explicit Quintile1…N (chosen) |
Repeat format / delimiter / columns per file (or copy-paste blocks) |
Same pattern as factors-mean strata in examples/config_skeleton.json; schema can validate each file; wrong or missing file fails at load with a clear path |
Name pattern e.g. …_quintile{n}.csv, N from adjustment_income_stratum_count |
One block per gender, fewer lines | Assumes rigid filenames; harder to mix non-standard names or skip a stratum |
Ordered files array |
List filenames once, shared format on parent | Slightly shorter JSON, but still N filenames; new schema shape |
| Directory glob | Almost nothing in JSON | Fragile ordering, accidental extra CSVs, weak validation |
Practical tip: For FINCH, duplicate the existing Female / Male csv block five times, change name to _quintile1 … _quintile5, and nest under Quintile1 … Quintile5. No magic paths-the filenames in JSON are exactly what get loaded.
Factors-mean quintiles stay in config.json (baseline_adjustments.income_stratum_factors_mean.strata); weight quantiles stay in dynamic_model.json because Kevin Hall already owns WeightQuantiles there today.
Update dynamic_model.json when quintile CSVs are in the data folder.
Replace the flat load at 1864-1883 with a helper similar to Height (1901-1937):
| Config input | adjustment_income_stratum_count |
Loaded shape per gender |
|---|---|---|
Single csv_file |
any | One vector; broadcast to N if stratum mode on and N > 1 |
Quintile1…QuintileN |
N | N vectors; error if file count ≠N |
| Wrong count | - | Parse error with file count vs expected N |
load_datatable_from_csv, first column double, sort once at load.Quintile1, Quintile2, …).config.modelling.baseline_adjustment.income_stratum_factors_mean for enabled and adjustment_income_stratum_count (same as height ~1897-1899).Storage (mirror height_params_):
std::unordered_map<core::Gender, std::vector<std::vector<double>>> weight_quantiles_by_stratum_;
Logic:
resolve_weight_quantiles_for_person - same rules as height (~69-85): stratum index when has_income_adjustment_stratum, else index 0.get_weight_quantile(epa_quantile, const std::vector<double>& quantiles) - no per-person allocation; quantiles already sorted at load.initialise_weight - resolve stratum vector, then existing EPA percentile -> weight quantile math (~1027-1038).Performance: I/O and sorting only at load; per person stays O(log n) on epa_quantiles_ plus O(1) index into the stratum vector.
Mirror the height helpers in kevin_hall_model.cpp (~87-225, ~1176-1194). Not optional-needed to verify stratum weight assignment in the log.
Gating - reuse the same policy as should_print_height_summary_tables (~49-58):
Table 1 - [WEIGHT STRATUM ASSIGNMENT] (by adjustment stratum)
Modeled on print_height_stratum_assignment_table:
| Column | Content |
|---|---|
| Bucket | person.income_adjustment_stratum (0..N-1) |
| Stratum ID | Quintile1 … QuintileN |
| Count | Active people in bucket |
| Quantile size | Number of values loaded for that stratum (sanity check) |
| Weight Min / Max / Mean | From person.risk_factors["Weight"] after assignment in that step |
Header note: uses person.income_adjustment_stratum from static model.
Table 2 - [WEIGHT BY FINAL INCOME CATEGORY] (by output income)
Modeled on print_height_by_final_income_category_table:
| Column | Content |
|---|---|
| Category | Low / LowerMid / UpperMid / High (4) or Low / Middle / High (3) |
| Count | People in person.income category |
| Weight Min / Max / Mean | After assignment |
Header note: person.income from ProjectRequirements categories after static remapping.
Call sites - add print_weight_summary_tables(context, phase) alongside existing height printing:
| Phase string | When |
|---|---|
generate |
End of generate_risk_factors (~291) |
update-newborns |
End of update_newborns (~370) |
update-children |
End of update_non_newborns child path (~441) |
bucket_count for the stratum table: max loaded stratum count across genders (same as height ~1182-1186).
Income-stratum assignment (static model) selects the weight-quantile curve. Weight then prints [WEIGHT STRATUM ASSIGNMENT] and [WEIGHT BY FINAL INCOME CATEGORY] alongside the height tables.
![]() |
|---|
| Income-stratum assignment buckets and sampled factor delta/apply rows (initial year) |
![]() |
|---|
| Weight stratum assignment and weight by final income category (year 2022, phase=generate); height tables appear in the same log |
In KevinHallHeight.Test.cpp and/or KevinHallWeightValidation.Test.cpp:
adjustment_income_stratum_count = 5[WEIGHT STRATUM ASSIGNMENT] and Quintile1 when tables are enabled (capture stdout like height tests ~542)Short note in documentation/technical/plans/weight-quintile-plan.md with config examples, both lifecycle diagrams, and table format-aligned with height-csv-quintile-plan.md.
Author: Mahima Ghosh