From 74574be384ca021aba7ed05c379e2a0890771094 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:51:14 +0200 Subject: [PATCH 1/6] parameterisation of lifetime income analysis --- .../java/simpaths/model/SimPathsModel.java | 4 +- .../lifetime_incomes/AdjustmentFactors.java | 162 +++++++++--------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 54304a752..92fb36b27 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -271,11 +271,11 @@ public void setFirstRun(boolean firstRun) { @GUIparameter(description = "tick to enable intertemporal optimised consumption and labour decisions") private boolean enableIntertemporalOptimisations = false; - private boolean lifetimeIncomeGenerate = false; // request to generate new set of lifetime incomes + private boolean lifetimeIncomeGenerate = true; // request to generate new set of lifetime incomes private Integer lifetimeIncomeStartBirthYear = 1934; private Integer lifetimeIncomeEndBirthYear = 2019; private Integer lifetimeIncomeEndAge = 80; - private Integer lifetimeIncomeCohortSize = 100000; + private Integer lifetimeIncomeCohortSize = 300000; private boolean lifetimeIncomeWriteToCSV = false; private long lifetimeIncomeRandomSeed = 505; private double lifetimeIncomeAge0StdDev = 0.9; diff --git a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java index 9fcb48fca..2f4ade51e 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java +++ b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java @@ -7,87 +7,87 @@ public class AdjustmentFactors { public static double fixedEffect = 0.5; public static double[] whiteNoise = { - 0.5000 , // age0 - 0.5000 , // age1 - 0.5000 , // age2 - 0.5000 , // age3 - 0.5000 , // age4 - 0.5000 , // age5 - 0.5000 , // age6 - 0.5000 , // age7 - 0.5000 , // age8 - 0.5000 , // age9 - 0.5000 , // age10 - 0.5000 , // age11 - 0.5000 , // age12 - 0.5000 , // age13 - 0.5000 , // age14 - 0.5000 , // age15 - 0.5000 , // age16 - 0.5000 , // age17 - 0.5000 , // age18 - 0.5000 , // age19 - 0.5000 , // age20 - 0.5000 , // age21 - 0.5000 , // age22 - 0.5000 , // age23 - 0.5000 , // age24 - 0.5000 , // age25 - 0.5000 , // age26 - 0.5000 , // age27 - 0.5000 , // age28 - 0.5000 , // age29 - 0.5000 , // age30 - 0.5000 , // age31 - 0.5000 , // age32 - 0.5000 , // age33 - 0.5000 , // age34 - 0.5000 , // age35 - 0.5000 , // age36 - 0.5000 , // age37 - 0.5000 , // age38 - 0.5000 , // age39 - 0.5000 , // age40 - 0.5000 , // age41 - 0.5000 , // age42 - 0.5000 , // age43 - 0.5000 , // age44 - 0.5000 , // age45 - 0.5000 , // age46 - 0.5000 , // age47 - 0.5000 , // age48 - 0.5000 , // age49 - 0.5000 , // age50 - 0.5000 , // age51 - 0.5000 , // age52 - 0.5000 , // age53 - 0.5000 , // age54 - 0.5000 , // age55 - 0.5000 , // age56 - 0.5000 , // age57 - 0.5000 , // age58 - 0.5000 , // age59 - 0.5000 , // age60 - 0.5000 , // age61 - 0.5000 , // age62 - 0.5000 , // age63 - 0.5000 , // age64 - 0.5000 , // age65 - 0.5000 , // age66 - 0.5000 , // age67 - 0.5000 , // age68 - 0.5000 , // age69 - 0.5000 , // age70 - 0.5000 , // age71 - 0.5000 , // age72 - 0.5000 , // age73 - 0.5000 , // age74 - 0.5000 , // age75 - 0.5000 , // age76 - 0.5000 , // age77 - 0.5000 , // age78 - 0.5000 , // age79 - 0.5000 , // age80 + 0.639893098 , // age0 + 0.624783408 , // age1 + 0.667171948 , // age2 + 0.683758156 , // age3 + 0.712236781 , // age4 + 0.709151244 , // age5 + 0.712304635 , // age6 + 0.730869311 , // age7 + 0.712430887 , // age8 + 0.683622348 , // age9 + 0.697016596 , // age10 + 0.666035772 , // age11 + 0.661157533 , // age12 + 0.669154405 , // age13 + 0.689440538 , // age14 + 0.687501894 , // age15 + 0.704932916 , // age16 + 0.697900413 , // age17 + 0.701738731 , // age18 + 0.680196543 , // age19 + 0.638906039 , // age20 + 0.659043878 , // age21 + 0.69077011 , // age22 + 0.676173163 , // age23 + 0.662343986 , // age24 + 0.670209912 , // age25 + 0.673320479 , // age26 + 0.68792881 , // age27 + 0.71796723 , // age28 + 0.735232476 , // age29 + 0.742738923 , // age30 + 0.722673522 , // age31 + 0.743136107 , // age32 + 0.738348963 , // age33 + 0.72350334 , // age34 + 0.766318724 , // age35 + 0.768365019 , // age36 + 0.7337006 , // age37 + 0.710851134 , // age38 + 0.72612377 , // age39 + 0.738621078 , // age40 + 0.730151591 , // age41 + 0.747148673 , // age42 + 0.746897174 , // age43 + 0.73598024 , // age44 + 0.751243438 , // age45 + 0.735483057 , // age46 + 0.745796089 , // age47 + 0.749050604 , // age48 + 0.751857051 , // age49 + 0.775671742 , // age50 + 0.768728264 , // age51 + 0.769569628 , // age52 + 0.771894225 , // age53 + 0.787275393 , // age54 + 0.781206324 , // age55 + 0.788353035 , // age56 + 0.82782717 , // age57 + 0.831884866 , // age58 + 0.842843976 , // age59 + 0.813602118 , // age60 + 0.754416871 , // age61 + 0.709298354 , // age62 + 0.723371377 , // age63 + 0.734968192 , // age64 + 0.632096908 , // age65 + 0.530836949 , // age66 + 0.494255143 , // age67 + 0.500151121 , // age68 + 0.517388875 , // age69 + 0.556422529 , // age70 + 0.562352937 , // age71 + 0.537723699 , // age72 + 0.529143288 , // age73 + 0.527895651 , // age74 + 0.525564874 , // age75 + 0.548628932 , // age76 + 0.561406355 , // age77 + 0.549365398 , // age78 + 0.547104755 , // age79 + 0.546168053 , // age80 }; public static double getWhiteNoise(int age) { From 0cff4f45c56fd728efc50ffee2a5ed5e411c20c5 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:11:17 +0200 Subject: [PATCH 2/6] intermediate --- documentation/repository-guide.md | 2 +- .../wiki/developer-guide/repository-guide.md | 2 +- .../compile/07_was_wealth_data.do | 5 +- .../compile/08_wealth_to_ukhls.do | 3 + src/main/java/simpaths/data/Parameters.java | 1 - src/main/java/simpaths/model/BenefitUnit.java | 4 +- src/main/java/simpaths/model/Person.java | 282 ++++++++++++++++-- .../java/simpaths/model/SimPathsModel.java | 75 +---- .../java/simpaths/model/enums/Gender.java | 11 +- .../lifetime_incomes/AdjustmentFactors.java | 164 +++++----- .../model/lifetime_incomes/AnnualIncome.java | 144 --------- .../model/lifetime_incomes/BirthCohort.java | 59 ---- .../EquivalisedIncomeCDF.java | 48 --- .../model/lifetime_incomes/ExportCSV.java | 119 -------- .../model/lifetime_incomes/ImputedIncome.java | 69 +++++ .../model/lifetime_incomes/Individual.java | 268 ----------------- .../IndividualComparator.java | 18 -- .../LifetimeIncomeImputation.java | 170 ----------- .../ManagerLifetimeIncomes.java | 172 +++++++++++ .../ManagerProjectLifetimeIncomes.java | 252 ---------------- .../lifetime_incomes/WhiteNoiseEstimate.java | 4 +- 21 files changed, 621 insertions(+), 1251 deletions(-) delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/AnnualIncome.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/BirthCohort.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/EquivalisedIncomeCDF.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/ExportCSV.java create mode 100644 src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/Individual.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/IndividualComparator.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/LifetimeIncomeImputation.java create mode 100644 src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java delete mode 100644 src/main/java/simpaths/model/lifetime_incomes/ManagerProjectLifetimeIncomes.java diff --git a/documentation/repository-guide.md b/documentation/repository-guide.md index 0a9bcbea7..f81822786 100644 --- a/documentation/repository-guide.md +++ b/documentation/repository-guide.md @@ -266,7 +266,7 @@ When IO is enabled, this package creates projected income paths for birth cohort | Class | Purpose | | --- | --- | -| `ManagerProjectLifetimeIncomes` | Generates the synthetic income trajectory database for all birth cohorts in the simulation horizon. | +| `ManagerLifetimeIncomes` | Generates the synthetic income trajectory database for all birth cohorts in the simulation horizon. | | `LifetimeIncomeImputation` | Matches each simulated person to a donor income trajectory via binary search on the income CDF. | | `AnnualIncome` | Implements the AR(2) income process with age-gender anchoring. | | `BirthCohort` | Groups individuals by birth year for cohort-level income projection. | diff --git a/documentation/wiki/developer-guide/repository-guide.md b/documentation/wiki/developer-guide/repository-guide.md index 1695b9092..865964f4d 100644 --- a/documentation/wiki/developer-guide/repository-guide.md +++ b/documentation/wiki/developer-guide/repository-guide.md @@ -260,7 +260,7 @@ When IO is enabled, this package creates projected income paths for birth cohort | Class | Purpose | | --- | --- | -| `ManagerProjectLifetimeIncomes` | Generates the synthetic income trajectory database for all birth cohorts in the simulation horizon. | +| `ManagerLifetimeIncomes` | Generates the synthetic income trajectory database for all birth cohorts in the simulation horizon. | | `LifetimeIncomeImputation` | Matches each simulated person to a donor income trajectory via binary search on the income CDF. | | `AnnualIncome` | Implements the AR(2) income process with age-gender anchoring. | | `BirthCohort` | Groups individuals by birth year for cohort-level income projection. | diff --git a/input/InitialPopulations/compile/07_was_wealth_data.do b/input/InitialPopulations/compile/07_was_wealth_data.do index fd535098c..c03f1b60b 100644 --- a/input/InitialPopulations/compile/07_was_wealth_data.do +++ b/input/InitialPopulations/compile/07_was_wealth_data.do @@ -264,6 +264,7 @@ foreach file in "${dir_was_data}/was_round_5_person_eul_oct_2020.dta" /// // ypnbihs recode dvgiemp dvgise dvgippen dvgiinv dvoigrrannual_i dvoiggtannual_i dvoigegannual_i dvoigfrannual_i dvoigmaannual_i dvoigroannual_i dvoigopannual_i (missing=0) gen inci = dvgiemp + dvgise + dvgippen + dvgiinv + dvoigrrannual_i + dvoiggtannual_i + dvoigegannual_i + dvoigfrannual_i + dvoigmaannual_i + dvoigroannual_i + dvoigopannual_i + gen pensioner = (dvgippen > 0.01) // fix some partner person numbers duplicates tag case partno, gen(dup) // Rows should be unique in case partno, assuming monogamy @@ -287,6 +288,7 @@ foreach file in "${dir_was_data}/was_round_5_person_eul_oct_2020.dta" /// gsort case -hrp person /* Place hh ref person at top of household */ by case: replace hrp=1 if hrp[1]==0 /* to account for missing reference adults - not really important */ by case: gen sps = partno==person[1] /* spouse/partner of hrp */ + by case: egen pensioners = sum(pensioner) gen chld = isdep2==1 /* Dependent child */ gen adlt = 1-chld /* Adult */ gsort case -hrp -sps person @@ -416,6 +418,7 @@ foreach file in "${dir_was_data}/was_round_5_person_eul_oct_2020.dta" /// // label var ocdc_contrate_emee "DC occupational employee pension contribution rate (% of gross employment income)" // label var ocdc_contrate_emer "DC occupational employer pension contribution rate (% of gross employment income)" label var pp_membu "number of personal pension members in benefit unit" + label var pensioners "number of recipients of private pension in benefit unit" gen occ_emp_contrib_rate = . replace occ_emp_contrib_rate = max(dvcontocc_emee_dc1,0) + max(dvcontocc_emee_dc2,0) if !missing(dvcontocc_emee_dc1) | !missing(dvcontocc_emee_dc2) @@ -771,7 +774,7 @@ foreach file in "${dir_was_data}/was_round_5_person_eul_oct_2020.dta" /// cc_debt cc_debt_trans cc_debt_pers hp_debt loan_debt loan_debt_exc_slc loan_debt_slc /// loan_doorstep loan_pawnbroker loan_payday informal_loan_debt highcost_loan_debt /// mail_debt unsec_debt_total unsec_debt_core highcost_debt_proxy lowcost_debt /// - op_memb pp_membu op_membu op_db op_dc /// + op_memb pp_membu op_membu op_db op_dc pensioners/// pp_core_annual pp_fallback_annual pp_contrib_annual pp_contrib_rate_income priv_total_contrib_rate_inc /// pp_contrib_source_core pp_contrib_source_fallback occ_emp_contrib_rate occ_emp_contrib_annual /// caser5 caser6 caser7 personw5 personw6 personr7 diff --git a/input/InitialPopulations/compile/08_wealth_to_ukhls.do b/input/InitialPopulations/compile/08_wealth_to_ukhls.do index d212e423a..53c498a9e 100644 --- a/input/InitialPopulations/compile/08_wealth_to_ukhls.do +++ b/input/InitialPopulations/compile/08_wealth_to_ukhls.do @@ -119,6 +119,8 @@ set seed `seed' gen ee = rnormal() gen ee2 = runiform() +gen pensioner = (ypnoab > 0.01) + gen was = 0 gen long bu = idbenefitunit @@ -192,6 +194,7 @@ use "ukhls_wealthtemp.dta", clear gen treat = (single_woman + single_man + couple_ref) gsort bu -treat by bu: egen chk = sum(treat) +by bu: egen pensioners = sum(pensioner) tab chk drop chk replace treat = 0 if (bu==bu[_n-1]) diff --git a/src/main/java/simpaths/data/Parameters.java b/src/main/java/simpaths/data/Parameters.java index b17184ada..78844e414 100644 --- a/src/main/java/simpaths/data/Parameters.java +++ b/src/main/java/simpaths/data/Parameters.java @@ -23,7 +23,6 @@ import org.apache.commons.math3.util.Pair; import org.apache.poi.openxml4j.util.ZipSecureFile; import simpaths.model.decisions.Grids; -import simpaths.model.lifetime_incomes.EquivalisedIncomeCDF; import simpaths.model.taxes.DonorTaxUnit; import simpaths.model.taxes.MatchFeature; import simpaths.model.taxes.database.TaxDonorDataParser; diff --git a/src/main/java/simpaths/model/BenefitUnit.java b/src/main/java/simpaths/model/BenefitUnit.java index 94e733224..9d3625d14 100644 --- a/src/main/java/simpaths/model/BenefitUnit.java +++ b/src/main/java/simpaths/model/BenefitUnit.java @@ -4841,6 +4841,8 @@ public Double getDisposableIncomeMonthly() { return yDispMonth; } + public Double getYDispMonth() { return yDispMonth; } + public Double getGrossIncomeMonthly() { return yGrossMonth; } @@ -5352,7 +5354,7 @@ public double getXDiscConsumptionAnnual(boolean throwError) { } public double getEquivalisedWeight() { - //Equivalence scale gives a weight of 1.0 to the first adult; + //OECD (BHC) equivalence scale gives a weight of 1.0 to the first adult; //0.5 to the second and each subsequent person aged 14 and over; //0.3 to each child aged under 14. double weight = 0.0; diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index e87178920..8ebbf2658 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -25,8 +25,7 @@ import simpaths.model.decisions.Axis; import simpaths.model.decisions.DecisionParams; import simpaths.model.enums.*; -import simpaths.model.lifetime_incomes.AnnualIncome; -import simpaths.model.lifetime_incomes.Individual; +import simpaths.model.lifetime_incomes.TemplateIndividual; import simpaths.model.person.PrivatePension; import java.math.BigDecimal; @@ -51,9 +50,6 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @JoinColumn(name = "burun", referencedColumnName = "simulation_run"), @JoinColumn(name = "prid", referencedColumnName = "working_id") }) private BenefitUnit benefitUnit; - @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.REFRESH) - @JoinColumn(name = "income_id", referencedColumnName = "id") - private Individual ltIncomeDonor; // identifiers private Long idPersOriginal; @@ -150,6 +146,7 @@ public class Person implements EventListener, IDoubleSource, IIntSource, Weight, @Lag(field="yBenUCReceivedFlag") @Transient private Boolean yBenUCReceivedFlagL1; @Column(name="yBenNonUCReceivedFlag") private Boolean yBenNonUCReceivedFlag; // Person receives a benefit which is not UC @Lag(field="yBenNonUCReceivedFlag") @Transient private Boolean yBenNonUCReceivedFlagL1; + @Transient TemplateIndividual ltIncomeDonor; @NullInitialised @Column(name="yLifeTime") private Double yLifeTime; // mean annual equivalised household disposable income by age @Enumerated(EnumType.STRING) private Labour labHrsWorkEnumWeek; //Number of hours of labour supplied each week @@ -349,11 +346,11 @@ public Person (Person originalPerson, long statSeed, SampleEntry demEnterSample) this.demEnterSample = demEnterSample; - if (originalPerson.ltIncomeDonor!=null) { + if (demEnterSample != SampleEntry.InputData && Parameters.lifetimeIncomeImpute) { + this.ltIncomeDonor = originalPerson.ltIncomeDonor; - } - if (Parameters.lifetimeIncomeImpute) this.yLifeTime = originalPerson.getYLifeTime(); + } demAge = originalPerson.demAge; demAgeSq = demAge * demAge; @@ -2619,6 +2616,92 @@ public enum Variables { labStatusPartnerC3NotEmployedL1, labStatusC4NotEmployed_MaleL1, Age, + Age0, + Age1, + Age2, + Age3, + Age4, + Age5, + Age6, + Age7, + Age8, + Age9, + Age10, + Age11, + Age12, + Age13, + Age14, + Age15, + Age16, + Age17, + Age18, + Age19, + Age20, + Age21, + Age22, + Age23, + Age24, + Age25, + Age26, + Age27, + Age28, + Age29, + Age30, + Age31, + Age32, + Age33, + Age34, + Age35, + Age36, + Age37, + Age38, + Age39, + Age40, + Age41, + Age42, + Age43, + Age44, + Age45, + Age46, + Age47, + Age48, + Age49, + Age50, + Age51, + Age52, + Age53, + Age54, + Age55, + Age56, + Age57, + Age58, + Age59, + Age60, + Age61, + Age62, + Age63, + Age64, + Age65, + Age66, + Age67, + Age68, + Age69, + Age70, + Age71, + Age72, + Age73, + Age74, + Age75, + Age76, + Age77, + Age78, + Age79, + Age80, + Age81, + Age82, + Age83, + Age84, + Log_GDP_per_capita, AgeSquared, AgeCubed, Age20to24, @@ -4022,6 +4105,94 @@ public double getDoubleValue(Enum variableID) { case LnAge -> { return Math.log(demAge); } + case Age0 -> {return (demAge==0) ? 1: 0;} + case Age1 -> {return (demAge==1) ? 1: 0;} + case Age2 -> {return (demAge==2) ? 1: 0;} + case Age3 -> {return (demAge==3) ? 1: 0;} + case Age4 -> {return (demAge==4) ? 1: 0;} + case Age5 -> {return (demAge==5) ? 1: 0;} + case Age6 -> {return (demAge==6) ? 1: 0;} + case Age7 -> {return (demAge==7) ? 1: 0;} + case Age8 -> {return (demAge==8) ? 1: 0;} + case Age9 -> {return (demAge==9) ? 1: 0;} + case Age10 -> {return (demAge==10) ? 1: 0;} + case Age11 -> {return (demAge==11) ? 1: 0;} + case Age12 -> {return (demAge==12) ? 1: 0;} + case Age13 -> {return (demAge==13) ? 1: 0;} + case Age14 -> {return (demAge==14) ? 1: 0;} + case Age15 -> {return (demAge==15) ? 1: 0;} + case Age16 -> {return (demAge==16) ? 1: 0;} + case Age17 -> {return (demAge==17) ? 1: 0;} + case Age18 -> {return (demAge==18) ? 1: 0;} + case Age19 -> {return (demAge==19) ? 1: 0;} + case Age20 -> {return (demAge==20) ? 1: 0;} + case Age21 -> {return (demAge==21) ? 1: 0;} + case Age22 -> {return (demAge==22) ? 1: 0;} + case Age23 -> {return (demAge==23) ? 1: 0;} + case Age24 -> {return (demAge==24) ? 1: 0;} + case Age25 -> {return (demAge==25) ? 1: 0;} + case Age26 -> {return (demAge==26) ? 1: 0;} + case Age27 -> {return (demAge==27) ? 1: 0;} + case Age28 -> {return (demAge==28) ? 1: 0;} + case Age29 -> {return (demAge==29) ? 1: 0;} + case Age30 -> {return (demAge==30) ? 1: 0;} + case Age31 -> {return (demAge==31) ? 1: 0;} + case Age32 -> {return (demAge==32) ? 1: 0;} + case Age33 -> {return (demAge==33) ? 1: 0;} + case Age34 -> {return (demAge==34) ? 1: 0;} + case Age35 -> {return (demAge==35) ? 1: 0;} + case Age36 -> {return (demAge==36) ? 1: 0;} + case Age37 -> {return (demAge==37) ? 1: 0;} + case Age38 -> {return (demAge==38) ? 1: 0;} + case Age39 -> {return (demAge==39) ? 1: 0;} + case Age40 -> {return (demAge==40) ? 1: 0;} + case Age41 -> {return (demAge==41) ? 1: 0;} + case Age42 -> {return (demAge==42) ? 1: 0;} + case Age43 -> {return (demAge==43) ? 1: 0;} + case Age44 -> {return (demAge==44) ? 1: 0;} + case Age45 -> {return (demAge==45) ? 1: 0;} + case Age46 -> {return (demAge==46) ? 1: 0;} + case Age47 -> {return (demAge==47) ? 1: 0;} + case Age48 -> {return (demAge==48) ? 1: 0;} + case Age49 -> {return (demAge==49) ? 1: 0;} + case Age50 -> {return (demAge==50) ? 1: 0;} + case Age51 -> {return (demAge==51) ? 1: 0;} + case Age52 -> {return (demAge==52) ? 1: 0;} + case Age53 -> {return (demAge==53) ? 1: 0;} + case Age54 -> {return (demAge==54) ? 1: 0;} + case Age55 -> {return (demAge==55) ? 1: 0;} + case Age56 -> {return (demAge==56) ? 1: 0;} + case Age57 -> {return (demAge==57) ? 1: 0;} + case Age58 -> {return (demAge==58) ? 1: 0;} + case Age59 -> {return (demAge==59) ? 1: 0;} + case Age60 -> {return (demAge==60) ? 1: 0;} + case Age61 -> {return (demAge==61) ? 1: 0;} + case Age62 -> {return (demAge==62) ? 1: 0;} + case Age63 -> {return (demAge==63) ? 1: 0;} + case Age64 -> {return (demAge==64) ? 1: 0;} + case Age65 -> {return (demAge==65) ? 1: 0;} + case Age66 -> {return (demAge==66) ? 1: 0;} + case Age67 -> {return (demAge==67) ? 1: 0;} + case Age68 -> {return (demAge==68) ? 1: 0;} + case Age69 -> {return (demAge==69) ? 1: 0;} + case Age70 -> {return (demAge==70) ? 1: 0;} + case Age71 -> {return (demAge==71) ? 1: 0;} + case Age72 -> {return (demAge==72) ? 1: 0;} + case Age73 -> {return (demAge==73) ? 1: 0;} + case Age74 -> {return (demAge==74) ? 1: 0;} + case Age75 -> {return (demAge==75) ? 1: 0;} + case Age76 -> {return (demAge==76) ? 1: 0;} + case Age77 -> {return (demAge==77) ? 1: 0;} + case Age78 -> {return (demAge==78) ? 1: 0;} + case Age79 -> {return (demAge==79) ? 1: 0;} + case Age80 -> {return (demAge==80) ? 1: 0;} + case Age81 -> {return (demAge==81) ? 1: 0;} + case Age82 -> {return (demAge==82) ? 1: 0;} + case Age83 -> {return (demAge==83) ? 1: 0;} + case Age84 -> {return (demAge==84) ? 1: 0;} + case Log_GDP_per_capita -> { + return Math.log(Parameters.getTimeSeriesValue(getYear(), TimeSeriesVariable.GDPperCapita)); + } case Age20to24 -> { return (demAge >= 20 && demAge <= 24) ? 1. : 0.; } @@ -7102,6 +7273,71 @@ public void populateSocialCareReceipt_lag1(SocialCareReceiptState state) { } } + public void matchLifetimeIncome(int age, boolean agePlus, final double[] sortedZ, List templateIndividuals) { + + boolean matchHere; + if (agePlus) + matchHere = (demAge >= age); + else + matchHere = (demAge == age); + if (matchHere) { + + // identify normalised income target + double dispIncomePerYear = benefitUnit.getYDispMonth() * 12.0; + if (dispIncomePerYear > 0.0) { + + double gmEqDispIncomePerYear = getGMEquivalisedIncome(demAge, getYear()); + double equivScale = benefitUnit.getEquivalisedWeight(); + double zTarget = Math.log(dispIncomePerYear / equivScale / gmEqDispIncomePerYear); + int ii = Arrays.binarySearch(sortedZ, zTarget); + if (ii >= 0) { + // exact match found + setLtIncomeDonor(templateIndividuals.get(ii)); + } else { + // adopt nearest neighbour + int ins = -ii - 1; + if (ins == 0) { + // lower bound + setLtIncomeDonor(templateIndividuals.get(ins)); + } else if (ins == sortedZ.length) { + // upper bound + setLtIncomeDonor(templateIndividuals.get(ins - 1)); + } else if (zTarget - sortedZ[ins -1] < sortedZ[ins] - zTarget) { + // choose lower bound + setLtIncomeDonor(templateIndividuals.get(ins - 1)); + } else { + // choose upper bound + setLtIncomeDonor(templateIndividuals.get(ins)); + } + } + } + else { + + setLtIncomeDonor(templateIndividuals.get(0)); + } + + // set lifetime income + setLtIncome(age); + } + } + + private double getGMEquivalisedIncome(int age, int year) { + + + Double val = Parameters.getEquivalisedIncome(demMaleFlag, age, year); + if (!Parameters.isFinite(val)) { + // outside observed range, use regression model + + if (demMaleFlag.toBoolean()) { + val = Math.exp(Parameters.getRegLifetimeIncome1a().getScore(this, Person.Variables.class)); + } + else { + val = Math.exp(Parameters.getRegLifetimeIncome1b().getScore(this, Person.Variables.class)); + } + } + return val; + } + public void setCareFromInformalFlag(boolean val) { careFromInformalFlag = val; } @@ -8117,24 +8353,28 @@ public Double getHealthPhysicalPcsL1() { return healthPhysicalPcsL1; } - public void setLtIncomeDonor(Individual individual) { - ltIncomeDonor = individual; + public void setLtIncomeDonor(TemplateIndividual donor) { + ltIncomeDonor = donor; + } + + public double getImputedIncome(int age) { + + int year = age + getYear() - demAge; + double gmIncome = getGMEquivalisedIncome(age, year); + double annualIncome = Math.exp(ltIncomeDonor.getNormIncome(age).getZValue()) * gmIncome; + return annualIncome; } public void setLtIncome(int maxAge) { yLifeTime = 0.0; - if (demAge > 0) { + for (int age=0; age<=maxAge; age++) { - int birthYear = ltIncomeDonor.getBirthYear(); - int ageLimit = Math.min(maxAge, demAge -1); - for (int aa=0; aa<=ageLimit; aa++) { - AnnualIncome annualIncome = ltIncomeDonor.getAnnualIncome(birthYear+aa); - if (annualIncome == null) - throw new RuntimeException("Annual income for year " + (birthYear+aa) + " not found for donor " + ltIncomeDonor.getId()); - yLifeTime += annualIncome.getValue(); - } - yLifeTime /= (double)(ageLimit+1); + int year = age + getYear() - demAge; + double gmIncome = getGMEquivalisedIncome(age, year); + double annualIncome = Math.exp(ltIncomeDonor.getNormIncome(age).getZValue()) * gmIncome; + yLifeTime += annualIncome; } + yLifeTime /= (double)(maxAge+1); } public double getYLifeTime() { @@ -8158,7 +8398,7 @@ public void updateLtIncome() { } } - public Individual getLtIncomeDonor() {return ltIncomeDonor;} + public TemplateIndividual getLtIncomeDonor() {return ltIncomeDonor;} public Integer getLiwwh() { return labEmpNyear; diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 92fb36b27..4412b0928 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -3,8 +3,6 @@ // import Java packages -import com.opencsv.exceptions.CsvDataTypeMismatchException; -import com.opencsv.exceptions.CsvRequiredFieldEmptyException; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; @@ -39,9 +37,8 @@ import simpaths.model.decisions.DecisionParams; import simpaths.model.decisions.ManagerPopulateGrids; import simpaths.model.enums.*; -import simpaths.model.lifetime_incomes.BirthCohort; -import simpaths.model.lifetime_incomes.LifetimeIncomeImputation; -import simpaths.model.lifetime_incomes.ManagerProjectLifetimeIncomes; +import simpaths.model.lifetime_incomes.ManagerLifetimeIncomes; +import simpaths.model.lifetime_incomes.TemplateIndividual; import simpaths.model.taxes.*; import simpaths.model.taxes.database.DatabaseExtension; import simpaths.model.taxes.database.TaxDonorDataParser; @@ -271,15 +268,12 @@ public void setFirstRun(boolean firstRun) { @GUIparameter(description = "tick to enable intertemporal optimised consumption and labour decisions") private boolean enableIntertemporalOptimisations = false; - private boolean lifetimeIncomeGenerate = true; // request to generate new set of lifetime incomes - private Integer lifetimeIncomeStartBirthYear = 1934; - private Integer lifetimeIncomeEndBirthYear = 2019; + private boolean lifetimeIncomeImpute = true; // request to generate new set of lifetime incomes private Integer lifetimeIncomeEndAge = 80; - private Integer lifetimeIncomeCohortSize = 300000; - private boolean lifetimeIncomeWriteToCSV = false; + private Integer lifetimeIncomeCohortSize = 500000; + private boolean lifetimeIncomeWriteToCSV = true; private long lifetimeIncomeRandomSeed = 505; - private double lifetimeIncomeAge0StdDev = 0.9; - private boolean lifetimeIncomeImpute = false; // request to impute lifetime income data for population at load + List lifetimeIncomeTemplates = new ArrayList<>(); @GUIparameter(description = "tick to use behavioural solutions saved by a previous simulation") private boolean useSavedBehaviour = false; @@ -390,10 +384,9 @@ public void buildObjects() { projectSocialCare, donorPoolAveraging, taxDonorUpratingByWage, fixTimeTrend, flagDefaultToTimeSeriesAverages, saveImperfectTaxDBMatches, timeTrendStopsIn, startYear, endYear, interestRateInnov, disposableIncomeFromLabourInnov, flagSuppressChildcareCosts, flagSuppressSocialCareCosts, lifetimeIncomeImpute); - if (lifetimeIncomeGenerate) { - ManagerProjectLifetimeIncomes.run(log, lifetimeIncomeStartBirthYear, - lifetimeIncomeEndBirthYear, lifetimeIncomeEndAge, lifetimeIncomeCohortSize, lifetimeIncomeWriteToCSV, - lifetimeIncomeRandomSeed, lifetimeIncomeAge0StdDev, outputPath); + if (lifetimeIncomeImpute) { + ManagerLifetimeIncomes.generateNormalisedProfiles(log, lifetimeIncomeEndAge, lifetimeIncomeCohortSize, lifetimeIncomeTemplates, + lifetimeIncomeWriteToCSV, lifetimeIncomeRandomSeed, outputPath); } if (enableIntertemporalOptimisations) { @@ -2618,7 +2611,6 @@ private void createInitialPopulationDataStructures() { //TODO: Slight differences between otherwise identical simulations arise when loading "processed" vs "unprocessed" data (distinguished by the if statement below) Processed processed = PersistPopulation ? getProcessed() : null; - LifetimeIncomeImputation lifetimeIncomes = lifetimeIncomeImpute ? getLifetimeIncomes(startYear) : null; if (processed!=null) { List households = new ArrayList<>(processed.getHouseholds()); @@ -2626,10 +2618,6 @@ private void createInitialPopulationDataStructures() { throw new RuntimeException("No households in processed set"); System.out.println("Found processed dataset - preparing for simulation"); - // check if need to add income histories - if (lifetimeIncomes!=null) - lifetimeIncomes.matchDonorProfiles(households); - // update counters and augment attributes long householdIdCounter = 1L, benefitUnitIdCounter = 1L, personIdCounter = 1L; for ( Household originalHousehold : households) { @@ -2665,9 +2653,6 @@ private void createInitialPopulationDataStructures() { System.out.println("completed loading survey data for starting population"); log.info("completed loading survey data for starting population"); - // check if need to add income histories - if (lifetimeIncomes!=null) - lifetimeIncomes.matchDonorProfiles(inputHouseholdList); if (!useWeights) { // Expand population, sample, and remove weights @@ -2780,6 +2765,12 @@ else if (flagSearch1) { System.out.println("Time elapsed " + stopwatch.getTime()/1000 + " seconds"); } + if (lifetimeIncomeImpute) { + + String outputPath = "./output/" + getEngine().getCurrentExperiment().runId; + ManagerLifetimeIncomes.imputeProfiles(log, persons, lifetimeIncomeTemplates, lifetimeIncomeWriteToCSV, outputPath); + } + if (Parameters.projectNonPensionWealth) { // HW2c residual reconstruction uses the same common income ranks // that are assigned immediately before housing projection later. @@ -3590,42 +3581,6 @@ public void clearPersonsToMatch() { } } - private LifetimeIncomeImputation getLifetimeIncomes(int year) { - - System.out.println("Loading simulated income histories"); - LifetimeIncomeImputation lifetimeIncomes = null; - EntityTransaction txn = null; - try { - - // query database - String fileName = Parameters.getInputDirectory() + "input"; - var propertyMap = new HashMap(); - propertyMap.put("hibernate.connection.url", "jdbc:h2:file:" + fileName + ";TRACE_LEVEL_FILE=0;TRACE_LEVEL_SYSTEM_OUT=0;AUTO_SERVER=TRUE"); - EntityManager em = Persistence.createEntityManagerFactory("lifetime-incomes", propertyMap).createEntityManager(); - txn = em.getTransaction(); - txn.begin(); - String query = "SELECT DISTINCT cohort FROM BirthCohort cohort"; -// String query = "SELECT DISTINCT cohort FROM BirthCohort cohort LEFT JOIN FETCH cohort.individuals individuals LEFT JOIN FETCH individuals.incomes incomes"; - System.out.println("Submitting SQL query"); - log.info("Submitting SQL query: " + query); - List cohorts = em.createQuery(query).getResultList(); - lifetimeIncomes = new LifetimeIncomeImputation(year, cohorts); - - // close database connection - log.info("Query complete"); - System.out.println("Query complete"); - em.close(); - } catch (Exception e) { - if (txn != null) { - txn.rollback(); - } - e.printStackTrace(); - throw new RuntimeException("Problem sourcing data for starting population"); - } - - return lifetimeIncomes; - } - private Processed getProcessed() { return getProcessed(country, startYear, popSize, ignoreTargetsAtPopulationLoad); } diff --git a/src/main/java/simpaths/model/enums/Gender.java b/src/main/java/simpaths/model/enums/Gender.java index 4d7383d5d..bd04e1351 100644 --- a/src/main/java/simpaths/model/enums/Gender.java +++ b/src/main/java/simpaths/model/enums/Gender.java @@ -1,7 +1,12 @@ package simpaths.model.enums; public enum Gender { - Male, - Female, - + Male(true), + Female(false); + + private final boolean value; + Gender(boolean value) { this.value = value; } + public boolean toBoolean() { return value; } + + public static Gender of(boolean b) { return b ? Male : Female; } } diff --git a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java index 2f4ade51e..805e5bf24 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java +++ b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java @@ -4,90 +4,90 @@ public class AdjustmentFactors { public static double normIncomeZero = 1.0; - public static double fixedEffect = 0.5; + public static double fixedEffect = 1.0; public static double[] whiteNoise = { - 0.639893098 , // age0 - 0.624783408 , // age1 - 0.667171948 , // age2 - 0.683758156 , // age3 - 0.712236781 , // age4 - 0.709151244 , // age5 - 0.712304635 , // age6 - 0.730869311 , // age7 - 0.712430887 , // age8 - 0.683622348 , // age9 - 0.697016596 , // age10 - 0.666035772 , // age11 - 0.661157533 , // age12 - 0.669154405 , // age13 - 0.689440538 , // age14 - 0.687501894 , // age15 - 0.704932916 , // age16 - 0.697900413 , // age17 - 0.701738731 , // age18 - 0.680196543 , // age19 - 0.638906039 , // age20 - 0.659043878 , // age21 - 0.69077011 , // age22 - 0.676173163 , // age23 - 0.662343986 , // age24 - 0.670209912 , // age25 - 0.673320479 , // age26 - 0.68792881 , // age27 - 0.71796723 , // age28 - 0.735232476 , // age29 - 0.742738923 , // age30 - 0.722673522 , // age31 - 0.743136107 , // age32 - 0.738348963 , // age33 - 0.72350334 , // age34 - 0.766318724 , // age35 - 0.768365019 , // age36 - 0.7337006 , // age37 - 0.710851134 , // age38 - 0.72612377 , // age39 - 0.738621078 , // age40 - 0.730151591 , // age41 - 0.747148673 , // age42 - 0.746897174 , // age43 - 0.73598024 , // age44 - 0.751243438 , // age45 - 0.735483057 , // age46 - 0.745796089 , // age47 - 0.749050604 , // age48 - 0.751857051 , // age49 - 0.775671742 , // age50 - 0.768728264 , // age51 - 0.769569628 , // age52 - 0.771894225 , // age53 - 0.787275393 , // age54 - 0.781206324 , // age55 - 0.788353035 , // age56 - 0.82782717 , // age57 - 0.831884866 , // age58 - 0.842843976 , // age59 - 0.813602118 , // age60 - 0.754416871 , // age61 - 0.709298354 , // age62 - 0.723371377 , // age63 - 0.734968192 , // age64 - 0.632096908 , // age65 - 0.530836949 , // age66 - 0.494255143 , // age67 - 0.500151121 , // age68 - 0.517388875 , // age69 - 0.556422529 , // age70 - 0.562352937 , // age71 - 0.537723699 , // age72 - 0.529143288 , // age73 - 0.527895651 , // age74 - 0.525564874 , // age75 - 0.548628932 , // age76 - 0.561406355 , // age77 - 0.549365398 , // age78 - 0.547104755 , // age79 - 0.546168053 , // age80 + 1 , // age0 + 0.816551806 , // age1 + 0.671776289 , // age2 + 0.688421489 , // age3 + 0.679900794 , // age4 + 0.667012955 , // age5 + 0.722852228 , // age6 + 0.671472692 , // age7 + 0.599534122 , // age8 + 0.685351441 , // age9 + 0.613059005 , // age10 + 0.62131478 , // age11 + 0.628947678 , // age12 + 0.661954246 , // age13 + 0.621189189 , // age14 + 0.656172352 , // age15 + 0.618058971 , // age16 + 0.627020309 , // age17 + 0.526110173 , // age18 + 0.457971187 , // age19 + 0.460786097 , // age20 + 0.49127186 , // age21 + 0.501715014 , // age22 + 0.483131471 , // age23 + 0.548024396 , // age24 + 0.542679997 , // age25 + 0.535023471 , // age26 + 0.58000527 , // age27 + 0.617527725 , // age28 + 0.616615978 , // age29 + 0.584899953 , // age30 + 0.599483402 , // age31 + 0.629539457 , // age32 + 0.594541677 , // age33 + 0.693864587 , // age34 + 0.667192975 , // age35 + 0.640435411 , // age36 + 0.6357391 , // age37 + 0.62033899 , // age38 + 0.630532822 , // age39 + 0.599621547 , // age40 + 0.651843724 , // age41 + 0.651229367 , // age42 + 0.621933044 , // age43 + 0.652963517 , // age44 + 0.603598254 , // age45 + 0.639341694 , // age46 + 0.628733613 , // age47 + 0.610794804 , // age48 + 0.625973302 , // age49 + 0.596545879 , // age50 + 0.640012106 , // age51 + 0.63102302 , // age52 + 0.629489893 , // age53 + 0.595867162 , // age54 + 0.56465231 , // age55 + 0.599367946 , // age56 + 0.591013298 , // age57 + 0.635381115 , // age58 + 0.620930877 , // age59 + 0.540859636 , // age60 + 0.494865152 , // age61 + 0.546741693 , // age62 + 0.580068222 , // age63 + 0.484895284 , // age64 + 0.423128383 , // age65 + 0.43296438 , // age66 + 0.446951354 , // age67 + 0.45968271 , // age68 + 0.553448357 , // age69 + 0.525122451 , // age70 + 0.497218134 , // age71 + 0.498566639 , // age72 + 0.491156781 , // age73 + 0.483444267 , // age74 + 0.499260188 , // age75 + 0.522373778 , // age76 + 0.515497121 , // age77 + 0.53025143 , // age78 + 0.493819264 , // age79 + 0.493819264 , // age80 }; public static double getWhiteNoise(int age) { diff --git a/src/main/java/simpaths/model/lifetime_incomes/AnnualIncome.java b/src/main/java/simpaths/model/lifetime_incomes/AnnualIncome.java deleted file mode 100644 index 970c1e240..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/AnnualIncome.java +++ /dev/null @@ -1,144 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import jakarta.persistence.*; -import microsim.statistics.IDoubleSource; -import simpaths.data.Parameters; -import simpaths.model.enums.Gender; - -@Entity -public class AnnualIncome implements IDoubleSource { - - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", unique = true, nullable = false) private Long id; - @ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.REFRESH) - @JoinColumns({ - @JoinColumn(name = "individual_id", referencedColumnName = "id") - }) - private Individual individual; - - @Column(name="calendar_year") private int year; - @Column(name="income_value") private Double value; - @Transient private Double z_m1 = null; - @Transient private Double z_m2 = null; - - @Transient private Double rnd = null; - @Transient private Double innov = null; - @Transient private Double gmIncome = null; - @Transient private Double gmIncome_m1 = null; - - - /** - * CONSTRUCTOR - */ - public AnnualIncome() {} - public AnnualIncome(int year, Individual individual, double rnd, double age0StdDev) { - this.individual = individual; - this.year = year; - this.rnd = rnd; - value = incomeEvaluation(rnd, age0StdDev); - - individual.addAnnualIncome(this); - } - - public double getValue() { - if (Parameters.isFinite(value)) { - return value; - } - else { - throw new RuntimeException("AnnualIncome.getValue() called before value has been set"); - } - } - public int getYear() {return year;} - public Individual getIndividual() {return individual;} - public void setIndividual(Individual individual) { - this.individual = individual; - } - - public double incomeEvaluation(double rnd, double age0StdDev) { - - Double val = null; - int age = individual.getAge(year); - if (age==0) { - // random draw of new income - - double gmIncome = getGMEquivalisedIncome(year); - this.gmIncome = gmIncome; - double gauss = Parameters.getStandardNormalDistribution().inverseCumulativeProbability(rnd); - innov = gauss; - val = Math.exp(age0StdDev*gauss) * gmIncome; - } - else if (age==1) { - // AR1 process - - double gmIncome = getGMEquivalisedIncome(year); - this.gmIncome = gmIncome; - - double gmIncome_m1 = getGMEquivalisedIncome(year-1); - this.gmIncome_m1 = gmIncome_m1; - double income_m1 = individual.getAnnualIncome(year-1).getValue(); - - z_m1 = Math.log(income_m1 / gmIncome_m1); - - double z_score = Parameters.getRegLifetimeIncome2b().getScore(this, AnnualIncome.DoublesVariables.class); - innov = 0.0; - val = Math.exp(z_score+innov) * gmIncome; - } - else { - // AR2 process - - double gmIncome = getGMEquivalisedIncome(year); - this.gmIncome = gmIncome; - - double gmIncome_m1 = getGMEquivalisedIncome(year-1); - this.gmIncome_m1 = gmIncome_m1; - double income_m1 = individual.getAnnualIncome(year-1).getValue(); - - double gmIncome_m2 = getGMEquivalisedIncome( year-2); - double income_m2 = individual.getAnnualIncome(year-2).getValue(); - - z_m1 = Math.log(income_m1 / gmIncome_m1); - z_m2 = Math.log(income_m2 / gmIncome_m2); - - double z_score = Parameters.getRegLifetimeIncome2a().getScore(this, AnnualIncome.DoublesVariables.class); - innov = 0.0; - val = Math.exp(z_score+innov) * gmIncome; - } - return val; - } - - private double getGMEquivalisedIncome(int year) { - - individual.setYear(year); - int age = individual.getAge(); - Gender gender = individual.getGender(); - Double val = Parameters.getEquivalisedIncome(gender, age, year); - if (!Parameters.isFinite(val)) { - // outside observed range, use regression model - - if (Gender.Male.equals(gender)) { - val = Math.exp(Parameters.getRegLifetimeIncome1a().getScore(individual, Individual.DoublesVariables.class)); - } - else { - val = Math.exp(Parameters.getRegLifetimeIncome1b().getScore(individual, Individual.DoublesVariables.class)); - } - } - return val; - } - - public enum DoublesVariables { - z_lag1, - z_lag2 - } - public double getDoubleValue(Enum variableID) { - - switch ((AnnualIncome.DoublesVariables) variableID) { - case z_lag1 -> {return z_m1;} - case z_lag2 -> {return z_m2;} - default -> { - throw new RuntimeException("request for unrecognised variable: individual"); - } - } - } - public String toString() { - return id.toString(); - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/BirthCohort.java b/src/main/java/simpaths/model/lifetime_incomes/BirthCohort.java deleted file mode 100644 index 77b407b95..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/BirthCohort.java +++ /dev/null @@ -1,59 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import jakarta.persistence.*; - -import java.util.*; - -import simpaths.model.enums.Gender; - -@Entity -public class BirthCohort { - - - /** - * ATTRIBUTES - */ - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", unique = true, nullable = false) private Long id; - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "cohort") - @OrderBy("id ASC") - private Set individuals = new LinkedHashSet<>(); - - @Column(name="birth_year") private Integer birthYear; - @Column(name="end_age") private Integer endAge; - @Enumerated(EnumType.STRING) private Gender demSex; - - @Transient List sortedIndividuals = null; - - - /** - * CONSTRUCTOR - */ - public BirthCohort() {} - public BirthCohort(Integer birthYear, Gender demSex, Integer endAge) { - this.birthYear = birthYear; - this.demSex = demSex; - this.endAge = endAge; - } - - public Set getIndividuals() { - return individuals; - } - public void setIndividuals(Set individuals) { - this.individuals = individuals; - } - public void setSortedIndividuals(List sortedIndividuals) {this.sortedIndividuals = sortedIndividuals;} - public List getSortedIndividuals() {return sortedIndividuals;} - public int getBirthYear() { - return birthYear; - } - public Gender getGender() { - return demSex; - } - public int getEndAge() {return endAge;} - public void addIndividual(Individual individual) { - individuals.add(individual); - } - public String toString() { - return id.toString(); - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/EquivalisedIncomeCDF.java b/src/main/java/simpaths/model/lifetime_incomes/EquivalisedIncomeCDF.java deleted file mode 100644 index 7135afd43..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/EquivalisedIncomeCDF.java +++ /dev/null @@ -1,48 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import microsim.data.MultiKeyCoefficientMap; -import org.apache.commons.collections4.keyvalue.MultiKey; - -import java.lang.reflect.Array; -import java.util.*; -import java.util.stream.Collectors; - -public class EquivalisedIncomeCDF { - TreeMap cdf = new TreeMap<>(); - Object[] keys; - - public EquivalisedIncomeCDF(MultiKeyCoefficientMap data) { - - for (Object key: data.keySet()) { - - double keyValue = ((Number) ((MultiKey) key).getKey(0)).doubleValue(); - double valHere = ((Number) data.getValue(key)).doubleValue(); - cdf.put(keyValue,valHere); - } - keys = cdf.keySet().toArray(); - } - - public double getValue(double rnd) { - - // find bracket keys - int lwr = 0; - int upr = keys.length-1; - while (lwr < upr-1) { - int tstIndex = (upr + lwr) / 2; - double tstValue = cdf.get(keys[tstIndex]); - if (tstValue < rnd) { - lwr = tstIndex; - } else { - upr = tstIndex; - } - } - - // interpolate value and return - double lwrValue = cdf.get(keys[lwr]); - double uprValue = cdf.get(keys[upr]); - double ratio = (rnd - lwrValue)/(uprValue - lwrValue); - double lwrKey = (Double)keys[lwr]; - double uprKey = (Double)keys[upr]; - return (lwrKey + ratio*(uprKey - lwrKey)); - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/ExportCSV.java b/src/main/java/simpaths/model/lifetime_incomes/ExportCSV.java deleted file mode 100644 index 5edb40bf0..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/ExportCSV.java +++ /dev/null @@ -1,119 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import jakarta.persistence.Transient; -import microsim.data.db.PanelEntityKey; -import microsim.engine.SimulationEngine; -import simpaths.data.Parameters; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.*; - -public class ExportCSV { - - //Fields for exporting tables to output .csv files - final static String newLine = "\n"; - final static String delimiter = ","; - final static String directory = Parameters.getInputDirectory() + "lifetime_incomes"; - - private Set fieldsForExport; - private BufferedWriter bufferWriter; - private String idFieldName; - - private Collection targetCollection; - - public ExportCSV(int birthYear, Object target) { - try { - - targetCollection = (Collection) target; - Object obj = targetCollection.iterator().next(); - - //Set up file and fileWriter - create new file and directory if required - String filename = obj.getClass().getSimpleName() + birthYear; - File ff = new File(directory + File.separator + filename + ".csv"); - boolean fAlreadyExists = ff.exists(); - if (!fAlreadyExists) { - File dir = new File(directory); - dir.mkdirs(); - ff.createNewFile(); - } - else { - ff.delete(); - ff.createNewFile(); - } - bufferWriter = new BufferedWriter(new FileWriter(ff, true)); - - //Create alphabetically sorted list of fields including private and inherited fields that belong to the target class. - List declaredFields = new ArrayList(); - List allFields = getAllFields(declaredFields, obj.getClass()); - TreeSet fieldNames = new TreeSet(); - for (Field field : allFields) { - - if (field.getType().isPrimitive() || Number.class.isAssignableFrom(field.getType()) || - field.getType().equals(String.class)|| field.getType().equals(Boolean.class) || - field.getType().isEnum() || field.getType().equals(Character.class) || - field.getType().equals(BirthCohort.class) || field.getType().equals(Individual.class) || - field.getType().equals(AnnualIncome.class) - ) { - - String name = field.getName(); - fieldNames.add(name); - } - } - fieldsForExport = new LinkedHashSet(); - for(String fieldName : fieldNames) { //Iterated in correct order - fieldsForExport.add(fieldName); - bufferWriter.append(fieldName + delimiter); - } - - for (Object oo : targetCollection) { - - bufferWriter.append(newLine); - for (String fieldName : fieldsForExport) { - Field thisField = findUnderlyingField(oo.getClass(), fieldName); - thisField.setAccessible(true); - Object value = thisField.get(oo); - if (value == null) { - bufferWriter.append("null"); - } - else { - bufferWriter.append(value.toString()); - } - bufferWriter.append(delimiter); - } - } - bufferWriter.flush(); - bufferWriter.close(); - } catch (IOException e) { - e.printStackTrace(); - } catch (IllegalArgumentException | IllegalAccessException e) { - e.printStackTrace(); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - } - - - private static List getAllFields(List fields, Class type) { - fields.addAll(Arrays.asList(type.getDeclaredFields())); - if (type.getSuperclass() != null) { - fields = getAllFields(fields, type.getSuperclass()); - } - return fields; - } - - - private static Field findUnderlyingField(Class clazz, String fieldName) { - Class current = clazz; - do { - try { - return current.getDeclaredField(fieldName); - } catch(Exception e) {} - } while((current = current.getSuperclass()) != null); - return null; - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java b/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java new file mode 100644 index 000000000..2092d1884 --- /dev/null +++ b/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java @@ -0,0 +1,69 @@ +package simpaths.model.lifetime_incomes; + +import simpaths.model.Person; + +public class ImputedIncome { + + private long id; + private int age; + private int year; + private double income; + private Double observedIncome; + + public ImputedIncome() {} + public ImputedIncome(Person person, int age) { + this.id = person.getKey().getId(); + this.age = age; + this.year = person.getYear() - person.getDemAge() + age; + this.income = person.getImputedIncome(age); + if (person.getDemAge() == age) { + + double dispIncomePerYear = person.getBenefitUnit().getYDispMonth() * 12.0; + double equivScale = person.getBenefitUnit().getEquivalisedWeight(); + this.observedIncome = dispIncomePerYear / equivScale; + } + else { + this.observedIncome = null; + } + } + + public Double getObservedIncome() { + return observedIncome; + } + + public void setObservedIncome(Double observedIncome) { + this.observedIncome = observedIncome; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public double getIncome() { + return income; + } + + public void setIncome(double income) { + this.income = income; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } +} diff --git a/src/main/java/simpaths/model/lifetime_incomes/Individual.java b/src/main/java/simpaths/model/lifetime_incomes/Individual.java deleted file mode 100644 index b163b9c60..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/Individual.java +++ /dev/null @@ -1,268 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import jakarta.persistence.*; -import microsim.event.EventListener; -import microsim.statistics.IDoubleSource; -import simpaths.data.Parameters; -import simpaths.model.Person; -import simpaths.model.enums.Gender; -import simpaths.model.enums.TimeSeriesVariable; - -import java.util.LinkedHashSet; - -import java.util.Set; - -@Entity -public class Individual implements IDoubleSource { - - - /** - * ATTRIBUTES - */ - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", unique = true, nullable = false) private Long id; - @ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.REFRESH) - @JoinColumns({ - @JoinColumn(name = "cohort_id", referencedColumnName = "id") - }) - private BirthCohort cohort; - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "individual") - @OrderBy("id ASC") - private Set incomes = new LinkedHashSet<>(); - @OneToMany(fetch = FetchType.LAZY, mappedBy = "ltIncomeDonor") - private Set persons = new LinkedHashSet<>(); - - @Transient int year; - - - /** - * CONSTRUCTOR - */ - public Individual() { - } - public Individual(BirthCohort cohort) { - this.cohort = cohort; - //cohort.addIndividual(this); - } - - public Long getId() {return id;} - public Integer getAge() { - return (cohort!=null) ? year - cohort.getBirthYear() : null; - } - public Integer getAge(int year) { - return (cohort!=null) ? year - cohort.getBirthYear() : null; - } - public Gender getGender() { - return (cohort != null) ? cohort.getGender() : null; - } - public Integer getBirthYear() { - return (cohort != null) ? cohort.getBirthYear() : null; - } - public AnnualIncome getAnnualIncome(int year) { - AnnualIncome income = null; - for (AnnualIncome inc : incomes) { - if (inc.getYear()==year) { - income = inc; - break; - } - } - return income; - } - public void setCohort(BirthCohort cohort) {this.cohort = cohort;} - public void setYear(int year) {this.year = year;} - public void addAnnualIncome(AnnualIncome income) { - incomes.add(income); - } - - public enum DoublesVariables { - - Age0, - Age1, - Age2, - Age3, - Age4, - Age5, - Age6, - Age7, - Age8, - Age9, - Age10, - Age11, - Age12, - Age13, - Age14, - Age15, - Age16, - Age17, - Age18, - Age19, - Age20, - Age21, - Age22, - Age23, - Age24, - Age25, - Age26, - Age27, - Age28, - Age29, - Age30, - Age31, - Age32, - Age33, - Age34, - Age35, - Age36, - Age37, - Age38, - Age39, - Age40, - Age41, - Age42, - Age43, - Age44, - Age45, - Age46, - Age47, - Age48, - Age49, - Age50, - Age51, - Age52, - Age53, - Age54, - Age55, - Age56, - Age57, - Age58, - Age59, - Age60, - Age61, - Age62, - Age63, - Age64, - Age65, - Age66, - Age67, - Age68, - Age69, - Age70, - Age71, - Age72, - Age73, - Age74, - Age75, - Age76, - Age77, - Age78, - Age79, - Age80, - Age81, - Age82, - Age83, - Age84, - Age80plus, - Age85plus, - Log_GDP_per_capita - } - public double getDoubleValue(Enum variableID) { - - switch ((Individual.DoublesVariables) variableID) { - case Age0 -> {return (year-getBirthYear()==0) ? 1: 0;} - case Age1 -> {return (year-getBirthYear()==1) ? 1: 0;} - case Age2 -> {return (year-getBirthYear()==2) ? 1: 0;} - case Age3 -> {return (year-getBirthYear()==3) ? 1: 0;} - case Age4 -> {return (year-getBirthYear()==4) ? 1: 0;} - case Age5 -> {return (year-getBirthYear()==5) ? 1: 0;} - case Age6 -> {return (year-getBirthYear()==6) ? 1: 0;} - case Age7 -> {return (year-getBirthYear()==7) ? 1: 0;} - case Age8 -> {return (year-getBirthYear()==8) ? 1: 0;} - case Age9 -> {return (year-getBirthYear()==9) ? 1: 0;} - case Age10 -> {return (year-getBirthYear()==10) ? 1: 0;} - case Age11 -> {return (year-getBirthYear()==11) ? 1: 0;} - case Age12 -> {return (year-getBirthYear()==12) ? 1: 0;} - case Age13 -> {return (year-getBirthYear()==13) ? 1: 0;} - case Age14 -> {return (year-getBirthYear()==14) ? 1: 0;} - case Age15 -> {return (year-getBirthYear()==15) ? 1: 0;} - case Age16 -> {return (year-getBirthYear()==16) ? 1: 0;} - case Age17 -> {return (year-getBirthYear()==17) ? 1: 0;} - case Age18 -> {return (year-getBirthYear()==18) ? 1: 0;} - case Age19 -> {return (year-getBirthYear()==19) ? 1: 0;} - case Age20 -> {return (year-getBirthYear()==20) ? 1: 0;} - case Age21 -> {return (year-getBirthYear()==21) ? 1: 0;} - case Age22 -> {return (year-getBirthYear()==22) ? 1: 0;} - case Age23 -> {return (year-getBirthYear()==23) ? 1: 0;} - case Age24 -> {return (year-getBirthYear()==24) ? 1: 0;} - case Age25 -> {return (year-getBirthYear()==25) ? 1: 0;} - case Age26 -> {return (year-getBirthYear()==26) ? 1: 0;} - case Age27 -> {return (year-getBirthYear()==27) ? 1: 0;} - case Age28 -> {return (year-getBirthYear()==28) ? 1: 0;} - case Age29 -> {return (year-getBirthYear()==29) ? 1: 0;} - case Age30 -> {return (year-getBirthYear()==30) ? 1: 0;} - case Age31 -> {return (year-getBirthYear()==31) ? 1: 0;} - case Age32 -> {return (year-getBirthYear()==32) ? 1: 0;} - case Age33 -> {return (year-getBirthYear()==33) ? 1: 0;} - case Age34 -> {return (year-getBirthYear()==34) ? 1: 0;} - case Age35 -> {return (year-getBirthYear()==35) ? 1: 0;} - case Age36 -> {return (year-getBirthYear()==36) ? 1: 0;} - case Age37 -> {return (year-getBirthYear()==37) ? 1: 0;} - case Age38 -> {return (year-getBirthYear()==38) ? 1: 0;} - case Age39 -> {return (year-getBirthYear()==39) ? 1: 0;} - case Age40 -> {return (year-getBirthYear()==40) ? 1: 0;} - case Age41 -> {return (year-getBirthYear()==41) ? 1: 0;} - case Age42 -> {return (year-getBirthYear()==42) ? 1: 0;} - case Age43 -> {return (year-getBirthYear()==43) ? 1: 0;} - case Age44 -> {return (year-getBirthYear()==44) ? 1: 0;} - case Age45 -> {return (year-getBirthYear()==45) ? 1: 0;} - case Age46 -> {return (year-getBirthYear()==46) ? 1: 0;} - case Age47 -> {return (year-getBirthYear()==47) ? 1: 0;} - case Age48 -> {return (year-getBirthYear()==48) ? 1: 0;} - case Age49 -> {return (year-getBirthYear()==49) ? 1: 0;} - case Age50 -> {return (year-getBirthYear()==50) ? 1: 0;} - case Age51 -> {return (year-getBirthYear()==51) ? 1: 0;} - case Age52 -> {return (year-getBirthYear()==52) ? 1: 0;} - case Age53 -> {return (year-getBirthYear()==53) ? 1: 0;} - case Age54 -> {return (year-getBirthYear()==54) ? 1: 0;} - case Age55 -> {return (year-getBirthYear()==55) ? 1: 0;} - case Age56 -> {return (year-getBirthYear()==56) ? 1: 0;} - case Age57 -> {return (year-getBirthYear()==57) ? 1: 0;} - case Age58 -> {return (year-getBirthYear()==58) ? 1: 0;} - case Age59 -> {return (year-getBirthYear()==59) ? 1: 0;} - case Age60 -> {return (year-getBirthYear()==60) ? 1: 0;} - case Age61 -> {return (year-getBirthYear()==61) ? 1: 0;} - case Age62 -> {return (year-getBirthYear()==62) ? 1: 0;} - case Age63 -> {return (year-getBirthYear()==63) ? 1: 0;} - case Age64 -> {return (year-getBirthYear()==64) ? 1: 0;} - case Age65 -> {return (year-getBirthYear()==65) ? 1: 0;} - case Age66 -> {return (year-getBirthYear()==66) ? 1: 0;} - case Age67 -> {return (year-getBirthYear()==67) ? 1: 0;} - case Age68 -> {return (year-getBirthYear()==68) ? 1: 0;} - case Age69 -> {return (year-getBirthYear()==69) ? 1: 0;} - case Age70 -> {return (year-getBirthYear()==70) ? 1: 0;} - case Age71 -> {return (year-getBirthYear()==71) ? 1: 0;} - case Age72 -> {return (year-getBirthYear()==72) ? 1: 0;} - case Age73 -> {return (year-getBirthYear()==73) ? 1: 0;} - case Age74 -> {return (year-getBirthYear()==74) ? 1: 0;} - case Age75 -> {return (year-getBirthYear()==75) ? 1: 0;} - case Age76 -> {return (year-getBirthYear()==76) ? 1: 0;} - case Age77 -> {return (year-getBirthYear()==77) ? 1: 0;} - case Age78 -> {return (year-getBirthYear()==78) ? 1: 0;} - case Age79 -> {return (year-getBirthYear()==79) ? 1: 0;} - case Age80 -> {return (year-getBirthYear()==80) ? 1: 0;} - case Age81 -> {return (year-getBirthYear()==81) ? 1: 0;} - case Age82 -> {return (year-getBirthYear()==82) ? 1: 0;} - case Age83 -> {return (year-getBirthYear()==83) ? 1: 0;} - case Age84 -> {return (year-getBirthYear()==84) ? 1: 0;} - case Age80plus -> {return (year-getBirthYear()>79) ? 1: 0;} - case Age85plus -> {return (year-getBirthYear()>84) ? 1: 0;} - case Log_GDP_per_capita -> { - return Math.log(Parameters.getTimeSeriesValue(year, TimeSeriesVariable.GDPperCapita)); - } - default -> { - throw new RuntimeException("request for unrecognised variable"); - } - } - } - public String toString() { - return id.toString(); - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/IndividualComparator.java b/src/main/java/simpaths/model/lifetime_incomes/IndividualComparator.java deleted file mode 100644 index 6ff80f3da..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/IndividualComparator.java +++ /dev/null @@ -1,18 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import java.util.Comparator; - -public class IndividualComparator implements Comparator { - - int year; - - @Override - public int compare(Individual firstIndividual, Individual secondIndividual) { - return Double.compare(firstIndividual.getAnnualIncome(year).getValue(), - secondIndividual.getAnnualIncome(year).getValue()); - } - - public IndividualComparator(int year) { - this.year = year; - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/LifetimeIncomeImputation.java b/src/main/java/simpaths/model/lifetime_incomes/LifetimeIncomeImputation.java deleted file mode 100644 index b3cc02fbe..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/LifetimeIncomeImputation.java +++ /dev/null @@ -1,170 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import simpaths.model.BenefitUnit; -import simpaths.model.Household; -import simpaths.model.Person; -import simpaths.model.enums.Gender; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.stream.IntStream; - -public class LifetimeIncomeImputation { - - Integer year; - Integer endAge = null; - List cohorts; - - public LifetimeIncomeImputation(int year, List cohorts) { - this.year = year; - this.cohorts = cohorts; - IndividualComparator comparator = new IndividualComparator(year); - for (BirthCohort cohort : cohorts) { - - if (endAge==null) - endAge = cohort.getEndAge(); - - if (cohort.getBirthYear() <= year && cohort.getBirthYear() + endAge >= year) { - - List individuals = new ArrayList<>(cohort.getIndividuals()); - individuals.sort(comparator); - cohort.setSortedIndividuals(individuals); - } - } - } - - public void matchDonorProfiles(List households) { - - if (ltIncomeNeeded(households)) { - // require imputation of income histories - System.out.println("Imputing income histories for simulated population"); - IntStream.range(0, households.size()).sorted().parallel().forEach(ii -> { - //for (int ii=0; ii < households.size(); ii++) { - - // consider each household in initialisation set - Household household = households.get(ii); - - // identify target equivalised household income - double disposableIncomePerAnnum = 0.0; - double equivalenceScale = 0.0; - boolean firstAdult = true; - for (BenefitUnit benefitUnit : household.getBenefitUnits()) { - disposableIncomePerAnnum += benefitUnit.getDisposableIncomeMonthly() * 12.0; - for (Person pp : benefitUnit.getMembers()) { - if (pp.getDemAge() > 13) { - if (firstAdult) { - equivalenceScale += 1.0; - firstAdult = false; - } else { - equivalenceScale += 0.5; - } - } else { - equivalenceScale += 0.3; - } - } - } - double targetIncome = disposableIncomePerAnnum / equivalenceScale; - - for (BenefitUnit benefitUnit : household.getBenefitUnits()) { - // loop through each benefit unit - - for (Person person : benefitUnit.getMembers()) { - // loop through each person - - if (person.getDemAge()==0) { - person.setLtIncome(endAge); - } - else { - - // match birth cohort (birth year and gender) - List individuals = getLtIncomeDonors(person); - if (individuals.isEmpty()) - throw new IllegalArgumentException("No individuals found for person " + person.getId()); - - // match income - int lwr = 0, upr = individuals.size() - 1; - double lwrValue = individuals.get(lwr).getAnnualIncome(year).getValue() - targetIncome; - if (lwrValue > 0.0) { - // lower bound - - person.setLtIncomeDonor(individuals.get(lwr)); - } else { - double uprValue = individuals.get(upr).getAnnualIncome(year).getValue() - targetIncome; - if (uprValue < 0.0) { - // upper bound - - person.setLtIncomeDonor(individuals.get(upr)); - } else { - // find bounded value - - while (lwr < upr - 1) { - int tstIndex = (upr + lwr) / 2; - double tstValue = individuals.get(tstIndex).getAnnualIncome(year).getValue() - targetIncome; - if (tstValue < 0.0) { - lwr = tstIndex; - lwrValue = tstValue; - } else { - upr = tstIndex; - uprValue = tstValue; - } - } - if (uprValue < -lwrValue) { - person.setLtIncomeDonor(individuals.get(upr)); - } else { - person.setLtIncomeDonor(individuals.get(lwr)); - } - } - } - person.setLtIncome(endAge); - } - } - } - }); - //} - System.out.println("Completed imputing income histories for simulated population"); - } - } - - private List getLtIncomeDonors(Person person) { - int birthYear = year - Math.min(person.getDemAge(),endAge); - Gender gender = person.getDemMaleFlag(); - BirthCohort targetCohort = null; - for (BirthCohort cohort : cohorts) { - - if (cohort.getBirthYear() == birthYear && cohort.getGender() == gender) { - targetCohort = cohort; - break; - } - } - if (targetCohort == null) - throw new IllegalArgumentException("No cohort found for birth year " + birthYear + " and gender " + gender); - List individuals = targetCohort.getSortedIndividuals(); - return individuals; - } - - private boolean ltIncomeNeeded(List households) { - - boolean ltIncomeNeeded = false; - if (!households.isEmpty()) { - - Iterator iteratorHousehold = households.iterator(); - Household household = iteratorHousehold.next(); - Set benefitUnits = household.getBenefitUnits(); - if (!benefitUnits.isEmpty()) { - - Iterator iteratorBenefitUnit = benefitUnits.iterator(); - BenefitUnit benefitUnit = iteratorBenefitUnit.next(); - Set persons = benefitUnit.getMembers(); - if (!persons.isEmpty()) { - - Iterator iteratorPerson = persons.iterator(); - Person person = iteratorPerson.next(); - ltIncomeNeeded = (person.getLtIncomeDonor() == null); - } - } - } - return ltIncomeNeeded; - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java b/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java new file mode 100644 index 000000000..16d5268e8 --- /dev/null +++ b/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java @@ -0,0 +1,172 @@ +package simpaths.model.lifetime_incomes; + +import com.opencsv.bean.CsvToBeanBuilder; +import com.opencsv.bean.StatefulBeanToCsv; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import com.opencsv.exceptions.CsvDataTypeMismatchException; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; +import simpaths.data.Parameters; +import java.io.*; +import java.util.*; +import java.util.random.RandomGenerator; +import java.util.stream.IntStream; +import org.apache.logging.log4j.Logger; +import simpaths.model.Person; + +public class ManagerLifetimeIncomes { + + + public static void generateNormalisedProfiles(Logger log, Integer endAge, Integer simCohortSize, List templateIndividuals, boolean writeToCSV, long seed, String outputDir) { + + log.info("Initialising lifetime income projections"); + System.out.println("Initialising lifetime income projections"); + + // start projecting lifetime incomes + RandomGenerator generator = new Random(seed); + + // load initialisation values + List initialisationObservations; + try { + String path = Parameters.getInputDirectory() + "lifetime_incomes" + File.separator + "initialisation_observations.csv"; + FileReader fileReader = new FileReader(path); + initialisationObservations = new CsvToBeanBuilder(fileReader).withType(InitialisationObservation.class).build().parse(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem reading initialisation observations"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + int initSampleSize = initialisationObservations.size(); + + // load white noise estimates + List whiteNoiseEstimatesAll; + try { + String path = Parameters.getInputDirectory() + "lifetime_incomes" + File.separator + "whitenoise_estimates.csv"; + FileReader fileReader = new FileReader(path); + whiteNoiseEstimatesAll = new CsvToBeanBuilder(fileReader).withType(WhiteNoiseEstimate.class).build().parse(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem reading initialisation observations"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + + // initialise template donors for all age/gender cohorts + for (int ii=0; ii < simCohortSize; ii++) { + + InitialisationObservation obs = initialisationObservations.get(generator.nextInt(initSampleSize)); + TemplateIndividual individual = new TemplateIndividual(ii, obs, generator.nextLong()); + templateIndividuals.add(individual); + } + + // project lifetimes for template donors + for (int age = 1; age <= endAge; age++) { + + final int currentAge = age; + List whiteNoiseEstimates = whiteNoiseEstimatesAll.stream().filter(w -> w.getAge() == currentAge).toList(); + int noiseSampleSize = whiteNoiseEstimates.size(); + double adjFactor = AdjustmentFactors.getWhiteNoise(currentAge); + + templateIndividuals.parallelStream().forEach(individual -> { + //for (TemplateIndividual individual : templateIndividuals) { + + WhiteNoiseEstimate noise = whiteNoiseEstimates.get(individual.getNextInt(noiseSampleSize)); + double etaAdj = noise.getEtaHat() * adjFactor; + individual.projectNormIncome(currentAge, etaAdj); + //} + }); + } + + // write template profiles to csv + boolean writeToCSV2 = true; + if (writeToCSV) { + + List incomes = new ArrayList<>(); + for (TemplateIndividual individual : templateIndividuals) { + + incomes.addAll(individual.getNormIncomes()); + } + try { + String path = outputDir + File.separator + "normalised_incomes.csv"; + Writer writer = new FileWriter(path); + StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build(); + beanToCsv.write(incomes); + writer.close(); + } catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) { + // a bean had an empty required field or a value that couldn't be converted + System.err.println("Could not write CSV: " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem reading initialisation observations"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + } + + // finalise + log.info("Completed lifetime income projections"); + } + + public static void imputeProfiles(Logger log, Set personSet, List templateIndividuals, boolean writeToCSV, String outputDir) { + + log.info("Imputing lifetime income profiles for sample"); + System.out.println("Imputing lifetime income profiles for sample"); + + int endAge = templateIndividuals.getFirst().getNormIncomes().size() - 1; + + // loop through each age in order + for (int age = 0; age <= endAge; age++) { + + // sort template individuals by norm income at current age + int ageHere = age; + templateIndividuals.sort(Comparator.comparingDouble(individual -> individual.getNormIncome(ageHere).getZValue())); + final double[] sortedZ = templateIndividuals.stream().mapToDouble(individual -> individual.getNormIncome(ageHere).getZValue()).toArray(); + boolean agePlus = (age==endAge); + + // loop through person set + personSet.parallelStream().forEach(person -> { + //for (Person person : personSet) { + + person.matchLifetimeIncome(ageHere, agePlus, sortedZ, templateIndividuals); + //} + }); + } + + // write template profiles to csv + boolean writeToCSV2 = true; + if (writeToCSV) { + + List incomes = new ArrayList<>(); + for (Person person : personSet) { + + for (int age = 0; age <= endAge; age++) { + + ImputedIncome income = new ImputedIncome(person, age); + incomes.add(income); + } + } + try { + String path = outputDir + File.separator + "imputed_incomes.csv"; + Writer writer = new FileWriter(path); + StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build(); + beanToCsv.write(incomes); + writer.close(); + } catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) { + // a bean had an empty required field or a value that couldn't be converted + System.err.println("Could not write CSV: " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem writing imputed incomes"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + } + + // finalise + log.info("Completed imputing lifetime incomes"); + } +} diff --git a/src/main/java/simpaths/model/lifetime_incomes/ManagerProjectLifetimeIncomes.java b/src/main/java/simpaths/model/lifetime_incomes/ManagerProjectLifetimeIncomes.java deleted file mode 100644 index 610dc5fc9..000000000 --- a/src/main/java/simpaths/model/lifetime_incomes/ManagerProjectLifetimeIncomes.java +++ /dev/null @@ -1,252 +0,0 @@ -package simpaths.model.lifetime_incomes; - -import com.opencsv.bean.CsvToBeanBuilder; -import com.opencsv.bean.StatefulBeanToCsv; -import com.opencsv.bean.StatefulBeanToCsvBuilder; -import com.opencsv.exceptions.CsvDataTypeMismatchException; -import com.opencsv.exceptions.CsvRequiredFieldEmptyException; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityTransaction; -import jakarta.persistence.Persistence; -import simpaths.data.Parameters; -import simpaths.model.enums.Gender; - -import java.io.*; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.*; -import java.util.random.RandomGenerator; -import java.util.stream.IntStream; - -import org.apache.logging.log4j.Logger; - -public class ManagerProjectLifetimeIncomes { - - - /** - * ENTRY POINT FOR MANAGER - */ - public static void run(Logger log, Integer startBirthYear, Integer endBirthYear, - Integer endAge, Integer simCohortSize, boolean writeToCSV, long seed, double age0StdDev, String outputDir) { - - log.info("Initialising lifetime income projections"); - System.out.println("Initialising lifetime income projections"); - - // start projecting lifetime incomes - initialiseLifetimeIncomeDatabase(); - RandomGenerator generator = new Random(seed); - - // load initialisation values - List initialisationObservations; - try { - String path = Parameters.getInputDirectory() + "lifetime_incomes" + File.separator + "initialisation_observations.csv"; - FileReader fileReader = new FileReader(path); - initialisationObservations = new CsvToBeanBuilder(fileReader).withType(InitialisationObservation.class).build().parse(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Problem reading initialisation observations"); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - int initSampleSize = initialisationObservations.size(); - - // load white noise estimates - List whiteNoiseEstimates; - try { - String path = Parameters.getInputDirectory() + "lifetime_incomes" + File.separator + "whitenoise_estimates.csv"; - FileReader fileReader = new FileReader(path); - whiteNoiseEstimates = new CsvToBeanBuilder(fileReader).withType(WhiteNoiseEstimate.class).build().parse(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Problem reading initialisation observations"); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - int noiseSampleSize = whiteNoiseEstimates.size(); - - // generate common set of z data for all age/gender cohorts - List templateIndividuals = new ArrayList<>(); - - // initialise template individuals - for (int ii=0; ii < simCohortSize; ii++) { - - InitialisationObservation obs = initialisationObservations.get(generator.nextInt(initSampleSize)); - TemplateIndividual individual = new TemplateIndividual(ii, obs, generator.nextLong()); - templateIndividuals.add(individual); - } - - // project lifetimes for template individuals - IntStream.range(0, simCohortSize).parallel().forEach(ii -> { - //for (TemplateIndividual individual : templateIndividuals) { - - TemplateIndividual individual = templateIndividuals.get(ii); - for (int age = 1; age <= endAge; age++) { - - WhiteNoiseEstimate noise = whiteNoiseEstimates.get(individual.getNextInt(noiseSampleSize)); - double etaAdj = noise.getEtaHat() * AdjustmentFactors.getWhiteNoise(age); - individual.projectNormIncome(age, etaAdj); - } - //} - }); - - boolean writeToCSV2 = true; - if (writeToCSV2) { - - List incomes = new ArrayList<>(); - for (TemplateIndividual individual : templateIndividuals) { - - incomes.addAll(individual.getNormIncomes()); - } - try { - String path = outputDir + File.separator + "normalised_incomes.csv"; - Writer writer = new FileWriter(path); - StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build(); - beanToCsv.write(incomes); - writer.close(); - } catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) { - // a bean had an empty required field or a value that couldn't be converted - System.err.println("Could not write CSV: " + e.getMessage()); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Problem reading initialisation observations"); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - } - - // loop over birth years - boolean initialised = true; - for (int by = startBirthYear; by <= endBirthYear; by++) { - - log.info("Projecting lifetime incomes for birth year " + by); - System.out.println("Projecting lifetime incomes for birth year " + by); - - // initialise sets for csv reporting - Set cohorts = new LinkedHashSet<>(); - Set individuals = new LinkedHashSet<>(); - Set annualIncomes = new LinkedHashSet<>(); - - // loop over genders - for (Gender gender : Gender.values()) { - - BirthCohort birthCohort = new BirthCohort(by, gender, endAge); - cohorts.add(birthCohort); - Individual[] individualsLocal = new Individual[simCohortSize]; - AnnualIncome[] annualIncomesLocal = new AnnualIncome[simCohortSize*(endAge+1)]; - - // loop over individuals - final int birthYear = by; - IntStream.range(0, simCohortSize).parallel().forEach(ii -> { -// for (int ii=0; ii < simCohortSize; ii++) { - // loop over new individuals - - Individual individual = new Individual(birthCohort); - individualsLocal[ii] = individual; - - // loop over ages - for (int aa = 0; aa <= endAge; aa++) { - - double rnd = generator.nextDouble(); - AnnualIncome income = new AnnualIncome(birthYear + aa, individual, rnd, age0StdDev); - annualIncomesLocal[ii*(endAge+1)+aa] = income; - } -// } - }); - for (Individual individual : individualsLocal) { - birthCohort.addIndividual(individual); - individuals.add(individual); - } - annualIncomes.addAll(Arrays.asList(annualIncomesLocal)); - - // commit to database - writeLifetimeIncomeDatabase(log, birthCohort); - } - - // write to csv - if (writeToCSV) { - ExportCSV exportCohorts = new ExportCSV(by, cohorts); - ExportCSV exportIndividuals = new ExportCSV(by, individuals); - ExportCSV exportAnnualIncomes = new ExportCSV(by, annualIncomes); - } - } - - log.info("Completed lifetime income projections"); - } - - private static void writeLifetimeIncomeDatabase(Logger log, BirthCohort cohort) { - - EntityTransaction txn = null; - try { - // initialise database for storing results - String fileName = Parameters.getInputDirectory() + "input"; - var propertyMap = new HashMap(); - propertyMap.put("hibernate.connection.url", "jdbc:h2:file:" + fileName + ";TRACE_LEVEL_FILE=0;TRACE_LEVEL_SYSTEM_OUT=0;AUTO_SERVER=TRUE"); - EntityManager em = Persistence.createEntityManagerFactory("lifetime-incomes", propertyMap).createEntityManager(); - txn = em.getTransaction(); - txn.begin(); - log.info("Running em.persist()"); - em.persist(cohort); - txn.commit(); - em.close(); - } catch (Exception e) { - if (txn != null) - txn.rollback(); - e.printStackTrace(); - throw new RuntimeException("Problem writing lifetime incomes dataset"); - } - } - - private static void initialiseLifetimeIncomeDatabase() { - - // initialise database connection - Connection conn = null; - try { - String fileName = Parameters.getInputDirectory() + "input"; - Class.forName("org.h2.Driver"); - conn = DriverManager.getConnection("jdbc:h2:file:" + fileName + ";TRACE_LEVEL_FILE=0;TRACE_LEVEL_SYSTEM_OUT=0;AUTO_SERVER=TRUE", "sa", ""); - - // check that database is empty at start - Statement stat = null; - try { - stat = conn.createStatement(); - stat.execute( "DROP TABLE IF EXISTS birthcohort CASCADE;"); - stat.execute( "DROP TABLE IF EXISTS individual CASCADE;"); - stat.execute( "DROP TABLE IF EXISTS annualincome CASCADE;"); - stat.execute( "CREATE TABLE birthcohort (ID BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, BIRTH_YEAR INT DEFAULT 2019, END_AGE INT DEFAULT 80, GENDER VARCHAR_IGNORECASE DEFAULT 'Male');"); - } catch(Exception e){ - // throw new IllegalArgumentException("SQL Exception thrown!" + e.getMessage()); - e.printStackTrace(); - } - finally { - try { - if(stat != null) - stat.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - catch(ClassNotFoundException | SQLException e){ - if(e instanceof ClassNotFoundException) { - System.out.println( "ERROR: Class not found: " + e.getMessage() + "\nCheck that the input.h2.db " - + "exists in the input folder. If not, unzip the input.h2.zip file and store the resulting " - + "input.h2.db in the input folder!\n"); - } - else { - throw new IllegalArgumentException("SQL Exception thrown! " + e.getMessage()); - } - } - finally { - try { - if (conn != null) { conn.close(); } - } catch (SQLException e) { - e.printStackTrace(); - } - } - } -} diff --git a/src/main/java/simpaths/model/lifetime_incomes/WhiteNoiseEstimate.java b/src/main/java/simpaths/model/lifetime_incomes/WhiteNoiseEstimate.java index 220b8e866..b736d1539 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/WhiteNoiseEstimate.java +++ b/src/main/java/simpaths/model/lifetime_incomes/WhiteNoiseEstimate.java @@ -10,7 +10,7 @@ public class WhiteNoiseEstimate { @CsvBindByName private double pidp; // person id @CsvBindByName private double etaHat; // estimated white noise - @CsvBindByName private double age; // observation age + @CsvBindByName private int age; // observation age @CsvBindByName private double weight; // survey weight // Required by CsvToObjectLoader (reflection-based instantiation). @@ -22,5 +22,5 @@ public WhiteNoiseEstimate() { public void setPidp(double pidp) { this.pidp = pidp; } public double getEtaHat() { return etaHat; } public void setEtaHat(double etaHat) { this.etaHat = etaHat; } - public double getAge() { return age; } + public int getAge() { return age; } } From 36cfb664c7f2b21eeddde108ba184c4a887f50ab Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:48:36 +0200 Subject: [PATCH 3/6] lifetime income analysis --- .gitignore | 30 ++ .../reg_lifetime_incomes.xlsx | Bin 365408 -> 366900 bytes src/main/java/simpaths/data/Parameters.java | 7 +- src/main/java/simpaths/model/Person.java | 14 +- .../java/simpaths/model/SimPathsModel.java | 8 +- .../lifetime_incomes/AdjustmentFactors.java | 177 ++++++++- .../model/lifetime_incomes/ImputedIncome.java | 30 ++ .../ManagerLifetimeIncomes.java | 114 +++++- .../lifetime_incomes/RegressionProxy.java | 374 ++++++++++++++++++ .../lifetime_incomes/SampleIndividual.java | 183 +++++++++ 10 files changed, 915 insertions(+), 22 deletions(-) create mode 100644 src/main/java/simpaths/model/lifetime_incomes/RegressionProxy.java create mode 100644 src/main/java/simpaths/model/lifetime_incomes/SampleIndividual.java diff --git a/.gitignore b/.gitignore index ce6802237..69c98a263 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,33 @@ input/EUROMODoutput/baseline/* input/InitialPopulations/original/* input/InitialPopulations/base/* +input/lifetime_incomes/sample/1994.csv +input/lifetime_incomes/sample/1995.csv +input/lifetime_incomes/sample/1996.csv +input/lifetime_incomes/sample/1997.csv +input/lifetime_incomes/sample/1998.csv +input/lifetime_incomes/sample/1999.csv +input/lifetime_incomes/sample/2000.csv +input/lifetime_incomes/sample/2001.csv +input/lifetime_incomes/sample/2002.csv +input/lifetime_incomes/sample/2003.csv +input/lifetime_incomes/sample/2004.csv +input/lifetime_incomes/sample/2005.csv +input/lifetime_incomes/sample/2006.csv +input/lifetime_incomes/sample/2007.csv +input/lifetime_incomes/sample/2008.csv +input/lifetime_incomes/sample/2009.csv +input/lifetime_incomes/sample/2010.csv +input/lifetime_incomes/sample/2011.csv +input/lifetime_incomes/sample/2012.csv +input/lifetime_incomes/sample/2013.csv +input/lifetime_incomes/sample/2014.csv +input/lifetime_incomes/sample/2015.csv +input/lifetime_incomes/sample/2016.csv +input/lifetime_incomes/sample/2017.csv +input/lifetime_incomes/sample/2018.csv +input/lifetime_incomes/sample/2019.csv +input/lifetime_incomes/sample/2020.csv +input/lifetime_incomes/sample/2021.csv +input/lifetime_incomes/sample/2022.csv +input/lifetime_incomes/sample/2023.csv diff --git a/input/regression_files/reg_lifetime_incomes.xlsx b/input/regression_files/reg_lifetime_incomes.xlsx index b0c0a919df6576ea2f72d422618a064bc6545ebc..52d324d78a50cd60bb1aff36cf696189b6371df2 100644 GIT binary patch delta 11016 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 && age<=equivalisedIncomeMaxAge) { + int ageHere = Math.min(age, equivalisedIncomeMaxAge); + if (year<=equivalisedIncomeMaxYear && year>=equivalisedIncomeMinYear) { // obtain value from observed matrix - Number nn = ((Number) equivalisedIncomeByGenderAgeYear.getValue(demSex.toString(), age, year)); + Number nn = ((Number) equivalisedIncomeByGenderAgeYear.getValue(demSex.toString(), ageHere, year)); if (nn==null) { - throw new IllegalAccessError("ERROR - problem evaluating mean equivalised income for year: " + year + ", age: " + age + " and demSex " + demSex.toString()); + throw new IllegalAccessError("ERROR - problem evaluating mean equivalised income for year: " + year + ", age: " + ageHere + " and demSex " + demSex.toString()); } val = nn.doubleValue(); } diff --git a/src/main/java/simpaths/model/Person.java b/src/main/java/simpaths/model/Person.java index 62478f40d..e589e3683 100644 --- a/src/main/java/simpaths/model/Person.java +++ b/src/main/java/simpaths/model/Person.java @@ -25,6 +25,7 @@ import simpaths.model.decisions.Axis; import simpaths.model.decisions.DecisionParams; import simpaths.model.enums.*; +import simpaths.model.lifetime_incomes.RegressionProxy; import simpaths.model.lifetime_incomes.TemplateIndividual; import simpaths.model.person.PrivatePension; @@ -5266,12 +5267,11 @@ public void matchLifetimeIncome(int age, boolean agePlus, final double[] sortedZ if (matchHere) { // identify normalised income target - double dispIncomePerYear = benefitUnit.getYDispMonth() * 12.0; - if (dispIncomePerYear > 0.0) { + double eqIncomePerYear = benefitUnit.getHousehold().getEquivalisedDisposableIncomeYearly(); + if (eqIncomePerYear > 0.0) { double gmEqDispIncomePerYear = getGMEquivalisedIncome(demAge, getYear()); - double equivScale = benefitUnit.getEquivalisedWeight(); - double zTarget = Math.log(dispIncomePerYear / equivScale / gmEqDispIncomePerYear); + double zTarget = Math.log(eqIncomePerYear / gmEqDispIncomePerYear); int ii = Arrays.binarySearch(sortedZ, zTarget); if (ii >= 0) { // exact match found @@ -5306,16 +5306,16 @@ public void matchLifetimeIncome(int age, boolean agePlus, final double[] sortedZ private double getGMEquivalisedIncome(int age, int year) { - Double val = Parameters.getEquivalisedIncome(demMaleFlag, age, year); if (!Parameters.isFinite(val)) { // outside observed range, use regression model + RegressionProxy proxy = new RegressionProxy(age, year); if (demMaleFlag.toBoolean()) { - val = Math.exp(Parameters.getRegLifetimeIncome1a().getScore(this, Person.Variables.class)); + val = Math.exp(Parameters.getRegLifetimeIncome1a().getScore(proxy, RegressionProxy.Variables.class)); } else { - val = Math.exp(Parameters.getRegLifetimeIncome1b().getScore(this, Person.Variables.class)); + val = Math.exp(Parameters.getRegLifetimeIncome1b().getScore(proxy, RegressionProxy.Variables.class)); } } return val; diff --git a/src/main/java/simpaths/model/SimPathsModel.java b/src/main/java/simpaths/model/SimPathsModel.java index 3e431aae2..bedeef3a9 100644 --- a/src/main/java/simpaths/model/SimPathsModel.java +++ b/src/main/java/simpaths/model/SimPathsModel.java @@ -281,7 +281,8 @@ private record PendingParentLink(Person child, long sourceChildId, Long sourceMo private boolean enableIntertemporalOptimisations = false; private boolean lifetimeIncomeImpute = true; // request to generate new set of lifetime incomes - private Integer lifetimeIncomeEndAge = 80; + private Integer lifetimeIncomeCapAge = 80; + private Integer lifetimeIncomeEndAge = 120; private Integer lifetimeIncomeCohortSize = 500000; private boolean lifetimeIncomeWriteToCSV = true; private long lifetimeIncomeRandomSeed = 505; @@ -399,8 +400,11 @@ public void buildObjects() { saveImperfectTaxDBMatches, timeTrendStopsIn, startYear, endYear, interestRateInnov, disposableIncomeFromLabourInnov, flagSuppressChildcareCosts, flagSuppressSocialCareCosts, lifetimeIncomeImpute); if (lifetimeIncomeImpute) { - ManagerLifetimeIncomes.generateNormalisedProfiles(log, lifetimeIncomeEndAge, lifetimeIncomeCohortSize, lifetimeIncomeTemplates, + ManagerLifetimeIncomes.generateNormalisedProfiles(log, lifetimeIncomeCapAge, lifetimeIncomeEndAge, lifetimeIncomeCohortSize, lifetimeIncomeTemplates, lifetimeIncomeWriteToCSV, lifetimeIncomeRandomSeed, outputPath); + boolean projectSample = true; + if (projectSample) + ManagerLifetimeIncomes.imputeProfiles2(log, lifetimeIncomeCapAge, lifetimeIncomeRandomSeed, lifetimeIncomeTemplates, lifetimeIncomeWriteToCSV, outputPath); } if (enableIntertemporalOptimisations) { diff --git a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java index 805e5bf24..e930449f8 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java +++ b/src/main/java/simpaths/model/lifetime_incomes/AdjustmentFactors.java @@ -4,8 +4,181 @@ public class AdjustmentFactors { public static double normIncomeZero = 1.0; - public static double fixedEffect = 1.0; + /* + public static double fixedEffect = 0.0; + public static double[] whiteNoise = { + 1 , // age0 + 0.87148293 , // age1 + 0.764571713 , // age2 + 0.772685847 , // age3 + 0.784034322 , // age4 + 0.788145336 , // age5 + 0.840927347 , // age6 + 0.804404853 , // age7 + 0.746862871 , // age8 + 0.805857937 , // age9 + 0.768046173 , // age10 + 0.768877138 , // age11 + 0.787305004 , // age12 + 0.807517712 , // age13 + 0.779309551 , // age14 + 0.797063961 , // age15 + 0.774348026 , // age16 + 0.765564658 , // age17 + 0.663181832 , // age18 + 0.59206669 , // age19 + 0.586870346 , // age20 + 0.611374431 , // age21 + 0.631364727 , // age22 + 0.624448431 , // age23 + 0.689266129 , // age24 + 0.697007655 , // age25 + 0.681058745 , // age26 + 0.716029031 , // age27 + 0.758303204 , // age28 + 0.755814235 , // age29 + 0.729608534 , // age30 + 0.731067955 , // age31 + 0.761946787 , // age32 + 0.75419121 , // age33 + 0.826190512 , // age34 + 0.809296211 , // age35 + 0.781894238 , // age36 + 0.785505776 , // age37 + 0.761463929 , // age38 + 0.760232732 , // age39 + 0.738854134 , // age40 + 0.778571519 , // age41 + 0.787172986 , // age42 + 0.768135586 , // age43 + 0.778594224 , // age44 + 0.747630779 , // age45 + 0.767889635 , // age46 + 0.762757537 , // age47 + 0.743691402 , // age48 + 0.740342881 , // age49 + 0.721592208 , // age50 + 0.759871531 , // age51 + 0.763245192 , // age52 + 0.747005532 , // age53 + 0.713803749 , // age54 + 0.679820227 , // age55 + 0.698457799 , // age56 + 0.700661299 , // age57 + 0.731558535 , // age58 + 0.727728572 , // age59 + 0.657778974 , // age60 + 0.616675498 , // age61 + 0.656335435 , // age62 + 0.687579782 , // age63 + 0.630234724 , // age64 + 0.593370842 , // age65 + 0.62863954 , // age66 + 0.656988208 , // age67 + 0.672553106 , // age68 + 0.775680501 , // age69 + 0.763509579 , // age70 + 0.739352231 , // age71 + 0.74964186 , // age72 + 0.737836806 , // age73 + 0.726926872 , // age74 + 0.735899241 , // age75 + 0.764102071 , // age76 + 0.76885391 , // age77 + 0.78931846 , // age78 + 0.739869501 , // age79 + 0.721907094 , // age80 + }; + */ + /* + public static double fixedEffect = 0.5; + public static double[] whiteNoise = { + 1 , // age0 + 0.810600425 , // age1 + 0.765659634 , // age2 + 0.754058618 , // age3 + 0.783828906 , // age4 + 0.768063746 , // age5 + 0.796604212 , // age6 + 0.789854452 , // age7 + 0.716078297 , // age8 + 0.736137043 , // age9 + 0.753398336 , // age10 + 0.716854528 , // age11 + 0.746125865 , // age12 + 0.76583905 , // age13 + 0.761874565 , // age14 + 0.751396932 , // age15 + 0.750993983 , // age16 + 0.720905381 , // age17 + 0.640968838 , // age18 + 0.558559436 , // age19 + 0.531750813 , // age20 + 0.572330549 , // age21 + 0.612800824 , // age22 + 0.601559337 , // age23 + 0.641238663 , // age24 + 0.676612772 , // age25 + 0.651634131 , // age26 + 0.680195328 , // age27 + 0.733788701 , // age28 + 0.732064056 , // age29 + 0.706656305 , // age30 + 0.683901466 , // age31 + 0.725622353 , // age32 + 0.731489093 , // age33 + 0.762805296 , // age34 + 0.798268366 , // age35 + 0.758836387 , // age36 + 0.742153661 , // age37 + 0.719845452 , // age38 + 0.725223646 , // age39 + 0.72121148 , // age40 + 0.734940966 , // age41 + 0.76595968 , // age42 + 0.742528405 , // age43 + 0.733591473 , // age44 + 0.729856594 , // age45 + 0.721115214 , // age46 + 0.737938141 , // age47 + 0.716538806 , // age48 + 0.704445303 , // age49 + 0.703614956 , // age50 + 0.718490275 , // age51 + 0.737896031 , // age52 + 0.715696588 , // age53 + 0.691953588 , // age54 + 0.653860122 , // age55 + 0.65851408 , // age56 + 0.688278184 , // age57 + 0.69938592 , // age58 + 0.710389374 , // age59 + 0.640564246 , // age60 + 0.577854441 , // age61 + 0.601254583 , // age62 + 0.661035049 , // age63 + 0.634446418 , // age64 + 0.539196673 , // age65 + 0.529101491 , // age66 + 0.586106269 , // age67 + 0.641271089 , // age68 + 0.746262214 , // age69 + 0.780353352 , // age70 + 0.714603171 , // age71 + 0.691786014 , // age72 + 0.678865725 , // age73 + 0.670399108 , // age74 + 0.670262638 , // age75 + 0.707404633 , // age76 + 0.722143763 , // age77 + 0.719761022 , // age78 + 0.685978702 , // age79 + 0.664111464 , // age80 + }; + */ + + public static double fixedEffect = 1.0; public static double[] whiteNoise = { 1 , // age0 0.816551806 , // age1 @@ -89,6 +262,8 @@ public class AdjustmentFactors { 0.493819264 , // age79 0.493819264 , // age80 }; + /* + */ public static double getWhiteNoise(int age) { return whiteNoise[age]; diff --git a/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java b/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java index 2092d1884..0fd659d69 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java +++ b/src/main/java/simpaths/model/lifetime_incomes/ImputedIncome.java @@ -7,6 +7,8 @@ public class ImputedIncome { private long id; private int age; private int year; + private int female; + private int ageAtDeath; private double income; private Double observedIncome; @@ -15,6 +17,7 @@ public ImputedIncome(Person person, int age) { this.id = person.getKey().getId(); this.age = age; this.year = person.getYear() - person.getDemAge() + age; + this.female = person.getDemMaleFlag().toBoolean() ? 0 : 1; this.income = person.getImputedIncome(age); if (person.getDemAge() == age) { @@ -26,6 +29,21 @@ public ImputedIncome(Person person, int age) { this.observedIncome = null; } } + public ImputedIncome(SampleIndividual individual, int age) { + this.id = individual.getId(); + this.age = age; + this.year = individual.getYear() - individual.getAge() + age; + this.female = individual.getGender().toBoolean() ? 0 : 1; + this.ageAtDeath = individual.getAgeAtDeath(); + this.income = individual.getImputedIncome(age); + if (individual.getAge() == age) { + + this.observedIncome = individual.getIncome(); + } + else { + this.observedIncome = null; + } + } public Double getObservedIncome() { return observedIncome; @@ -66,4 +84,16 @@ public int getYear() { public void setYear(int year) { this.year = year; } + + public int getFemale() { + return female; + } + + public void setFemmale(int female) { + this.female = female; + } + + public int getAgeAtDeath() {return ageAtDeath;} + + public void setAgeAtDeath(int ageAtDeath) {this.ageAtDeath = ageAtDeath;} } diff --git a/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java b/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java index 16d5268e8..d6acfe718 100644 --- a/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java +++ b/src/main/java/simpaths/model/lifetime_incomes/ManagerLifetimeIncomes.java @@ -9,18 +9,27 @@ import java.io.*; import java.util.*; import java.util.random.RandomGenerator; -import java.util.stream.IntStream; import org.apache.logging.log4j.Logger; import simpaths.model.Person; +import simpaths.model.enums.Gender; public class ManagerLifetimeIncomes { - public static void generateNormalisedProfiles(Logger log, Integer endAge, Integer simCohortSize, List templateIndividuals, boolean writeToCSV, long seed, String outputDir) { + public static void generateNormalisedProfiles(Logger log, Integer capAge, Integer endAge, Integer simCohortSize, List templateIndividuals, boolean writeToCSV, long seed, String outputDir) { log.info("Initialising lifetime income projections"); System.out.println("Initialising lifetime income projections"); + + /* + SampleIndividual ind = new SampleIndividual(); + ind.setGender(Gender.Male); + double income = ind.getGMEquivalisedIncome(80, 2043); + income = ind.getGMEquivalisedIncome(85, 2050); + */ + + // start projecting lifetime incomes RandomGenerator generator = new Random(seed); @@ -61,27 +70,28 @@ public static void generateNormalisedProfiles(Logger log, Integer endAge, Intege templateIndividuals.add(individual); } - // project lifetimes for template donors + // project lifetimes for template donors (normalised incomes) for (int age = 1; age <= endAge; age++) { - final int currentAge = age; - List whiteNoiseEstimates = whiteNoiseEstimatesAll.stream().filter(w -> w.getAge() == currentAge).toList(); + final int currentCapAge = Math.min(age, capAge); + final int currentUncapAge = age; + List whiteNoiseEstimates = whiteNoiseEstimatesAll.stream().filter(w -> w.getAge() == currentCapAge).toList(); int noiseSampleSize = whiteNoiseEstimates.size(); - double adjFactor = AdjustmentFactors.getWhiteNoise(currentAge); + double adjFactor = AdjustmentFactors.getWhiteNoise(currentCapAge); templateIndividuals.parallelStream().forEach(individual -> { //for (TemplateIndividual individual : templateIndividuals) { WhiteNoiseEstimate noise = whiteNoiseEstimates.get(individual.getNextInt(noiseSampleSize)); double etaAdj = noise.getEtaHat() * adjFactor; - individual.projectNormIncome(currentAge, etaAdj); + individual.projectNormIncome(currentUncapAge, etaAdj); //} }); } // write template profiles to csv - boolean writeToCSV2 = true; - if (writeToCSV) { + boolean writeToCSV2 = false; + if (writeToCSV2) { List incomes = new ArrayList<>(); for (TemplateIndividual individual : templateIndividuals) { @@ -169,4 +179,90 @@ public static void imputeProfiles(Logger log, Set personSet, List templateIndividuals, boolean writeToCSV, String outputDir) { + + log.info("Imputing lifetime income profiles for sample"); + System.out.println("Imputing lifetime income profiles for sample"); + + // define year + int year = 2023; + + // load survey sample + List sampleIndividuals; + try { + String path = Parameters.getInputDirectory() + "lifetime_incomes" + File.separator + "/sample/" + year + ".csv"; + FileReader fileReader = new FileReader(path); + sampleIndividuals = new CsvToBeanBuilder(fileReader).withType(SampleIndividual.class).build().parse(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem reading initialisation start sample for analysis"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + long seedHere = seed; + for (SampleIndividual individual : sampleIndividuals) { + + seedHere += 1; + individual.setGender(individual.getMale() == 1 ? Gender.Male : Gender.Female); + individual.setGenerator(seedHere); + } + + // match templates to sample popualtion + // loop through each age in order + for (int age = 0; age <= capAge; age++) { + + // sort template individuals by norm income at current age + int ageHere = age; + templateIndividuals.sort(Comparator.comparingDouble(individual -> individual.getNormIncome(ageHere).getZValue())); + final double[] sortedZ = templateIndividuals.stream().mapToDouble(individual -> individual.getNormIncome(ageHere).getZValue()).toArray(); + boolean agePlus = (age>=capAge); + + // loop through sample + sampleIndividuals.parallelStream().forEach(individual -> { + //for (Person person : personSet) { + + individual.matchLifetimeIncome(ageHere, agePlus, sortedZ, templateIndividuals); + //} + }); + } + + // identify end age of template population + int endAge = templateIndividuals.getFirst().getNormIncomes().size() - 1; + + // write matched income profiles to csv + if (writeToCSV) { + + List incomes = new ArrayList<>(); + for (SampleIndividual individual : sampleIndividuals) { + + individual.projectAgeAtDeath(); + for (int age = 0; age <= endAge; age++) { + + ImputedIncome income = new ImputedIncome(individual, age); + incomes.add(income); + } + } + try { + String path = outputDir + File.separator + "imputed_incomes.csv"; + Writer writer = new FileWriter(path); + StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build(); + beanToCsv.write(incomes); + writer.close(); + } catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) { + // a bean had an empty required field or a value that couldn't be converted + System.err.println("Could not write CSV: " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Problem writing imputed incomes"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + } + + // finalise + log.info("Completed imputing lifetime incomes"); + } } diff --git a/src/main/java/simpaths/model/lifetime_incomes/RegressionProxy.java b/src/main/java/simpaths/model/lifetime_incomes/RegressionProxy.java new file mode 100644 index 000000000..f59dcf48a --- /dev/null +++ b/src/main/java/simpaths/model/lifetime_incomes/RegressionProxy.java @@ -0,0 +1,374 @@ +package simpaths.model.lifetime_incomes; + +import microsim.statistics.IDoubleSource; +import simpaths.data.Parameters; +import simpaths.model.enums.TimeSeriesVariable; + +public class RegressionProxy implements IDoubleSource { + + private int age; + private int year; + + public RegressionProxy() {} + public RegressionProxy(int age, int year) { + this.age = age; + this.year = year; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + + public enum Variables { + Age0, + Age1, + Age2, + Age3, + Age4, + Age5, + Age6, + Age7, + Age8, + Age9, + Age10, + Age11, + Age12, + Age13, + Age14, + Age15, + Age16, + Age17, + Age18, + Age19, + Age20, + Age21, + Age22, + Age23, + Age24, + Age25, + Age26, + Age27, + Age28, + Age29, + Age30, + Age31, + Age32, + Age33, + Age34, + Age35, + Age36, + Age37, + Age38, + Age39, + Age40, + Age41, + Age42, + Age43, + Age44, + Age45, + Age46, + Age47, + Age48, + Age49, + Age50, + Age51, + Age52, + Age53, + Age54, + Age55, + Age56, + Age57, + Age58, + Age59, + Age60, + Age61, + Age62, + Age63, + Age64, + Age65, + Age66, + Age67, + Age68, + Age69, + Age70, + Age71, + Age72, + Age73, + Age74, + Age75, + Age76, + Age77, + Age78, + Age79, + Log_GDP_per_capita, + Age80plus, + } + public double getDoubleValue(Enum variableID) { + + switch ((Variables) variableID) { + + case Age0 -> { + return (age == 0) ? 1 : 0; + } + case Age1 -> { + return (age == 1) ? 1 : 0; + } + case Age2 -> { + return (age == 2) ? 1 : 0; + } + case Age3 -> { + return (age == 3) ? 1 : 0; + } + case Age4 -> { + return (age == 4) ? 1 : 0; + } + case Age5 -> { + return (age == 5) ? 1 : 0; + } + case Age6 -> { + return (age == 6) ? 1 : 0; + } + case Age7 -> { + return (age == 7) ? 1 : 0; + } + case Age8 -> { + return (age == 8) ? 1 : 0; + } + case Age9 -> { + return (age == 9) ? 1 : 0; + } + case Age10 -> { + return (age == 10) ? 1 : 0; + } + case Age11 -> { + return (age == 11) ? 1 : 0; + } + case Age12 -> { + return (age == 12) ? 1 : 0; + } + case Age13 -> { + return (age == 13) ? 1 : 0; + } + case Age14 -> { + return (age == 14) ? 1 : 0; + } + case Age15 -> { + return (age == 15) ? 1 : 0; + } + case Age16 -> { + return (age == 16) ? 1 : 0; + } + case Age17 -> { + return (age == 17) ? 1 : 0; + } + case Age18 -> { + return (age == 18) ? 1 : 0; + } + case Age19 -> { + return (age == 19) ? 1 : 0; + } + case Age20 -> { + return (age == 20) ? 1 : 0; + } + case Age21 -> { + return (age == 21) ? 1 : 0; + } + case Age22 -> { + return (age == 22) ? 1 : 0; + } + case Age23 -> { + return (age == 23) ? 1 : 0; + } + case Age24 -> { + return (age == 24) ? 1 : 0; + } + case Age25 -> { + return (age == 25) ? 1 : 0; + } + case Age26 -> { + return (age == 26) ? 1 : 0; + } + case Age27 -> { + return (age == 27) ? 1 : 0; + } + case Age28 -> { + return (age == 28) ? 1 : 0; + } + case Age29 -> { + return (age == 29) ? 1 : 0; + } + case Age30 -> { + return (age == 30) ? 1 : 0; + } + case Age31 -> { + return (age == 31) ? 1 : 0; + } + case Age32 -> { + return (age == 32) ? 1 : 0; + } + case Age33 -> { + return (age == 33) ? 1 : 0; + } + case Age34 -> { + return (age == 34) ? 1 : 0; + } + case Age35 -> { + return (age == 35) ? 1 : 0; + } + case Age36 -> { + return (age == 36) ? 1 : 0; + } + case Age37 -> { + return (age == 37) ? 1 : 0; + } + case Age38 -> { + return (age == 38) ? 1 : 0; + } + case Age39 -> { + return (age == 39) ? 1 : 0; + } + case Age40 -> { + return (age == 40) ? 1 : 0; + } + case Age41 -> { + return (age == 41) ? 1 : 0; + } + case Age42 -> { + return (age == 42) ? 1 : 0; + } + case Age43 -> { + return (age == 43) ? 1 : 0; + } + case Age44 -> { + return (age == 44) ? 1 : 0; + } + case Age45 -> { + return (age == 45) ? 1 : 0; + } + case Age46 -> { + return (age == 46) ? 1 : 0; + } + case Age47 -> { + return (age == 47) ? 1 : 0; + } + case Age48 -> { + return (age == 48) ? 1 : 0; + } + case Age49 -> { + return (age == 49) ? 1 : 0; + } + case Age50 -> { + return (age == 50) ? 1 : 0; + } + case Age51 -> { + return (age == 51) ? 1 : 0; + } + case Age52 -> { + return (age == 52) ? 1 : 0; + } + case Age53 -> { + return (age == 53) ? 1 : 0; + } + case Age54 -> { + return (age == 54) ? 1 : 0; + } + case Age55 -> { + return (age == 55) ? 1 : 0; + } + case Age56 -> { + return (age == 56) ? 1 : 0; + } + case Age57 -> { + return (age == 57) ? 1 : 0; + } + case Age58 -> { + return (age == 58) ? 1 : 0; + } + case Age59 -> { + return (age == 59) ? 1 : 0; + } + case Age60 -> { + return (age == 60) ? 1 : 0; + } + case Age61 -> { + return (age == 61) ? 1 : 0; + } + case Age62 -> { + return (age == 62) ? 1 : 0; + } + case Age63 -> { + return (age == 63) ? 1 : 0; + } + case Age64 -> { + return (age == 64) ? 1 : 0; + } + case Age65 -> { + return (age == 65) ? 1 : 0; + } + case Age66 -> { + return (age == 66) ? 1 : 0; + } + case Age67 -> { + return (age == 67) ? 1 : 0; + } + case Age68 -> { + return (age == 68) ? 1 : 0; + } + case Age69 -> { + return (age == 69) ? 1 : 0; + } + case Age70 -> { + return (age == 70) ? 1 : 0; + } + case Age71 -> { + return (age == 71) ? 1 : 0; + } + case Age72 -> { + return (age == 72) ? 1 : 0; + } + case Age73 -> { + return (age == 73) ? 1 : 0; + } + case Age74 -> { + return (age == 74) ? 1 : 0; + } + case Age75 -> { + return (age == 75) ? 1 : 0; + } + case Age76 -> { + return (age == 76) ? 1 : 0; + } + case Age77 -> { + return (age == 77) ? 1 : 0; + } + case Age78 -> { + return (age == 78) ? 1 : 0; + } + case Age79 -> { + return (age == 79) ? 1 : 0; + } + case Log_GDP_per_capita -> { + return Math.log(Parameters.getTimeSeriesValue(year, TimeSeriesVariable.GDPperCapita)); + } + case Age80plus -> { + return (age >= 80) ? 1. : 0.; + } + default -> { + return 0.0; + } + } + } +} diff --git a/src/main/java/simpaths/model/lifetime_incomes/SampleIndividual.java b/src/main/java/simpaths/model/lifetime_incomes/SampleIndividual.java new file mode 100644 index 000000000..a5ad8b471 --- /dev/null +++ b/src/main/java/simpaths/model/lifetime_incomes/SampleIndividual.java @@ -0,0 +1,183 @@ +package simpaths.model.lifetime_incomes; + +import com.opencsv.bean.CsvBindByName; +import simpaths.data.Parameters; +import simpaths.model.enums.Gender; +import simpaths.model.enums.TimeSeriesVariable; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.random.RandomGenerator; + +public class SampleIndividual { + + @CsvBindByName private int id; + @CsvBindByName private int age; + @CsvBindByName private int male; + @CsvBindByName private double income; + @CsvBindByName private int year; + private Gender gender; + private int ageAtDeath; + TemplateIndividual ltIncomeDonor; + RandomGenerator generator; // random number generator + + public SampleIndividual() { + } + + public void setGenerator(long seed) { + generator = new Random(seed); + } + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public double getIncome() { + return income; + } + + public void setIncome(double income) { + this.income = income; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getMale() { + gender = (male ==1) ? Gender.Male : Gender.Female; + return male; + } + + public void setMale(int male) { + this.male = male; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public Gender getGender() { + return gender; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public TemplateIndividual getLtIncomeDonor() { + return ltIncomeDonor; + } + + public void setLtIncomeDonor(TemplateIndividual ltIncomeDonor) { + this.ltIncomeDonor = ltIncomeDonor; + } + + public int getAgeAtDeath() { + return ageAtDeath; + } + + public void setAgeAtDeath(int ageAtDeath) { + this.ageAtDeath = ageAtDeath; + } + + + public void matchLifetimeIncome(int ageH, boolean agePlus, final double[] sortedZ, List templateIndividuals) { + + boolean matchHere; + if (agePlus) + matchHere = (age >= ageH); + else + matchHere = (age == ageH); + if (matchHere) { + + // identify normalised income target + if (income > 0.0) { + + double gmEqDispIncomePerYear = getGMEquivalisedIncome(age, year); + double zTarget = Math.log(income / gmEqDispIncomePerYear); + int ii = Arrays.binarySearch(sortedZ, zTarget); + if (ii >= 0) { + // exact match found + setLtIncomeDonor(templateIndividuals.get(ii)); + } else { + // adopt nearest neighbour + int ins = -ii - 1; + if (ins == 0) { + // lower bound + setLtIncomeDonor(templateIndividuals.get(ins)); + } else if (ins == sortedZ.length) { + // upper bound + setLtIncomeDonor(templateIndividuals.get(ins - 1)); + } else if (zTarget - sortedZ[ins -1] < sortedZ[ins] - zTarget) { + // choose lower bound + setLtIncomeDonor(templateIndividuals.get(ins - 1)); + } else { + // choose upper bound + setLtIncomeDonor(templateIndividuals.get(ins)); + } + } + } + else { + + setLtIncomeDonor(templateIndividuals.get(0)); + } + } + } + + public double getGMEquivalisedIncome(int ageH, int yearH) { + + Double val = Parameters.getEquivalisedIncome(gender, ageH, yearH); + if (!Parameters.isFinite(val)) { + // outside observed range, use regression model + + RegressionProxy proxy = new RegressionProxy(ageH, yearH); + if (gender.toBoolean()) { + val = Math.exp(Parameters.getRegLifetimeIncome1a().getScore(proxy, RegressionProxy.Variables.class)); + } + else { + val = Math.exp(Parameters.getRegLifetimeIncome1b().getScore(proxy, RegressionProxy.Variables.class)); + } + } + return val; + } + + public double getImputedIncome(int ageH) { + + int yearH = year - age + ageH; + double gmIncome = getGMEquivalisedIncome(ageH, yearH); + if (ltIncomeDonor != null) + return Math.exp(ltIncomeDonor.getNormIncome(ageH).getZValue()) * gmIncome; + else + return gmIncome; + } + + public void projectAgeAtDeath() { + + boolean found = false; + int maxAge = ltIncomeDonor.getNormIncomes().size() - 1; + for (int ageHere = age+1; ageHere < maxAge; ageHere++) { + int yearHere = year - age + ageHere; + double mortality = Parameters.getMortalityProbability(gender, ageHere, yearHere); + if (generator.nextDouble() < mortality) { + found = true; + ageAtDeath = ageHere; + break; + } + } + if (!found) + ageAtDeath = maxAge; + } +} From 43e98699953f08f3cce216bc4f86d3e1c19b3223 Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:19:10 +0200 Subject: [PATCH 4/6] omit lifetime incomes here --- input/DatabaseCountryYear.xlsx | Bin 3338 -> 3347 bytes .../java/simpaths/model/SimPathsModel.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/input/DatabaseCountryYear.xlsx b/input/DatabaseCountryYear.xlsx index b9306fb4c5e5c2ae94d419a2a5cb62d714ffe808..d830fad73285234cc0c53aaf270e1328a3f8a935 100644 GIT binary patch delta 1628 zcmeB@nk>Z|;LXe;!ob17!SH!w#75pqMrI(rxsy>E%$U5KNvpo;zd%v`_9}siCQB8X z)T=M>bG*F9Hp|x|?B(;>ldI-PKKmH5{g>+=!&LDVQ+My0$aJ#p;>FEV`+qOBI^~yd zp5>vKl++$Mh5c#RxicSGCcN#r_hi?ZkZGDK`8$q$X)*9U_sH_bboZni=4TlTQkMuG z*Q#p_T5WgiXyjp6r|%a-&k^((1ITjRdZBEXpzwI#j%Ab`lZcD#an9}!MRz^lY zewqKg2Z7ew9@C5_HLQ(YoU83oYMcLU$&}Zk?mV7v-K1G=zT=+M6K3yb;VV=;XQPIP z_@RO&BCq*+4qf3*NG>kFw?bfXz9yesNVBYif$dpI?G?^}w}egxddoe@zqH?ychY5j z-x}tp&%)BoHaTmEDOc_>I-_r`LdXSf*AJT!SRt9rdxE&Gvs4^mbv zI;G~U8a{WjS8S){?G*(P4obWI;|=^K`^Z||y?g$zeEa#Qc_y>w9IQ;;xaf7LBWFVL z(nISO$+!Q?^e+GNap9N5OB>!ZGIjEO5q;|{5hwB4m*4HjR9S|{p4oE+6{oFBt}9Sj z;;**&<}!_WGX=Rc^*^R$3EO18spoWdt?X6(1>d3nfd$Qi)n{y_gWg>wRk3=Zx^NhSS;`UeAT$D3}+LIt6w$QtT#lL zNHm!mPV_1fYxO(Po$RT!jdAVw%+mR9Wvg>M?;YIjGk^Y)=kb>;E=)@E(G&VVZQ9NM z$0lvu`}}-H+26ZA_(9>jZsO!?uE4Ne%!D4kitL*89)Fs*mS?dGG->$hiw5P}ioA=i zz8ul}HY4{|ot@t*26w;J7x&DZn^ya^*+N)dytyrFsY8Il#a#tgK5VS7uV^i;uD>3m zG@s9Kp36h$OBbHoTnkdWJo)&RF6GuoB@=C*DgD&Ae4@qdtIJLPPk}4NUtVP0VS44y z{JtBd=Pszt(y#Ydyze!G?M!O!tGhE6{Egg}oM;&Qb=f@A5(^ckn`VQv2575AM@zGuXJ|IdrQIR`3Jef=)GI&sdp6n4tQ^6#%Z_iZor?%4aV zxs?4rlL9Bxq~^qzXzK*hQ= zJCQ4^^38|4eCHR;5<9$X^@dmVPbRx)?%2#+xKNl)CwZfQl6quexMW; zbg3=eXF;=+yO`>`_cF=7+w8L+n=bF~jt3sw1EMv3QkyzOd<^Ml63MBE;~?3 zI+>N*j2B49!b`2m9^8gtnHp|s#_Y*6xE16<^2mjN2t0y7j17~ob1Q?@ASYyA6g4v^ zOY+FXfXqRTczzT$lI#o&6*>Cl`9;}D`T5y;6}dS9-YAZaO@F;;#Z{mt6QGBHX#)uu dOrFT233ddsrzMb7FfjN|zR9D_wvP)W2mk?Pms$V- delta 1632 zcmbO%)g{Fn;LXe;!ob17!C=tjzmd0+kr_yD?qrk(GbS%*(yFg>J+b%hD@T`0Q<^yD zy>YZxSaRF!LXOiWFZ=s*t~@?t(7$}w+4YZlHFrC#y>q=+#KWwo?^&3g{PkBV)_a4U zxder+&n%MB*j|*_m)?*g{zdJ2>atBfij{{|mq{KnvFx6n5HHvH@Pdi^f!>L3GOK^E zdaeB>5uVN~&VJWDctX++wfiD}zH`;_{bCOAX6JZVQ+{J20|Uc$AVzWf<_cyfh@1Da zsMqi1YcddFeQ)K#^Yrn?Eac4HxLu;QdNq$)%g4IsPgJH)*S|bj zz2eF_u34&@stMWGGj4N5#h!m{tNXM|nJsAdMK(wC-8UrH*7X$3TCzZ8U4+OM?nDct zwo-Pt#7Ar!40l@Rhjc8@m*kTRX_j>`uswZwOM;tIrnjoQZsmmip8ucec%0j3@{xDS z{OHn|-t*GhS#Rq_d@~oDKUp5+AGO+-_RD~N;bcVf&*WvS>h*T4N%;jvA%U$Y12jXQ zF9`}yI%lROBR1j0yUXG|mzM;$`;~t${(tEL>LGbJ#d};md=kHWV=@SY^)LZnW%Dxekxq60VymythNH z3U3Zl7L_?)mY=N9Yn{BNKGSvPgSCfeZ(8OmU{f=(+r;za;-%Y;Nj~p%V^v-;D{ph# zC8w8F6W!+TnOAu!%UHB>a%<(KrRTFs?7G+7F>0USb8E(YMYAm&P5ee~A9nb&Mt*v1 z;^2ExQRK{>@9x_t{ri0N^viRG?Ij;iR!*H(@c*9@XWoL|lMhO-KbN?B;8S8x%91ps@KID1$_`|m!S+~7)F7AF=)ALb> zOD@8B{^tnqlgrfm_jD<@J}NP+Jmb1-@)JYBsCS*4 zYa;HiSbyJJh6h<;3 z&9v|nzeRrBI*u2fhxv?~JhpC@x^yqk;mo^zC$i4zEAHHrxt|e~o+8c4GHw9VQ#5*d ziUkV7ffSHcvDu%41CnE+ICbj%4jTyU{VY1|{T0tuOiq5Q!d5gf3nlH|eNqJ!B(}Lf zYK|>*2`!$zyz;b8(cfgNGA8*gn=SptInxuo!!vI``SZHC{P%(DM|)-F*tdu=NPf&c zQQ3XNo=fnQSPvKX_2#91Z}$XxTrgMqA{S!X<)gkyWv8%P=4tN8OlC7SBmVl*bLKA} zn>}7MLHT?1VFA4?vBlrd$tqQ>EbEi+e|w;pF$nGVj@}UJ!Pi@BZBn zJi9U{ip5@B_J5*3SE%A%=^4xUcq5J_O%4h;$DFR%CiW>rC1{Su3LE2X#co!HEI)mh z-AtKvR^{f@mvc7mF%lII{=TX){Hb& Date: Sat, 31 Oct 2026 18:14:54 +0100 Subject: [PATCH 5/6] Fix formatting of debt variables in wealth data Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- input/InitialPopulations/compile/07_was_wealth_data.do | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/InitialPopulations/compile/07_was_wealth_data.do b/input/InitialPopulations/compile/07_was_wealth_data.do index fb6fa8438..69fc0500a 100644 --- a/input/InitialPopulations/compile/07_was_wealth_data.do +++ b/input/InitialPopulations/compile/07_was_wealth_data.do @@ -774,7 +774,7 @@ foreach file in "${dir_was_data}/was_round_5_person_eul_oct_2020.dta" /// cc_debt cc_debt_trans cc_debt_pers hp_debt loan_debt loan_debt_exc_slc loan_debt_slc /// loan_doorstep loan_pawnbroker loan_payday informal_loan_debt highcost_loan_debt /// mail_debt unsec_debt_total unsec_debt_core highcost_debt_proxy lowcost_debt /// - op_memb pp_membu op_membu op_db op_dc pensioners/// + op_memb pp_membu op_membu op_db op_dc pensioners /// pp_core_annual pp_fallback_annual pp_contrib_annual pp_contrib_rate_income priv_total_contrib_rate_inc /// pp_contrib_source_core pp_contrib_source_fallback occ_emp_contrib_rate occ_emp_contrib_annual /// caser5 caser6 caser7 personw5 personw6 personr7 From 20d548b7000c30ddafec0f331f81b97e0c10bd4a Mon Sep 17 00:00:00 2001 From: justin-ven <43171764+justin-ven@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:37:22 +0100 Subject: [PATCH 6/6] responses to copilot comments --- input/DatabaseCountryYear.xlsx | Bin 3347 -> 3348 bytes src/main/java/simpaths/model/Person.java | 2 +- .../model/lifetime_incomes/ImputedIncome.java | 4 +--- .../ManagerLifetimeIncomes.java | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/input/DatabaseCountryYear.xlsx b/input/DatabaseCountryYear.xlsx index d830fad73285234cc0c53aaf270e1328a3f8a935..0279c60dc932889f73a91b5bb05f834d39f4275c 100644 GIT binary patch delta 474 zcmbO%HARXyz?+#xgn@&DgTZP_{6^kNMrI(rxsy>E%$U5K=^R*~g4qcou$M)>elOP{ z0}<8-u^(g)-8Efn@2cv_e(>m3zP^QfZ#8&k0=E$pG=_L+g@9P2_OBO z?9MkVTwR-dC)BCb>Wo>ys+TSTu3^9LxpJO<%N6nd%--XF{kak{k4 zZ}I(w-e-@puTJiIcxom03h6At#d|-xP7`&xll9nv)5y!^>s%iFgNud!INRGT=5t=% z=Ny^xv+T$<6k<<+kAjipPRYpL~kj(hkI1H*xYcSD-zMnHU&&fp#Lm zELH}FiX8polFFRaV!evooB(e|CJ_dBxKDQEk!F;d9LpmIHolHWn#qu3vJZ!hHk!g% e|Ff>sw1IZ00_~7MSO8@CO@73q&9bWh>g6JjLblKb0?!Tm@#=d(>bs}1+x=GU@wb${YI`s z1`;d}Vn4`wmCfFg6zF-$v7m5kb8<%gZHLKM{;Yg)Tl%fSl)mq>GBWz{%lzj(2(;Gr zm}WGoVQuWG~)-XSP9v1s- z(&jfC5_Yqm{^$1C@+V_}H#WwHD%4b#@U5!;V7=!ieT9 zfcW(smmSFSlli&Lz_bUqHJEPWw&4Vd$AV3te45)b3dCDCaq=}+pgoJ37#Mhgb|Sz` zRtAQO9R1>w%AC|