Data set up

library(haven)
library(readstata13)
library(sjlabelled)
library(dplyr)
library(readxl)
library(stringr)
library(table1)
library(ggplot2)
library(ggpubr)
library(lme4)
library(nlme)
library(survey)
library(Hmisc)
library(tidyr)
library(metafor)
library(effects)
library(ppcor)
library(tidyverse)
library(igraph)
library(ggraph)
library(kableExtra)
library(reshape2)
library(abind)

# load the function metareg.timeXgroup() which outputs the group by time interaction term from a meta-regression
source(paste0(path,"Project A. Targets for intervention in life satisfaction/R outputs/UndSoc_functions.R"))

# load cleaned long-form dataset "usoc", as well as summary datasets "osm.list" on households and "pidp.list" on unique participants
load(paste0(path, "R code/usoc_cleaned.RData"))

Rdata contains:
- dataframe usoc 45,867 observations of 100 variables, cleaned in other script. - dataframe pidp.list characteristics of 16,287 individuals in dataset e.g. ethnicity, age joined study, number of waves participated
- dataframe osm.list characteristics of 9,541 households in dataset

# remove the very few 16 year olds from the dataset (n=15): surveyed in error
usoc <- usoc[usoc$dvage!="16",]
usoc$dvage <- droplevels(usoc$dvage)

# remove any data collected in the year 2025 (only 18 participants)
usoc <- usoc[usoc$intdaty_dv!="2025", ]
usoc$intdaty_dv <- droplevels(usoc$intdaty_dv)

# remove rows where ID is missing (all variables are NA for these two rows)
usoc <- usoc[!is.na(usoc$pidp),]

Most data cleaning is done in the “Cleaning UndSoc Youth data.Rmd” script. But below, I create some additional categorical variables.

usoc$time.window <- factor(cut(usoc$int.year.sched, breaks=c(-Inf,2013, 2018, Inf), labels=c(1, 2, 3)))
# note you're using the scheduled-interview year variable, not the actual interview year variable. This was to ensure participants have just one observation per calender year. The actual and scheduled dates only max 4 months off.

usoc$age.3groups <- factor(factor(usoc$dvage, levels=c(10,11,12,13,14,15),
                         labels=c("10-11", "10-11", "12-13", "12-13", "14-15", "14-15")))

# change order of levels from most to least prevalent or order asked
usoc$ethnicity <- factor(usoc$ethnicity, levels=c("British or Irish" ,"Any other white background","South or East Asian",  "African or Caribbean", "Mixed background" , "Other"))


# create income groups: tertiles
for (x in unique(usoc$int.year.sched)){
usoc$income.groups[usoc$int.year.sched==x] <- 
  cut(usoc$equivinc[usoc$int.year.sched==x], 
                    breaks=c(-Inf,
                              quantile(usoc$equivinc[usoc$int.year.sched==x], 
                                       probs=c(.33, .66)), 
                             Inf))                   
}

usoc$income.groups <- factor(usoc$income.groups, levels=c(1,2,3), 
                               labels=c("low.inc", "mid.inc", "high.inc"))

usoc$income.groups <- relevel(usoc$income.groups, "high.inc")

# £1597.422,  £2379.582 are the breakpoints (i.e. anything greater than £2,379.58 per month per person is 'high income')

# create a version of income where its capped at £8000 per person per month to avoid influence of extreme outliers. for figures only, not used in main analysis
usoc$equivinc.capped <- ifelse(usoc$equivinc > 8000 | usoc$equivinc < 0, NA, usoc$equivinc)

Set colours for satisfaction domains, for plots:

cols6 <- c("lightgrey","#FC8D62FF", "#8DA0CBFF", "#66C2A5FF","#A6D854FF",  "#E78AC3FF" )
            # life.    family.      friends.       school     schoolwork  appearance, 

cols5 <- c(           "#FC8D62FF", "#8DA0CBFF", "#66C2A5FF","#A6D854FF",  "#E78AC3FF")
                       # family.      friends.       school     schoolwork  appearance

cols18 <- c("#FC8D62FF","#FC8D62FF", "#FC8D62FF",  "#8DA0CBFF", "#8DA0CBFF", "#8DA0CBFF", "#66C2A5FF","#66C2A5FF","#66C2A5FF","#A6D854FF", "#A6D854FF", "#A6D854FF",  "#E78AC3FF", "#E78AC3FF", "#E78AC3FF","lightgrey", "lightgrey", "lightgrey" )

Variable naming

Some variable names were changed from their original names.

Life satisfaction variables
- h.schoolwork <- yphsw
- h.appear <- yphap
- h.fam <- yphfm
- h.friends <- yphfr
- h.school <- yphsc
- h.life <- yphlf. The response numbering of these variables changed from 1 to 7 to -3 to +3. Also centered versions of these variables also exist with the suffic “.c” e.g. “h.schoolwork.c”

Socio-demographics hsdincome <- fihhmnnet1_dv

Descriptives

# Table of sociodemographics by wave 

table1(~age.group + sex_dv + ethnicity + hsdincome + equivinc  + country + h.life.c + doby_dv.n | wave.y, data=usoc, overall=F) 
2009-10
(N=4899)
2010-11
(N=5015)
2011-12
(N=4427)
2012-13
(N=4044)
2013-14
(N=3655)
2014-15
(N=3427)
2015-16
(N=3602)
2016-17
(N=3227)
2017-18
(N=2729)
2018-19
(N=2443)
2019-20
(N=2049)
2020-21
(N=1626)
2021-22
(N=1719)
2022-23
(N=1552)
2023-24
(N=1400)
age.group
10-12 2427 (49.5%) 2485 (49.6%) 2185 (49.4%) 1945 (48.1%) 1781 (48.7%) 1599 (46.7%) 1811 (50.3%) 1589 (49.2%) 1391 (51.0%) 1197 (49.0%) 1005 (49.0%) 800 (49.2%) 823 (47.9%) 758 (48.8%) 694 (49.6%)
13-15 2472 (50.5%) 2530 (50.4%) 2242 (50.6%) 2099 (51.9%) 1874 (51.3%) 1828 (53.3%) 1791 (49.7%) 1638 (50.8%) 1338 (49.0%) 1246 (51.0%) 1044 (51.0%) 826 (50.8%) 896 (52.1%) 794 (51.2%) 706 (50.4%)
sex_dv
inconsistent 2 (0.0%) 2 (0.0%) 0 (0%) 1 (0.0%) 2 (0.1%) 1 (0.0%) 1 (0.0%) 0 (0%) 2 (0.1%) 0 (0%) 1 (0.0%) 0 (0%) 1 (0.1%) 0 (0%) 0 (0%)
Male 2435 (49.7%) 2529 (50.4%) 2221 (50.2%) 2040 (50.4%) 1855 (50.8%) 1736 (50.7%) 1787 (49.6%) 1598 (49.5%) 1356 (49.7%) 1196 (49.0%) 1012 (49.4%) 813 (50.0%) 812 (47.2%) 717 (46.2%) 672 (48.0%)
Female 2462 (50.3%) 2484 (49.5%) 2206 (49.8%) 2003 (49.5%) 1798 (49.2%) 1690 (49.3%) 1814 (50.4%) 1629 (50.5%) 1371 (50.2%) 1247 (51.0%) 1036 (50.6%) 813 (50.0%) 906 (52.7%) 835 (53.8%) 728 (52.0%)
ethnicity
British or Irish 3463 (70.7%) 3873 (77.2%) 3402 (76.8%) 3071 (75.9%) 2759 (75.5%) 2577 (75.2%) 2439 (67.7%) 2194 (68.0%) 1913 (70.1%) 1651 (67.6%) 1483 (72.4%) 1145 (70.4%) 1208 (70.3%) 1100 (70.9%) 949 (67.8%)
Any other white background 81 (1.7%) 73 (1.5%) 62 (1.4%) 59 (1.5%) 55 (1.5%) 51 (1.5%) 62 (1.7%) 65 (2.0%) 52 (1.9%) 54 (2.2%) 51 (2.5%) 34 (2.1%) 42 (2.4%) 51 (3.3%) 49 (3.5%)
South or East Asian 637 (13.0%) 499 (10.0%) 469 (10.6%) 458 (11.3%) 425 (11.6%) 417 (12.2%) 591 (16.4%) 527 (16.3%) 410 (15.0%) 408 (16.7%) 272 (13.3%) 237 (14.6%) 248 (14.4%) 205 (13.2%) 215 (15.4%)
African or Caribbean 327 (6.7%) 229 (4.6%) 205 (4.6%) 180 (4.5%) 170 (4.7%) 153 (4.5%) 216 (6.0%) 177 (5.5%) 137 (5.0%) 109 (4.5%) 63 (3.1%) 55 (3.4%) 57 (3.3%) 54 (3.5%) 29 (2.1%)
Mixed background 244 (5.0%) 197 (3.9%) 180 (4.1%) 169 (4.2%) 167 (4.6%) 154 (4.5%) 211 (5.9%) 177 (5.5%) 164 (6.0%) 145 (5.9%) 133 (6.5%) 125 (7.7%) 123 (7.2%) 104 (6.7%) 92 (6.6%)
Other 146 (3.0%) 129 (2.6%) 107 (2.4%) 99 (2.4%) 78 (2.1%) 66 (1.9%) 78 (2.2%) 78 (2.4%) 47 (1.7%) 64 (2.6%) 38 (1.9%) 28 (1.7%) 35 (2.0%) 29 (1.9%) 42 (3.0%)
Missing 1 (0.0%) 15 (0.3%) 2 (0.0%) 8 (0.2%) 1 (0.0%) 9 (0.3%) 5 (0.1%) 9 (0.3%) 6 (0.2%) 12 (0.5%) 9 (0.4%) 2 (0.1%) 6 (0.3%) 9 (0.6%) 24 (1.7%)
hsdincome
Mean (SD) 2900 (1960) 3110 (2200) 3300 (2290) 3350 (2250) 3390 (2140) 3660 (4200) 3780 (12300) 3870 (3550) 3780 (2500) 3920 (2460) 4070 (2320) 4290 (2890) 4220 (2750) 4550 (3180) 5010 (3150)
Median [Min, Max] 2480 [-777, 27000] 2700 [-1630, 47300] 2900 [-5390, 45300] 2940 [-991, 44700] 3020 [0, 51300] 3110 [-9.00, 198000] 3130 [-3190, 724000] 3240 [0, 58000] 3340 [-452, 59200] 3470 [0, 29800] 3640 [-451, 23700] 3830 [91.7, 42300] 3750 [0, 43500] 3980 [-510, 45000] 4490 [-419, 46300]
equivinc
Mean (SD) 1240 (820) 1330 (900) 1400 (971) 1420 (977) 1430 (888) 1540 (1740) 1560 (3680) 1620 (1440) 1600 (1080) 1630 (1010) 1720 (956) 1830 (1200) 1790 (1200) 1940 (1350) 2140 (1260)
Median [Min, Max] 1070 [-565, 12500] 1150 [-494, 20600] 1230 [-2240, 19700] 1240 [-431, 27900] 1260 [0, 20500] 1310 [-1430, 79200] 1300 [-1770, 213000] 1370 [-938, 24400] 1410 [-181, 28200] 1430 [-209, 12900] 1520 [-399, 8690] 1650 [70.6, 16900] 1580 [-2340, 21800] 1700 [-182, 17300] 1950 [-343, 16500]
country
England 4145 (84.6%) 3792 (75.6%) 3408 (77.0%) 3131 (77.4%) 2922 (79.9%) 2694 (78.6%) 2971 (82.5%) 2618 (81.1%) 2233 (81.8%) 2032 (83.2%) 1673 (81.6%) 1369 (84.2%) 1411 (82.1%) 1284 (82.7%) 1177 (84.1%)
Wales 221 (4.5%) 353 (7.0%) 303 (6.8%) 271 (6.7%) 186 (5.1%) 174 (5.1%) 139 (3.9%) 144 (4.5%) 128 (4.7%) 112 (4.6%) 96 (4.7%) 78 (4.8%) 84 (4.9%) 55 (3.5%) 59 (4.2%)
Scotland 321 (6.6%) 448 (8.9%) 381 (8.6%) 357 (8.8%) 284 (7.8%) 292 (8.5%) 285 (7.9%) 267 (8.3%) 219 (8.0%) 186 (7.6%) 156 (7.6%) 117 (7.2%) 139 (8.1%) 122 (7.9%) 129 (9.2%)
Northern Ireland 212 (4.3%) 414 (8.3%) 334 (7.5%) 284 (7.0%) 260 (7.1%) 265 (7.7%) 205 (5.7%) 198 (6.1%) 148 (5.4%) 109 (4.5%) 123 (6.0%) 62 (3.8%) 85 (4.9%) 91 (5.9%) 35 (2.5%)
Missing 0 (0%) 8 (0.2%) 1 (0.0%) 1 (0.0%) 3 (0.1%) 2 (0.1%) 2 (0.1%) 0 (0%) 1 (0.0%) 4 (0.2%) 1 (0.0%) 0 (0%) 0 (0%) 0 (0%) 0 (0%)
h.life.c
Mean (SD) 1.90 (1.18) 1.94 (1.11) 1.94 (1.13) 1.91 (1.13) 1.87 (1.19) 1.86 (1.19) 1.84 (1.23) 1.80 (1.21) 1.76 (1.21) 1.71 (1.26) 1.62 (1.27) 1.61 (1.28) 1.57 (1.31) 1.54 (1.27) 1.56 (1.25)
Median [Min, Max] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00]
Missing 40 (0.8%) 44 (0.9%) 46 (1.0%) 29 (0.7%) 63 (1.7%) 51 (1.5%) 37 (1.0%) 27 (0.8%) 25 (0.9%) 16 (0.7%) 24 (1.2%) 12 (0.7%) 22 (1.3%) 15 (1.0%) 15 (1.1%)
doby_dv.n
Mean (SD) 2000 (1.84) 2000 (1.81) 2000 (1.83) 2000 (1.84) 2000 (1.82) 2000 (1.86) 2000 (1.83) 2000 (1.87) 2000 (1.82) 2010 (1.81) 2010 (1.80) 2010 (1.80) 2010 (1.86) 2010 (1.86) 2010 (1.82)
Median [Min, Max] 2000 [1990, 2000] 2000 [1990, 2000] 2000 [1990, 2000] 2000 [2000, 2000] 2000 [2000, 2000] 2000 [2000, 2010] 2000 [2000, 2010] 2000 [2000, 2010] 2010 [2000, 2010] 2010 [2000, 2010] 2010 [2000, 2010] 2010 [2000, 2010] 2010 [2010, 2010] 2010 [2010, 2010] 2010 [2010, 2010]
Missing 3 (0.1%) 0 (0%) 1 (0.0%) 1 (0.0%) 0 (0%) 0 (0%) 1 (0.0%) 0 (0%) 0 (0%) 2 (0.1%) 1 (0.0%) 0 (0%) 0 (0%) 0 (0%) 0 (0%)
# ... and by calendar year
table1(~age.group + sex_dv + ethnicity + hsdincome + equivinc  + country + h.life.c + doby_dv.n | int.year.sched, data=usoc, overall=F) 
2009
(N=2578)
2010
(N=5361)
2011
(N=4657)
2012
(N=4157)
2013
(N=3888)
2014
(N=3612)
2015
(N=3225)
2016
(N=3418)
2017
(N=3024)
2018
(N=2490)
2019
(N=2377)
2020
(N=1662)
2021
(N=1683)
2022
(N=1577)
2023
(N=1349)
2024
(N=756)
age.group
10-12 1298 (50.3%) 2626 (49.0%) 2312 (49.6%) 1994 (48.0%) 1909 (49.1%) 1710 (47.3%) 1598 (49.6%) 1678 (49.1%) 1534 (50.7%) 1231 (49.4%) 1162 (48.9%) 818 (49.2%) 820 (48.7%) 776 (49.2%) 646 (47.9%) 378 (50.0%)
13-15 1280 (49.7%) 2735 (51.0%) 2345 (50.4%) 2163 (52.0%) 1979 (50.9%) 1902 (52.7%) 1627 (50.4%) 1740 (50.9%) 1490 (49.3%) 1259 (50.6%) 1215 (51.1%) 844 (50.8%) 863 (51.3%) 801 (50.8%) 703 (52.1%) 378 (50.0%)
sex_dv
inconsistent 2 (0.1%) 2 (0.0%) 0 (0%) 1 (0.0%) 1 (0.0%) 2 (0.1%) 1 (0.0%) 0 (0%) 1 (0.0%) 1 (0.0%) 1 (0.0%) 0 (0%) 1 (0.1%) 0 (0%) 0 (0%) 0 (0%)
Male 1295 (50.2%) 2707 (50.5%) 2344 (50.3%) 2067 (49.7%) 1967 (50.6%) 1844 (51.1%) 1609 (49.9%) 1687 (49.4%) 1502 (49.7%) 1226 (49.2%) 1163 (48.9%) 830 (49.9%) 804 (47.8%) 748 (47.4%) 625 (46.3%) 361 (47.8%)
Female 1281 (49.7%) 2652 (49.5%) 2313 (49.7%) 2089 (50.3%) 1920 (49.4%) 1766 (48.9%) 1615 (50.1%) 1731 (50.6%) 1521 (50.3%) 1263 (50.7%) 1213 (51.0%) 832 (50.1%) 878 (52.2%) 829 (52.6%) 724 (53.7%) 395 (52.2%)
ethnicity
British or Irish 1876 (72.8%) 4072 (76.0%) 3580 (76.9%) 3172 (76.3%) 2921 (75.1%) 2725 (75.4%) 2407 (74.6%) 2300 (67.3%) 2037 (67.4%) 1698 (68.2%) 1605 (67.5%) 1172 (70.5%) 1208 (71.8%) 1088 (69.0%) 922 (68.3%) 444 (58.7%)
Any other white background 45 (1.7%) 71 (1.3%) 69 (1.5%) 60 (1.4%) 56 (1.4%) 53 (1.5%) 42 (1.3%) 64 (1.9%) 65 (2.1%) 50 (2.0%) 60 (2.5%) 39 (2.3%) 39 (2.3%) 50 (3.2%) 44 (3.3%) 34 (4.5%)
South or East Asian 310 (12.0%) 560 (10.4%) 472 (10.1%) 449 (10.8%) 463 (11.9%) 413 (11.4%) 414 (12.8%) 573 (16.8%) 507 (16.8%) 394 (15.8%) 389 (16.4%) 243 (14.6%) 240 (14.3%) 230 (14.6%) 204 (15.1%) 157 (20.8%)
African or Caribbean 157 (6.1%) 274 (5.1%) 221 (4.7%) 194 (4.7%) 180 (4.6%) 176 (4.9%) 136 (4.2%) 194 (5.7%) 170 (5.6%) 125 (5.0%) 110 (4.6%) 53 (3.2%) 50 (3.0%) 59 (3.7%) 41 (3.0%) 21 (2.8%)
Mixed background 115 (4.5%) 230 (4.3%) 195 (4.2%) 171 (4.1%) 178 (4.6%) 161 (4.5%) 165 (5.1%) 203 (5.9%) 167 (5.5%) 166 (6.7%) 146 (6.1%) 117 (7.0%) 115 (6.8%) 110 (7.0%) 89 (6.6%) 57 (7.5%)
Other 74 (2.9%) 142 (2.6%) 115 (2.5%) 106 (2.6%) 87 (2.2%) 77 (2.1%) 56 (1.7%) 76 (2.2%) 70 (2.3%) 51 (2.0%) 52 (2.2%) 34 (2.0%) 26 (1.5%) 35 (2.2%) 30 (2.2%) 33 (4.4%)
Missing 1 (0.0%) 12 (0.2%) 5 (0.1%) 5 (0.1%) 3 (0.1%) 7 (0.2%) 5 (0.2%) 8 (0.2%) 8 (0.3%) 6 (0.2%) 15 (0.6%) 4 (0.2%) 5 (0.3%) 5 (0.3%) 19 (1.4%) 10 (1.3%)
hsdincome
Mean (SD) 2870 (1910) 3000 (2010) 3210 (2180) 3340 (2380) 3360 (2060) 3630 (4220) 3600 (2150) 3860 (12600) 3920 (3640) 3870 (2650) 3950 (2270) 4230 (2790) 4230 (2840) 4310 (2640) 4740 (3140) 5260 (3620)
Median [Min, Max] 2450 [-777, 22500] 2620 [-1630, 34500] 2790 [-5390, 47300] 2950 [-2510, 45300] 2990 [-991, 44700] 3080 [-9.00, 198000] 3190 [-3190, 27200] 3150 [0, 724000] 3290 [0, 58000] 3390 [-452, 59200] 3540 [-451, 24700] 3790 [259, 42300] 3730 [0, 43500] 3900 [-510, 29300] 4230 [-419, 45000] 4560 [-118, 46300]
equivinc
Mean (SD) 1230 (815) 1290 (809) 1370 (946) 1420 (1020) 1420 (849) 1530 (1750) 1520 (908) 1590 (3780) 1640 (1470) 1630 (1130) 1650 (954) 1800 (1170) 1800 (1200) 1840 (1150) 2020 (1290) 2220 (1430)
Median [Min, Max] 1060 [-296, 12500] 1120 [-565, 12900] 1190 [-2240, 20600] 1240 [-1090, 27900] 1250 [-431, 19400] 1300 [-1430, 79200] 1330 [-1770, 12700] 1320 [-536, 213000] 1390 [-938, 24400] 1420 [-209, 28200] 1450 [-399, 11700] 1620 [123, 16900] 1590 [0, 21800] 1650 [-2340, 13900] 1830 [-343, 17300] 2000 [-35.6, 16500]
country
England 2086 (80.9%) 4091 (76.3%) 3597 (77.2%) 3240 (77.9%) 3102 (79.8%) 2864 (79.3%) 2604 (80.7%) 2795 (81.8%) 2525 (83.5%) 2062 (82.8%) 1964 (82.6%) 1409 (84.8%) 1379 (81.9%) 1313 (83.3%) 1135 (84.1%) 694 (91.8%)
Wales 102 (4.0%) 372 (6.9%) 323 (6.9%) 285 (6.9%) 218 (5.6%) 178 (4.9%) 144 (4.5%) 147 (4.3%) 125 (4.1%) 115 (4.6%) 110 (4.6%) 76 (4.6%) 82 (4.9%) 53 (3.4%) 57 (4.2%) 16 (2.1%)
Scotland 178 (6.9%) 478 (8.9%) 400 (8.6%) 347 (8.3%) 306 (7.9%) 302 (8.4%) 272 (8.4%) 276 (8.1%) 226 (7.5%) 201 (8.1%) 179 (7.5%) 115 (6.9%) 136 (8.1%) 119 (7.5%) 122 (9.0%) 46 (6.1%)
Northern Ireland 212 (8.2%) 414 (7.7%) 334 (7.2%) 284 (6.8%) 260 (6.7%) 265 (7.3%) 204 (6.3%) 199 (5.8%) 148 (4.9%) 109 (4.4%) 121 (5.1%) 62 (3.7%) 86 (5.1%) 92 (5.8%) 35 (2.6%) 0 (0%)
Missing 0 (0%) 6 (0.1%) 3 (0.1%) 1 (0.0%) 2 (0.1%) 3 (0.1%) 1 (0.0%) 1 (0.0%) 0 (0%) 3 (0.1%) 3 (0.1%) 0 (0%) 0 (0%) 0 (0%) 0 (0%) 0 (0%)
h.life.c
Mean (SD) 1.89 (1.20) 1.93 (1.14) 1.94 (1.12) 1.92 (1.11) 1.90 (1.18) 1.86 (1.18) 1.84 (1.22) 1.82 (1.22) 1.81 (1.19) 1.70 (1.28) 1.67 (1.26) 1.61 (1.26) 1.57 (1.30) 1.54 (1.30) 1.57 (1.25) 1.55 (1.27)
Median [Min, Max] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00] 2.00 [-3.00, 3.00]
Missing 11 (0.4%) 55 (1.0%) 46 (1.0%) 32 (0.8%) 54 (1.4%) 60 (1.7%) 29 (0.9%) 33 (1.0%) 27 (0.9%) 24 (1.0%) 25 (1.1%) 11 (0.7%) 19 (1.1%) 22 (1.4%) 6 (0.4%) 12 (1.6%)
doby_dv.n
Mean (SD) 2000 (1.77) 2000 (1.75) 2000 (1.76) 2000 (1.76) 2000 (1.76) 2000 (1.77) 2000 (1.78) 2000 (1.77) 2000 (1.76) 2010 (1.76) 2010 (1.75) 2010 (1.74) 2010 (1.76) 2010 (1.77) 2010 (1.78) 2010 (1.80)
Median [Min, Max] 2000 [1990, 2000] 2000 [1990, 2000] 2000 [1990, 2000] 2000 [2000, 2000] 2000 [2000, 2000] 2000 [2000, 2010] 2000 [2000, 2010] 2000 [2000, 2010] 2000 [2000, 2010] 2010 [2000, 2010] 2010 [2000, 2010] 2010 [2000, 2010] 2010 [2010, 2010] 2010 [2010, 2010] 2010 [2010, 2010] 2010 [2010, 2010]
Missing 2 (0.1%) 1 (0.0%) 1 (0.0%) 1 (0.0%) 0 (0%) 0 (0%) 0 (0%) 1 (0.0%) 0 (0%) 0 (0%) 3 (0.1%) 0 (0%) 0 (0%) 0 (0%) 0 (0%) 0 (0%)
# visualise number of *unique individuals* per calendar year *with data* on all satisfaction items
nbyyear <- usoc[!is.na(usoc$h.life.c) & !is.na(usoc$h.school.c) & !is.na(usoc$h.schoolwork.c) & !is.na(usoc$h.fam.c) & !is.na(usoc$h.friends.c) & !is.na(usoc$h.appear.c),] %>%
  group_by(int.year.sched) %>%
  distinct(pidp, .keep_all = T) %>%
  summarise(n=n()) %>%
  ungroup()


ggplot(data=nbyyear, aes(x=int.year.sched, y=n))+
  geom_col()+
  labs(title = "Total N per calendar year")+
  theme(axis.text.x = element_text(angle=30))

# also check N per year with SDQ data
nbyyear <- usoc[!is.na(usoc$ypsdqtd_dv),] %>%
  group_by(int.year.sched) %>%
  distinct(pidp, .keep_all = T) %>%
  summarise(n=n()) %>%
  ungroup()

ggplot(data=nbyyear, aes(x=int.year.sched, y=n))+
  geom_col()+
  labs(title = "N per calendar year with SDQ")+
  theme(axis.text.x = element_text(angle=30))

Note extent of N dropping over time. Sampling weights used to account for non-randomly distributed attrition.

# tabulate unchanging features of unique participants
table1(~sex +  dob + country + ethnic + age_atfirst + num.rows, data=pidp.list )
Overall
(N=16287)
sex
inconsistent 6 (0.0%)
male 8153 (50.1%)
female 8128 (49.9%)
dob
Mean (SD) 2000 (5.18)
Median [Min, Max] 2000 [1990, 2010]
Missing 5 (0.0%)
country
England 13089 (80.4%)
Wales 907 (5.6%)
Scotland 1291 (7.9%)
Northern Ireland 992 (6.1%)
Missing 8 (0.0%)
ethnic
British or Irish 11341 (69.6%)
Any other white background 336 (2.1%)
South or East Asian 859 (5.3%)
African or Caribbean 2273 (14.0%)
Mixed background 952 (5.8%)
Other 432 (2.7%)
Missing 94 (0.6%)
age_atfirst
Mean (SD) 12.5 (1.76)
Median [Min, Max] 13.0 [10.0, 16.0]
Missing 2 (0.0%)
num.rows
Mean (SD) 2.82 (1.67)
Median [Min, Max] 2.00 [1.00, 7.00]
# something strange happenining with the dob variable-- the min year of birth should be 1993, not 1990.
min(pidp.list$dob, na.rm=T)
## [1] 1993
# calculate number and proportion of :

## males and females
usoc %>%
  distinct(pidp, .keep_all = T) %>%
  group_by(sex_dv) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)%>%
  ungroup()
## # A tibble: 3 × 4
##   sex_dv           n tot.n     prop
##   <fct>        <int> <int>    <dbl>
## 1 inconsistent     6 16280 0.000369
## 2 Male          8150 16280 0.501   
## 3 Female        8124 16280 0.499
## from England, Scotland, Wales & NI
usoc %>%
  distinct(pidp, .keep_all = T) %>%
  group_by(country) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)%>%
  ungroup()
## # A tibble: 5 × 4
##   country              n tot.n     prop
##   <fct>            <int> <int>    <dbl>
## 1 England          13082 16280 0.804   
## 2 Wales              907 16280 0.0557  
## 3 Scotland          1291 16280 0.0793  
## 4 Northern Ireland   992 16280 0.0609  
## 5 <NA>                 8 16280 0.000491
usoc$England <- ifelse(usoc$country=="England", "England", "RUK")

## num unique subjects at each wave in each country -- hang on, can participants be repersented across the different waves here?
tmp <- usoc %>%
  distinct(pidp, .keep_all = T) %>%
  group_by(England, wave.y) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)%>%
  ungroup()
tmp$group <- c(rep(1, 15),rep(2, 15), c(3,3,3,3))
mean(tmp$n[tmp$group==1]) # 872 per wave from Eng, on ave
## [1] 872.1333
mean(tmp$n[tmp$group==2]) # 213 per wave from RUK, on ave
## [1] 212.6667
# observe number of participants from *same* origin household
osm.list <- usoc %>%
  distinct(pidp, .keep_all = T) %>%
  group_by(osm_hh) %>%
  summarise(n=n()) %>%
  ungroup()

Explore specific variables

Time

We use the scheduled (sampling) month/year as the measure of calender year. This is to avoid including the same individual twice in the one year (which happened when actual year of interview would happen e.g. Jan 2017 & Dec 2017; >100 participants had observations ~11 months apart, who would be doubly represented)

E.g. if a hsd is scheduled to be interviewed Nov 2009 (and Nov every year) but they are 2 months late completing their interview (actually doing it in Jan 2010), their next interview will come around again Nov 2010, and they may be counted twice if we used actual year of interview.

# make sure each pidp has only been included once per year
df <- data.frame(year = sort(unique(usoc$int.year.sched)), 
                 num.dbls = NA, 
                 pidps = NA)

dup.pidps <- c()
for (y in sort(unique(usoc$int.year.sched)) )
  if (sum(table(usoc$pidp[usoc$int.year.sched==y])>1)>0) {
    tmp <- usoc[usoc$int.year.sched==y,]
    df$num.dbls[df$year==y] <- length(tmp$pidp[duplicated(tmp$pidp)])
    df$pidps[df$year==y] <- toString(tmp$pidp[duplicated(tmp$pidp)])
    
    dup.pidps <- unique(c(dup.pidps, tmp$pidp[duplicated(tmp$pidp)]))
  }

# look at full data for these participnats (not all from NI)
tmp <- usoc[is.element(usoc$pidp,dup.pidps),]

Time of youth survey “Scheduled interview date” may not always be the same date the youth (aged 10-15) filled in the survey. Its the date the adult respondent in the hsd was scheduled to be interviewed.

The actual interview date may not even be the youth survey completion date… intdatm_dv: “this is the date the personal interview of adult respondents took place. If all interviews in the household took place on the same day, all non-respondents (and youth respondents for whom no interview date is recorded) received the other household members’ interview date.” For interviews that are held in-person, the YP would put it in an envelope hand the survey back on the same day they were there interviewing. In those cases, the HSD interview date is an accurate reflection of youth survey completion. For remote interviews the YP would post the survey back to the Usoc team, which could be delayed by several weeks.

“For each sample month (e.g. a_month), interviews (e.g. intdatm_dv) take place over 3-4 months, but the majority of interviews take place in the calendar month coinciding with the sample month.”

Weights

Only cross-sectional weights available in each youth survey, with different “zz” parts, which reflects the timeline of your research:
- a_ythscus_xw # us = starting at wave 1 (2009-10)
- b_ythscub_xw # ub = starting between wave 2 and 5
…
- m_ythscui_xw # ui = starting at wave 6 onwards (2014-15)

Note change from us to ub to ui

Here are the original weights variables from imported data: “a_ythscus_xw” [US] “b_ythscub_xw” [UB] “c_ythscub_xw” [UB] “d_ythscub_xw” [UB] “e_ythscub_xw” [UB] “f_ythscui_xw” [UI] “g_ythscui_xw” [UI] “h_ythscui_xw” [UI] “i_ythscui_xw” [UI] “j_ythscui_xw” [UI] “k_ythscui_xw” [UI] “l_ythscui_xw” [UI] “m_ythscui_xw” [UI] “n_ythscg2_xw” [G2]

## EXPLORE WEIGHTS
# check number of 0 weights per year
table(usoc$int.year.sched, usoc$x_wgt==0)
##       
##        FALSE TRUE
##   2009  2568   10
##   2010  5340   21
##   2011  4635   22
##   2012  4145   12
##   2013  3871   17
##   2014  3241  371
##   2015  3079  146
##   2016  3399   19
##   2017  3012   12
##   2018  2476   14
##   2019  2364   13
##   2020  1655    7
##   2021  1678    5
##   2022  1573    4
##   2023  1349    0
##   2024   754    2
# check mean and SD of weights in first wave
summary(usoc$x_wgt[usoc$wave=="a"])
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.6003  1.1013  0.9955  1.3256  3.4091
sd(usoc$x_wgt[usoc$wave=="a"])
## [1] 0.4517437
# weights have a mean of 0 (or close) and a SD of 0.45 

summary(usoc$x_wgt[usoc$wave=="c"])
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.5694  1.0333  0.9959  1.3502  2.4254
sd(usoc$x_wgt[usoc$wave=="c"])
## [1] 0.4862356
# sum of the weights in wave A should approximate N...
sum(usoc$x_wgt[usoc$wave=="a"]) # 4877
## [1] 4877
# number of unique participants in wave A?
length(unique(usoc$pidp[usoc$wave=="a"]))  # 4899
## [1] 4899
# shouldnt they add up to the same total N? maybe 22 individuals were missing basic info to "weight" on.
# maybe 22 participants were given a zero weight? -- yes that's it: 
tmp[usoc$x_wgt==0 & usoc$wave=="a", 1:10]
##       pidp hidp osm_hh hsdincome x_wgt hh_wgt wave month int.mth.sched
## NA      NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.1    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.2    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.3    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.4    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.5    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.6    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.7    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.8    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.9    NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.10   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.11   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.12   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.13   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.14   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.15   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.16   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.17   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.18   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.19   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.20   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
## NA.21   NA   NA     NA        NA    NA     NA <NA>  <NA>          <NA>
##       int.year.sched
## NA                NA
## NA.1              NA
## NA.2              NA
## NA.3              NA
## NA.4              NA
## NA.5              NA
## NA.6              NA
## NA.7              NA
## NA.8              NA
## NA.9              NA
## NA.10             NA
## NA.11             NA
## NA.12             NA
## NA.13             NA
## NA.14             NA
## NA.15             NA
## NA.16             NA
## NA.17             NA
## NA.18             NA
## NA.19             NA
## NA.20             NA
## NA.21             NA
# reasons for zero weight could be: missing info on key hsd variables used for wgt creation; exclusion from the target pop


## RECONSTRUCT CUSTOM WEIGHTS PER YEAR
# for each calendar year, rescale or normalise the weights so they sum to the sample size (or population total) for that year.

usoc <- usoc %>%
  group_by(int.year.sched, wave) %>%
  #  within each year, calculate the total of the weights for each *wave*
  mutate(wgt.total = sum(x_wgt, na.rm=T)) %>%
  ungroup() 

# now we have the total weight for each year x wave

usoc$wgt.rescaled <- NA # initialise 

# for each calendar year...
for (yr in sort(unique(usoc$int.year.sched))) {
  # list all waves contributing to that year
  waves.in.yr <- sort(unique(usoc$wave[usoc$int.year.sched==yr]))
  # choose the first wave of that list as the ref (note: for 2009, there's just one contributing wave "a")
  ref.wave  <- waves.in.yr[1] 
  ref.total <- unique(usoc$wgt.total[usoc$int.year.sched==yr & usoc$wave==ref.wave])[1] # one number: the sum of all weights for this year & the ref wave
  
  # for all waves contributing to this year...
  for (wv in waves.in.yr) {
   wv.total <- unique(usoc$wgt.total[usoc$int.year.sched == yr & usoc$wave == wv])[1] # one number
   SCALING.FACTOR <- ref.total/wv.total # e.g. sum of wgts for wave "a" / sum of wgts for wave "b" (similar to scaling of the N)
   usoc$wgt.rescaled[usoc$int.year.sched == yr & usoc$wave == wv] <- SCALING.FACTOR * usoc$x_wgt[usoc$int.year.sched == yr & usoc$wave == wv]
  }
}

Observations about the weight-rescaling
* no change to weights in wave “a” (2009 or 2010)
* change to wave “b” weights in 2010 but not in 2011
* change to wave c weights in 2011 (upscaled by ~0.22) but not change to wave c weights in 2012
* only negative scaling factor was in wave o for 2023.

Top 3 points make sense when you look at this table, only the “first in class” remain unchanged, the first wave in each year:

table(usoc$wave, usoc$int.year.sched)
##    
##     2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023
##   a 2578 2321    0    0    0    0    0    0    0    0    0    0    0    0    0
##   b    0 3040 1975    0    0    0    0    0    0    0    0    0    0    0    0
##   c    0    0 2682 1745    0    0    0    0    0    0    0    0    0    0    0
##   d    0    0    0 2412 1632    0    0    0    0    0    0    0    0    0    0
##   e    0    0    0    0 2256 1399    0    0    0    0    0    0    0    0    0
##   f    0    0    0    0    0 2213 1214    0    0    0    0    0    0    0    0
##   g    0    0    0    0    0    0 2011 1591    0    0    0    0    0    0    0
##   h    0    0    0    0    0    0    0 1827 1400    0    0    0    0    0    0
##   i    0    0    0    0    0    0    0    0 1624 1105    0    0    0    0    0
##   j    0    0    0    0    0    0    0    0    0 1385 1058    0    0    0    0
##   k    0    0    0    0    0    0    0    0    0    0 1319  730    0    0    0
##   l    0    0    0    0    0    0    0    0    0    0    0  932  694    0    0
##   m    0    0    0    0    0    0    0    0    0    0    0    0  989  730    0
##   n    0    0    0    0    0    0    0    0    0    0    0    0    0  847  705
##   o    0    0    0    0    0    0    0    0    0    0    0    0    0    0  644
##    
##     2024
##   a    0
##   b    0
##   c    0
##   d    0
##   e    0
##   f    0
##   g    0
##   h    0
##   i    0
##   j    0
##   k    0
##   l    0
##   m    0
##   n    0
##   o  756

Household income

The household income and equivalised income variables were calculated per wave. i.e. one person in one household could have very different equivalised incomes across wave A and B due to change in income, change in # dependants, or both. “Low income” in 2009 would not be the same as in 2024.

hist(usoc$equivinc[usoc$equivinc<5000])

# most households have about £1000 per month per person. Anything over £4000 pm pp is an outlier

ggplot(data=usoc[usoc$hsdincome>0 & usoc$hsdincome<10000,], aes(hsdincome))+
  geom_histogram(fill="darkorange", colour="black", binwidth = 500)+
  facet_wrap(~wave.y)+
  labs(title="Distribution of net monthly incomes (limited to £0 - £10,000)")

# this plot shows us that the mean of the sample has increased from <£2500 to >£2500 per month. But the distribution has also become much flatter-- lower numbers in the sample and more people at lower and higher ends of this range. 


# plot income across income groups
ggplot(data=usoc[usoc$hsdincome>0 & usoc$hsdincome<10000,], aes(x=hsdincome, fill=income.groups))+
  geom_histogram(colour="black", binwidth = 500, alpha=0.7)+
  facet_wrap(~int.year.sched)+
  labs(title="Distribution of net monthly incomes (limited to £0 - £10,000)")

Life Satisfaction

In the UKHLS young people were asked to tick a box to indicate,“which best describes how you feel about your life as a whole?.” Response options were presented on a 7‐point scale represented by more or less smiley faces. Participants were instructed that 1 (happiest face) “is completely happy” and 7 (very sad face) is “not at all happy” and asked to mark the scale point that most closely resembles how they currently feel about their life. We reverse coded so that high scores indicated high levels of life satisfaction.

for (age in sort(unique(usoc$dvage))){
  
usoc.lim <- usoc[usoc$dvage==age, ]

data4cor <- subset(usoc.lim, select=c(ypsdqtd_dv, h.appear.c, h.school.c , h.schoolwork.c , h.friends.c , h.fam.c, h.life.c ))

names(data4cor) <- c("SDQ total diff", "Satis.appear", "Satis.school", "Satis.schoolwork", "Satis.friends", "Satis.family", "Satis.life")
  
cormat <- round(cor(data4cor, use="pairwise.complete.obs"), digits=3)

lower <- cormat
lower[lower.tri(cormat)] <- NA

meltedcormat <- melt(lower, na.rm=T)
meltedcormat$value[meltedcormat$value==1] <- NA


ggplot(data=meltedcormat, aes(Var2, Var1, fill=value))+
  geom_tile(color="white") +
  scale_fill_gradient2(low="red", mid = "white", high="royalblue3", 
                       limit = c(-1,1),        # you might want to alter these min and max colour limits
                       space="Lab",
                       name="Pearson's r") +
  theme_minimal()+
  labs(title= paste("Participants aged ", age))+
  theme(plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle=60, 
                                   vjust=1 ,  hjust=1),
        axis.title.x=element_blank(),
        axis.title.y=element_blank())+
  geom_text(aes(Var2, Var1, label=round(value,2)), color="black", size=3)+
  coord_flip()


}
  dvlist <- c("h.life","h.school", "h.schoolwork", "h.friends","h.fam", "h.appear" )

# calculate % with each response in each year
# data.frame with 7 columns and ~15 rows
# cell should show % of that year's sample

d <- usoc %>%
  group_by(int.year.sched, socialmed) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)


# note: the mean is not updating across facets (waves)

for (x in dvlist){
var.ind <- which(names(usoc)==x)

d <- usoc %>%
  group_by(int.year.sched, usoc[,var.ind]) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)

d <- d[!is.na(d[,2]),]

g<-ggplot(data=d, aes_string(x=x, y="prop"))+
  geom_col()+
  scale_y_continuous(labels = scales::percent_format(accuracy = 5L), name="% each response")+
  scale_x_continuous(breaks=c(1,4,7), labels=c(":-(", ":-|", ":-)"), name = x)+
  facet_wrap(~int.year.sched)+
  theme(axis.text.x = element_text(angle=270, vjust = 0.5))
print(g)

}

# Plot distribution of SDQ with clinical-level scores in red 
d <- usoc %>%
  group_by(wave.y, ypsdqtd_dv) %>%
  summarise(n=n()) %>%
  mutate(tot.n = sum(n),
         prop = n/tot.n)

d <- d[!is.na(d$ypsdqtd_dv),]

d$clin <- as.factor(ifelse(d$ypsdqtd_dv>=20, "Very high (>=20)", "<20"))

ggplot(data=d, aes(x=ypsdqtd_dv, y=prop, fill=clin))+
  geom_col(binwidth=1)+
  scale_y_continuous(labels = scales::percent_format(), name="% each response")+
  #scale_x_continuous(breaks=c(1,4,7), labels=c(":-(", ":-|", ":-)"), name = x)+
  facet_wrap(~wave.y)+
  scale_fill_manual(values=c("black", "red"))

Main analysis

2. Relative influence of sub-domains on general life satisfaction

Find inter-correlations between satisfaction in each aspect of life

# takes ~15 seconds

# create data frames to store B coef and SEs of domain onto general life satisfaction, for each wave of data
coefs.wgts <- c() # adjusted for all other domains of life
coefs.wgts.unadj <- c() # unadjusted


 for (year.x in sort(unique(usoc$int.year.sched)) ){
#for (wave.x in c("a", "c", "e", "g", "i", "k", "m")){

  # extract cross-sec data for wave x
  xs <- usoc[usoc$int.year.sched==year.x,]

  # exclude participants with missing data on any life satis var
  xs <- xs[!is.na(xs$h.school.c) & !is.na(xs$h.schoolwork.c) & !is.na(xs$h.friends.c) & !is.na(xs$h.fam.c) & !is.na(xs$h.appear.c) & !is.na(xs$h.life.c), ]
  
  # set svy design for this wave
  usoc.svy <- svydesign(id=~psu + hidp, # hidp accounts for dependence among individuals within the same household by adjusting standard errors (via robust sandwich estimators)
                        strata=~strata, 
                        weights=~wgt.rescaled, 
                        data=xs,
                        nest=TRUE)
  options(survey.lonely.psu = "adjust") 

  
  # PREDICT GENERAL LIFE SATISFACTIONF FROM EACH DOMAIN (adjusting for all other domains)
  svyglm.multi <- svyglm(formula = h.life.c ~ h.school.c + h.schoolwork.c + h.friends.c + h.fam.c + h.appear.c,
                          design = usoc.svy)
  
  x= as.data.frame(summary(svyglm.multi)[["coefficients"]])
  
  coefs.wgts <- rbind(coefs.wgts, c(as.numeric(coef(svyglm.multi)), # betas
                                    as.numeric(x[,2])) ) # SEs
  
  
  # PREDICT GENERAL LIFE SATISFACTIONF FROM EACH DOMAIN (bivariate-ly)
  ivlist <- c("h.school.c", "h.schoolwork.c", "h.friends.c", "h.fam.c", "h.appear.c")
  
  # run GLM substituting each area of life as only predictor
  svyglm.uni <- lapply(ivlist, function(x) {
                  svyglm(eval(substitute(h.life.c ~   i , 
                         list(i = as.name(x)))), 
                        design = usoc.svy)})
  
   x <- c()
  for (domain in 1:5){
    x <- as.data.frame(rbind(x, 
                             data.frame(year.x=year.x,
                                        domain=ivlist[domain],
                                        Beta = summary(svyglm.uni[[domain]])[["coefficients"]][2,1], # mean of the 1st (non-intercept) term
                                        SE = summary(svyglm.uni[[domain]])[["coefficients"]][2,2], # this is the sampling error of the mean.
                                        N = length(svyglm.uni[[1]][["effects"]])
                                          ))) }
   
   coefs.wgts.unadj <- rbind(coefs.wgts.unadj, x)
   
 }  



coefs.wgts <- cbind(sort(unique(usoc$int.year.sched)), as.data.frame(round(coefs.wgts,4)))

names(coefs.wgts) <- c("year", "B.intercept", "B.school", "B.schoolwork", "B.friends", "B.family", "B.appearance",
                       "SE.intercept", "SE.school", "SE.schoolwork", "SE.friends", "SE.family", "SE.appearance")

coefs.wgts$year.n <- coefs.wgts$year-2009 # centers at 2009
coefs.wgts$window <- c(rep(1,5), rep(2,5), rep(3,6)) # note extra year in window3!


# re-structure to long-format data
long.wgts <- coefs.wgts %>%
  dplyr::select(year, B.school, B.schoolwork, B.friends, B.family, B.appearance)%>%
  gather(key = "domain", value="Beta", -year)
long.se <- coefs.wgts %>%
  dplyr::select(year, SE.school, SE.schoolwork, SE.friends, SE.family, SE.appearance)%>%
  gather(key = "domain", value="SE", -year)
wgts.long <- cbind(long.wgts, long.se[,"SE"])
names(wgts.long)[4] <- "SE"

wgts.long$`Happiness with...` <- factor(wgts.long$domain, 
                                   levels=c( "B.family" , "B.friends", 
                                             "B.school", "B.schoolwork",
                                             "B.appearance" ),
                                   labels=c( "Family" , "Friends", 
                                             "School", "Schoolwork",
                                             "Appearance" ))
wgts.long$year.n <- wgts.long$year-2009
wgts.long$windows <- cut(wgts.long$year, breaks=c(2009, 2014, 2019, 2025), labels=c(1, 2, 3))



# Plot change over time in asssociations

coefs.wgts.unadj$`Satisfaction with...` <- factor(coefs.wgts.unadj$domain, 
                                   levels=c( "h.fam.c" , "h.friends.c", 
                                             "h.school.c", "h.schoolwork.c",
                                             "h.appear.c" ),
                                   labels=c( "Family" , "Friends", 
                                             "School", "Schoolwork",
                                             "Appearance" ))

# bar chart UNADJUSTED
ggplot(data=coefs.wgts.unadj, aes(x=year.x, y=Beta))+
  geom_col(aes(fill=`Satisfaction with...`), alpha=0.7)+
  geom_errorbar(aes(ymin=Beta-SE, ymax=Beta+SE, colour=`Satisfaction with...`), width=0)+
  geom_smooth(aes(x=year.x, y=Beta), method="loess",  colour="grey60", se=F)+
  geom_smooth(aes(x=year.x, y=Beta), method="lm", formula = y~x, colour="black", se=T)+
  #geom_smooth(aes(x=year.x, y=Beta), method="lm", formula = y~poly(x,2), colour="black", se=F)+
  labs(y="Unadjusted association with life satisfaction (B ± SE)\n", x="\nSurvey year")+
  facet_grid(~`Satisfaction with...`)+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_y_continuous(limits=c(0, 1))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, angle=60, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())

# bar chart ADJUSTED
ggplot(data=wgts.long, aes(x=year, y=Beta))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.7)+
  geom_errorbar(aes(ymin=Beta-SE, ymax=Beta+SE, colour=`Happiness with...`), width=0)+
  geom_smooth(aes(x=year, y=Beta), method="loess",  colour="grey60", se=F)+
  geom_smooth(aes(x=year, y=Beta), method="lm", formula = y~x, colour="black", se=T)+
  #geom_smooth(aes(x=year, y=Beta), method="lm", formula = y~poly(x,2), colour="black", se=F)+
  labs(y="Adjusted association with life satisfaction (B ± SE)\n", x="\nSurvey year")+
  facet_grid(~`Happiness with...`)+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_y_continuous(limits=c(0, 0.5))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, angle=60, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())

## WITHOUT COVID
# bar chart UNADJUSTED
ggplot(data=coefs.wgts.unadj[coefs.wgts.unadj$year.x!=2020 & coefs.wgts.unadj$year.x!=2021,], aes(x=year.x, y=Beta))+
  geom_col(aes(fill=`Satisfaction with...`), alpha=0.7)+
  geom_errorbar(aes(ymin=Beta-SE, ymax=Beta+SE, colour=`Satisfaction with...`), width=0)+
  geom_smooth(aes(x=year.x, y=Beta), method="loess",  colour="grey60", se=F)+
  geom_smooth(aes(x=year.x, y=Beta), method="lm", formula = y~x, colour="black", se=T)+
  #geom_smooth(aes(x=year.x, y=Beta), method="lm", formula = y~poly(x,2), colour="black", se=F)+
  labs(y="UNADJUSTED association with life satisfaction (B ± SE)\n", x="\nSurvey year")+
  facet_grid(~`Satisfaction with...`)+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_y_continuous(limits=c(0, 1))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, angle=60, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())

# bar chart ADJUSTED
ggplot(data=wgts.long[wgts.long$year!=2020 & wgts.long$year!=2021,], aes(x=year, y=Beta))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.7)+
  geom_errorbar(aes(ymin=Beta-SE, ymax=Beta+SE, colour=`Happiness with...`), width=0)+
  geom_smooth(aes(x=year, y=Beta), method="loess",  colour="grey60", se=F)+
  geom_smooth(aes(x=year, y=Beta), method="lm", formula = y~x, colour="black", se=T)+
  #geom_smooth(aes(x=year, y=Beta), method="lm", formula = y~poly(x,2), colour="black", se=F)+
  labs(y="Adjusted association with life satisfaction (B ± SE)\n", x="\nSurvey year")+
  facet_grid(~`Happiness with...`)+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_y_continuous(limits=c(0, 0.5))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, angle=60, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())

Note the y-axis represents the association between each domain and “general life satisfaction”, after controlling for the other domains.

  • Notable rise in the relative influence of “happiness with appearance” on general life satisfcation, over full time period
  • Rise in relative influence of “happiness with family”
  • Smaller changes in friends, school and schoolwork
# save meta-regression output here:
wgt.avgs <- c()
wgt.trends  <- c()
wgt.avg.5ywin  <- c() # this one's different to the previous "change *within* 5 year windows". This time we're looking at 5-year *average* i.e. intercept
var.cov.matrix <- c()
var.cov.guide <- c()

for (domain in levels(as.factor(wgts.long$domain))) {

  dat <- wgts.long[wgts.long$domain==domain, ]
  
  D <- diag(dat$SE, 16, 16)
  V <- D %*% R %*% D
  
  # store these variance-covariance matrices for subgroup analyses later
  var.cov.matrix <- abind(var.cov.matrix, V)
  var.cov.guide <- cbind(var.cov.guide, domain)
  
  # intercept-only M.A: What is the pooled mean across all my waves?
  rma.ave <- rma.mv(yi = Beta,
                    V = V,
                    data=dat) 
       
        # save
        wgt.avgs <- rbind(wgt.avgs, data.frame(
                                      domain = domain,
                                      beta = as.numeric(rma.ave[["beta"]]), 
                                      se = as.numeric(rma.ave[["se"]]),
                                      ci.low = as.numeric(rma.ave[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.ave[["ci.ub"]])) )
  
  # include time as moderator...
  # full time period 
  rma.trend.full <- rma.mv(yi = Beta,
                           V = V,
                           mods = ~ year.n,
                           data = dat)
         # save
         wgt.trends <- rbind(wgt.trends, data.frame(
                                           domain=domain,
                                           beta=as.numeric(rma.trend.full[["beta"]][2]),
                                           se=as.numeric(rma.trend.full[["se"]][2]),
                                           ci.low=as.numeric(rma.trend.full[["ci.lb"]][2]),
                                           ci.hi=as.numeric(rma.trend.full[["ci.ub"]][2]),
                                           p=round(as.numeric(rma.trend.full[["pval"]][2]),4)))
         
    # Note: There's so much year-to-year fluctuation with this data, I don't think we should be looking at within-5 year changes. But we could estimate *average* associations within each 5-year window, to average out some of those fluctuations...
         
         for (win in 1:3){
           dat.win <- wgts.long[wgts.long$domain==domain & wgts.long$windows==win & !is.na(wgts.long$windows), ]
           D <- diag(dat.win$SE)
    
            # limit the covariance matrix R to the correct years
            Rsub <- R[2:nrow(R), 2:ncol(R)] # remove 2009 (excluded from 5-y windows)
            if (win==1){
              Rsub <- Rsub[1:5, 1:5]
            } else if  (win==2){
              Rsub <- Rsub[6:10, 6:10]
            } else if  (win==3){
              Rsub <- Rsub[11:15, 11:15]
            }
            # create the variance-covariance matrix
            V <- D %*% Rsub %*% D
            
            rma.ave.win <- rma.mv(yi = Beta, V = V, data=dat.win)
            
            wgt.avg.5ywin <- rbind(wgt.avg.5ywin, data.frame(
                                      domain = domain,
                                      window = win,
                                      beta = as.numeric(rma.ave.win[["beta"]]), 
                                      se = as.numeric(rma.ave.win[["se"]]),
                                      ci.low = as.numeric(rma.ave.win[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.ave.win[["ci.ub"]])) )
            
           }

}

wgt.avgs[,c(2:5)] <- round(wgt.avgs[,c(2:5)], 2)
wgt.trends[,c(2:5)] <- round(wgt.trends[,c(2:5)], 3)

print(wgt.avgs)
##         domain beta   se ci.low ci.hi
## 1 B.appearance 0.25 0.01   0.24  0.27
## 2     B.family 0.33 0.01   0.31  0.36
## 3    B.friends 0.15 0.01   0.12  0.18
## 4     B.school 0.10 0.01   0.08  0.12
## 5 B.schoolwork 0.12 0.01   0.10  0.13
print(wgt.trends)
##         domain   beta    se ci.low ci.hi      p
## 1 B.appearance  0.009 0.002  0.005 0.012 0.0000
## 2     B.family  0.003 0.003 -0.003 0.008 0.3215
## 3    B.friends -0.004 0.003 -0.009 0.001 0.1453
## 4     B.school  0.000 0.002 -0.004 0.003 0.8751
## 5 B.schoolwork -0.001 0.002 -0.004 0.003 0.7309
library(abind)

# save meta-regression output here:
wgt.avgs <- c()
wgt.trends  <- c()
var.cov.matrix <- c()
var.cov.guide <- c()

for (domain in levels(as.factor(coefs.wgts.unadj$domain))) {

  dat <- coefs.wgts.unadj[coefs.wgts.unadj$domain==domain, ]
  
  D <- diag(dat$SE, 16, 16)
  V <- D %*% R %*% D
  
  # store these variance-covariance matrices for subgroup analyses later
  var.cov.matrix <- abind(var.cov.matrix, V)
  var.cov.guide <- cbind(var.cov.guide, domain)
  
  # intercept-only M.A: What is the pooled mean across all my waves?
  rma.ave <- rma.mv(yi = Beta,
                    V = V,
                    data=dat) 
       
        # save
        wgt.avgs <- rbind(wgt.avgs, data.frame(
                                      domain = domain,
                                      beta = as.numeric(rma.ave[["beta"]]), 
                                      se = as.numeric(rma.ave[["se"]]),
                                      ci.low = as.numeric(rma.ave[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.ave[["ci.ub"]])) )
  
  # include time as moderator...
  # full time period 
  rma.trend.full <- rma.mv(yi = Beta,
                           V = V,
                           mods = ~ year.x,
                           data = dat)
         # save
         wgt.trends <- rbind(wgt.trends, data.frame(
                                           domain=domain,
                                           beta=as.numeric(rma.trend.full[["beta"]][2]),
                                           se=as.numeric(rma.trend.full[["se"]][2]),
                                           ci.low=as.numeric(rma.trend.full[["ci.lb"]][2]),
                                           ci.hi=as.numeric(rma.trend.full[["ci.ub"]][2]),
                                           p=round(as.numeric(rma.trend.full[["pval"]][2]),4)))

}

wgt.avgs[,c(2:5)] <- round(wgt.avgs[,c(2:5)], 2)
wgt.trends[,c(2:5)] <- round(wgt.trends[,c(2:5)], 3)

– by demographic group

# step 1. run the adjusted model (all subdomains predicting gen life happiness) within each subgroup
# TAKES ~1-2 MINS

wgts.bygroup <- c() 

for (group in c("Female", "Male", "10-12", "13-15", "high.inc",  "low.inc" , "mid.inc")){
 
 for (year.x in sort(unique(usoc$int.year.sched)) ){

     # limit dataset to a specific year
  xs <- usoc[usoc$int.year.sched==year.x ,]
  xs <- xs[!is.na(xs$h.school.c) & !is.na(xs$h.schoolwork.c) & !is.na(xs$h.friends.c) & !is.na(xs$h.fam.c) & !is.na(xs$h.appear.c) & !is.na(xs$h.life.c), ]
  
    usoc.svy <- svydesign(id=~psu + hidp,
                        strata=~strata, 
                        weights=~wgt.rescaled, 
                        data=xs,
                        nest=TRUE)
    options(survey.lonely.psu = "adjust") 
    
    # limit cross-sectional survey data to group
    if (group=="Female"|group=="Male"){
    tmp <- usoc.svy$variables$sex_dv == group   
     } else if (group=="10-12"|group=="13-15") {
    tmp <- usoc.svy$variables$age.group == group
     } else if (str_detect(group, "inc")) {
    tmp <- usoc.svy$variables$income.groups == group
     }
    
    usoc.svg.group <- subset(usoc.svy, tmp)
    options(survey.lonely.psu = "adjust") 
    
    # find relative influence of each aspect of life, for THIS group, in this year.
    group.glm <- svyglm(formula = h.life.c ~ h.school.c + h.schoolwork.c
                    + h.friends.c + h.fam.c + h.appear.c,
                    design = usoc.svg.group)
    
    coef <- as.data.frame(summary(group.glm)[["coefficients"]])
  
    wgts.bygroup <- rbind(wgts.bygroup, data.frame(
                                              year = year.x,
                                              group = group,
                                              n = length(group.glm$effects),
                                              school.B = coef$Estimate[rownames(coef)=="h.school.c"],
                                              school.SE = coef$`Std. Error`[rownames(coef)=="h.school.c"],
                                              schoolwork.B = coef$Estimate[rownames(coef)=="h.schoolwork.c"],
                                              schoolwork.SE = coef$`Std. Error`[rownames(coef)=="h.schoolwork.c"],
                                              fam.B = coef$Estimate[rownames(coef)=="h.fam.c"],
                                              fam.SE = coef$`Std. Error`[rownames(coef)=="h.fam.c"],
                                              friend.B = coef$Estimate[rownames(coef)=="h.friends.c"],
                                              friend.SE = coef$`Std. Error`[rownames(coef)=="h.friends.c"],
                                              appear.B = coef$Estimate[rownames(coef)=="h.appear.c"],
                                              appear.SE = coef$`Std. Error`[rownames(coef)=="h.appear.c"]))
 }
}

# re-structure to long-format data
long.wgts <- wgts.bygroup %>%
  dplyr::select(year, group, n, school.B, schoolwork.B, friend.B, fam.B, appear.B)%>%
  gather(key = "domain", value="Beta", -c(year, group, n))
long.se <- wgts.bygroup %>%
  dplyr::select(year, group, n, school.SE, schoolwork.SE, friend.SE, fam.SE, appear.SE)%>%
  gather(key = "domain", value="SE", -c(year, group, n))
group.wgts.long <- cbind(long.wgts, long.se[,"SE"])
names(group.wgts.long)[6] <- "SE"

group.wgts.long$`Happiness with...` <- factor(group.wgts.long$domain, 
                                   levels=c("fam.B","friend.B", "school.B", "schoolwork.B", "appear.B" ),
                                   labels=c( "Family" , "Friends", 
                                             "School", "Schoolwork",
                                             "Appearance" ))
group.wgts.long$year.n <- group.wgts.long$year-2009



# step 2 
# input group-specific estimates into a meta-analysis to estimate 
#(i)  the average association estimates for that group and 
#(ii) see the association has changed over time for either group 
group.B.ave <- c()
group.B.5yave <- c()
group.B.change <- c()

for (group in levels(as.factor(group.wgts.long$group))) {
  for (domain in levels(as.factor(group.wgts.long$domain))) {
  
  dat <- group.wgts.long[group.wgts.long$domain==domain & group.wgts.long$group==group, ]
  
  D <- diag(dat$SE, 16, 16)
  V <- D %*% R %*% D
  
  # intercept-only M.A: What is the pooled estimate across all years?
  rma.ave <- rma.mv(yi = Beta,
                    V = V,
                    data=dat) 
       
        # save
        group.B.ave <- rbind(group.B.ave, data.frame(
                                      domain = domain,
                                      group = group,
                                      beta = as.numeric(rma.ave[["beta"]]), 
                                      se = as.numeric(rma.ave[["se"]]),
                                      ci.low = as.numeric(rma.ave[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.ave[["ci.ub"]])) )
        
  # average for last 5 years only
  dat.lim <- dat[dat$year>=2020,]
  
  D.lim <- diag(dat.lim$SE)
  R.lim <- R[12:16, 12:16]
  
  V.lim <- D.lim %*% R.lim %*% D.lim
  
  # intercept-only M.A: What is the pooled estimate across these 5 years?
  rma.5yave <- rma.mv(yi = Beta,
                    V = V.lim,
                    data=dat.lim)
  
  # save
        group.B.5yave <- rbind(group.B.5yave, data.frame(
                                      domain = domain,
                                      group = group,
                                      beta = as.numeric(rma.5yave[["beta"]]), 
                                      se = as.numeric(rma.5yave[["se"]]),
                                      ci.low = as.numeric(rma.5yave[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.5yave[["ci.ub"]])) )
  
  # include time as moderator to see if things have changed significantly for that group
  # full time period 
  rma.trend.full <- rma.mv(yi = Beta,
                           V = V,
                           mods = ~ year.n,
                           data = dat)
         # save
         group.B.change <- rbind(group.B.change, data.frame(
                                           domain=domain,
                                           group=group,
                                           beta=as.numeric(rma.trend.full[["beta"]][2]),
                                           se=as.numeric(rma.trend.full[["se"]][2]),
                                           ci.low=as.numeric(rma.trend.full[["ci.lb"]][2]),
                                           ci.hi=as.numeric(rma.trend.full[["ci.ub"]][2]),
                                           p=round(as.numeric(rma.trend.full[["pval"]][2]),4)))
         
}
}




# extract table of Ns per year
Ns <- data.frame(year = wgts.bygroup$year[wgts.bygroup$group=="Female"],
          Females = wgts.bygroup$n[wgts.bygroup$group=="Female"],
          Males = wgts.bygroup$n[wgts.bygroup$group=="Male"],
          `10-12 years` =  wgts.bygroup$n[wgts.bygroup$group=="10-12"],
          `13-15 years` =  wgts.bygroup$n[wgts.bygroup$group=="13-15"],
          `High income` = wgts.bygroup$n[wgts.bygroup$group=="high.inc"],
          `Middle income` = wgts.bygroup$n[wgts.bygroup$group=="mid.inc"],
          `Low income` = wgts.bygroup$n[wgts.bygroup$group=="low.inc"]
          )
# approach: run one rma() per group factor (e.g. sex) and include an interaction between sex and time. Use custom function metareg.timeXgroup

# prepare the dataset of betas for the function
group.wgts.long$year.x <- group.wgts.long$year
group.wgts.long$group.factor <- factor(group.wgts.long$group, levels=c("10-12","13-15",
                                                                       "Female" , "Male" ,
                                                                       "high.inc", "mid.inc","low.inc"),
                                       labels=c("age", "age", "sex", "sex", "income", "income", "income"))
group.wgts.long$dvlist <- factor(group.wgts.long$domain, levels=c("school.B","schoolwork.B","fam.B", "friend.B","appear.B"), labels=c("h.school.c", "h.schoolwork.c","h.fam.c",  "h.friends.c", "h.appear.c"))
group.wgts.long$mean <- group.wgts.long$Beta
group.wgts.long$se <- group.wgts.long$SE


# limit to one group factor (CHECK metareg.timeXgroup() FUNCTION TO SEE WHY NOT WORKING (VAR-COVAR MATRIX))
df = group.wgts.long[group.wgts.long$group.factor=="sex",]
betas.sexdiffs <- metareg.timeXgroup(df, "full")

df = group.wgts.long[group.wgts.long$group.factor=="age",]
betas.agediffs <- metareg.timeXgroup(df, "full")

df = group.wgts.long[group.wgts.long$group.factor=="income",]
betas.incdiffs <- metareg.timeXgroup(df, "full")
dplot <- group.wgts.long
 dplot$group <- factor(dplot$group, levels=c("Female", "Male", "10-12", "13-15", "high.inc", "mid.inc","low.inc"))

ggplot(data=dplot[dplot$group=="Male" | dplot$group=="Female", ], 
               aes(x=year, y=Beta, group=group))+
  geom_line(aes(colour=group), size=1)+
  geom_ribbon(aes(ymin=Beta-2*SE, ymax=Beta+2*SE, fill=group),alpha=0.2)+
  facet_grid(cols=vars(`Happiness with...`))+
  theme_minimal()+
  #scale_y_continuous(breaks=c(1,2), minor_breaks = c(0.5, 1.5,2.5), 
  #                   limits = c(0.3,2.6), name = "\nHappiness (-3 to +3)")+
  scale_x_continuous(breaks=seq(2010,2024,2),  limits = c(2009,2024), name = "")+
  #scale_shape_manual(values=c(3,15))+
  scale_colour_manual(values=c("darkorange2", "turquoise3"))+
  scale_fill_manual(values=c( "darkorange2", "turquoise3"))+
  theme(axis.text.x = element_text(angle=60, hjust = 1, size=6))

ggplot(data=dplot[dplot$group=="10-12" | dplot$group=="13-15", ], 
               aes(x=year, y=Beta, group=group))+
  geom_line(aes(colour=group), size=1)+
  geom_ribbon(aes(ymin=Beta-2*SE, ymax=Beta+2*SE, fill=group),alpha=0.2)+
  facet_grid(cols=vars(`Happiness with...`))+
  theme_minimal()+
  #scale_y_continuous(breaks=c(1,2), minor_breaks = c(0.5, 1.5,2.5), 
  #                   limits = c(0.3,2.6), name = "\nHappiness (-3 to +3)")+
  scale_x_continuous(breaks=seq(2010,2024,2),  limits = c(2009,2024), name = "")+
  #scale_shape_manual(values=c(3,15))+
  scale_colour_manual(values=c("olivedrab3", "darkseagreen4"))+
  scale_fill_manual(values=c("olivedrab3", "darkseagreen4"))+
  theme(axis.text.x = element_text(angle=60, hjust = 1, size=6))

ggplot(data=dplot[str_detect(dplot$group, "inc"), ], 
               aes(x=year, y=Beta, group=group))+
  geom_line(aes(colour=group), size=1)+
  geom_ribbon(aes(ymin=Beta-2*SE, ymax=Beta+2*SE, fill=group),alpha=0.2)+
  facet_grid(cols=vars(`Happiness with...`))+
  theme_minimal()+
  #scale_y_continuous(breaks=c(1,2), minor_breaks = c(0.5, 1.5,2.5), 
  #                   limits = c(0.3,2.6), name = "\nHappiness (-3 to +3)")+
  scale_x_continuous(breaks=seq(2010,2024,2),  limits = c(2009,2024), name = "")+
  #scale_shape_manual(values=c(3,15))+
  scale_colour_manual(values=c("darkslateblue","plum4","thistle" ))+
  scale_fill_manual(values=c( "darkslateblue","plum4","thistle" ))+
  theme(axis.text.x = element_text(angle=60, hjust = 1, size=6))

#  averages over all 16 yrs
group.B.ave$domain.nice <- str_remove_all(group.B.ave$domain, ".B")

ggplot(data=group.B.ave[str_detect(group.B.ave$group, "-1"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by age-group")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

ggplot(data=group.B.ave[str_detect(group.B.ave$group, "ale"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by sex")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

ggplot(data=group.B.ave[str_detect(group.B.ave$group, "inc"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by hsd income")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

# If we limit data to the last 5 years?

# average associational strengths in last 5 yrs

group.B.5yave$domain.nice <- str_remove_all(group.B.5yave$domain, ".B")

ggplot(data=group.B.5yave[str_detect(group.B.5yave$group, "-1"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by age-group")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

ggplot(data=group.B.5yave[str_detect(group.B.5yave$group, "ale"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by sex")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

ggplot(data=group.B.5yave[str_detect(group.B.5yave$group, "inc"),], 
       aes(x= reorder(domain.nice,-beta), y=beta, ymin=ci.low, ymax=ci.hi, fill=domain.nice))+
  geom_col()+
  geom_errorbar(width=0)+
  labs(x="Subdomains of life", y="Association with life satisfaction", title= "Split by hsd income")+
  facet_grid(~group)+
  theme(axis.text.x = element_text(angle=30, size=9, hjust=1),
        legend.position = "none")

There was no significant change over time in any group differences in associational strength between each sub-domain and general life satisfaction.

The association between subdomains and genral life happiness may be so high that there is not much room for moderation by group. But when we replace the life happiness item with SDQ total, we also don’t see much difference by sex.

Which groups and which domains showed a significant change over the 16 year time period?

ordered.df <- group.B.change[order(group.B.change$p),]

kable(ordered.df[1:10,], escape=F)%>% 
  kableExtra::kable_styling(full_width = F, font_size = 15)
domain group beta se ci.low ci.hi p
16 appear.B high.inc 0.0099132 0.0026057 0.0048061 0.0150202 0.0001
6 appear.B 13-15 0.0085061 0.0024330 0.0037374 0.0132748 0.0005
11 appear.B Female 0.0079901 0.0023101 0.0034624 0.0125177 0.0005
1 appear.B 10-12 0.0086877 0.0025798 0.0036314 0.0137439 0.0008
26 appear.B Male 0.0079438 0.0027543 0.0025455 0.0133420 0.0039
21 appear.B low.inc 0.0084731 0.0030817 0.0024330 0.0145131 0.0060
31 appear.B mid.inc 0.0082887 0.0030383 0.0023337 0.0142436 0.0064
8 friend.B 13-15 -0.0088178 0.0034441 -0.0155681 -0.0020675 0.0105
13 friend.B Female -0.0084844 0.0033168 -0.0149852 -0.0019836 0.0105
12 fam.B Female 0.0085772 0.0036735 0.0013772 0.0157772 0.0196

All groups showed a significant increase in the relative “weighting” of appearance on general life satisfaction (here’s the groups in order of that change):
- high income kids (B = 0.011)
- 13-15 year olds (B = 0.009)
- 10-12 year olds (B = 0.009)
- Females (B = 0.008)
- Males (B = 0.008)
- Low income kids (B = 0.009), p = .007
- Mid income kids (B = 0.007), p = .025

Partial & bivariate correlations (bounded between [-1, 1])

data4cor <- subset(usoc, select=c(h.life, h.appear, h.school, h.schoolwork, h.friends, h.fam))
data4cor <- data4cor[complete.cases(data4cor),]

names(data4cor) <- c("Life as whole", "Appearance", "School", "Schoolwork", "Friends", "Family")

# compute partial correlations
pc  <- pcor(data4cor)
pc_matrix <- pc$estimate

# convert to edge list
edges <- pc_matrix %>%
  as.data.frame() %>%
  rownames_to_column("var1") %>%
  pivot_longer(-var1, names_to = "var2", values_to = "partial_r") %>%
  filter(var1 < var2)

# Define the other variables (in any order you prefer)
vars <- c("Life as whole", "Appearance", "School", "Schoolwork", "Friends", "Family")
others <- vars[vars != "Life as whole"]

# Create semicircle layout for the lower nodes
theta <- seq(-pi/6, -5*pi/6, length.out = length(others))  # wide, smooth arc

nodes <- data.frame(
  name = vars,
  x = c(0,                  # h.life centred in middle
        cos(theta)),        # other variables on arc
  y = c(0.1,                  # h.life
        -0.1 + 5*sin(theta))  # increase curvature for bottom row of nodes
)

g <- graph_from_data_frame(edges, directed = FALSE, vertices = nodes)

# Identify edges connected to "h.life"
connected_to_hlife <- incident(g, "Life as whole", mode = "all")

# Create an edge color vector for all edges
edge_cols <- rep("grey70", ecount(g))
edge_cols[connected_to_hlife] <- "black"

E(g)$edge_color <- edge_cols

ggraph(g, layout = "manual", x = x, y = y) +
  geom_edge_link(aes(width = partial_r,  colour= edge_color)) +
  #geom_node_point(size = 12, colour = "white") +
  geom_node_label(aes(label = name, fill=name), size =4, label.r = unit(7, "pt"),
                  label.padding = unit(6, "pt")) +
  scale_edge_width(limits = c(0.01,   0.5), # partial correlation values
                   range =  c(0.1, 5)) + # line thickness values
  scale_fill_manual(values=c("#E78AC3FF" ,"#8DA0CBFF", "#FC8D62FF",  "lightgrey","#66C2A5FF","#A6D854FF" ))+
  scale_edge_colour_manual(values=c("black","grey80"))+
  #scale_edge_colour_gradient2(low = "red", mid = "grey80", high = "blue") +
   theme_void()+
   theme(legend.position = "none",
         plot.background = element_rect(fill="white"))

#print(plot)
# run the network plot for every 5 years

plot_list <- vector("list", length = 3)


# for (YEAR in sort(unique(usoc$int.year.sched))){
for (window in sort(unique(usoc$time.window))) {

# select life satis data for 1 year only (no NAs)
data4cor <- subset(usoc[usoc$time.window==window,], select=c(h.life, h.appear, h.school, h.schoolwork, h.friends, h.fam))

data4cor <- data4cor[complete.cases(data4cor),]

# compute partial correlations
pc  <- pcor(data4cor)
pc_matrix <- pc$estimate

# convert to edge list
edges <- pc_matrix %>%
  as.data.frame() %>%
  rownames_to_column("var1") %>%
  pivot_longer(-var1, names_to = "var2", values_to = "partial_r") %>%
  filter(var1 < var2)

# Define the other variables (in any order you prefer)
vars <- c("h.life", "h.appear" , "h.schoolwork","h.school", "h.friends", "h.fam" )
others <- vars[vars != "h.life"]

# Create semicircle layout for the lower nodes
theta <- seq(-pi/6, -5*pi/6, length.out = length(others))  # wide, smooth arc

nodes <- data.frame(
  name = vars,
  x = c(0,                  # h.life at centre top
        cos(theta)),        # other variables on arc
  y = c(0.8,                  # h.life
        -1 + 7*sin(theta))  # increase curvature for bottom row of nodes
)

g <- graph_from_data_frame(edges, directed = FALSE, vertices = nodes)

# Identify edges connected to "h.life"
connected_to_hlife <- incident(g, "h.life", mode = "all")

# Create an edge color vector for all edges
edge_cols <- rep("grey70", ecount(g))
edge_cols[connected_to_hlife] <- "black"

E(g)$edge_color <- edge_cols

plot_list[[as.numeric(window)]] <- ggraph(g, layout = "manual", x = x, y = y) +
  geom_edge_link(aes(width = partial_r,  colour= edge_color)) +
  #geom_node_point(size = 12, colour = "white") +
  geom_node_label(aes(label = name, fill=name), size =4, label.r = unit(7, "pt"),
                  label.padding = unit(6, "pt")) +
  scale_edge_width(limits = c(0.05,   0.5), # partial correlation values
                   range =  c(0.1, 4)) + # line thickness values
  scale_fill_manual(values=c("#E78AC3FF" ,"#8DA0CBFF", "#FC8D62FF",  "lightgrey","#66C2A5FF","#A6D854FF" ))+
  scale_edge_colour_manual(values=c("black","grey80"))+
  #scale_edge_colour_gradient2(low = "red", mid = "grey80", high = "blue") +
  labs(title=paste("Time window:", window))+
   theme_void()+
   theme(legend.position = "none",
         plot.background = element_rect(fill="white"))
#print(plot)


}

ggarrange(plot_list[[1]], plot_list[[2]], plot_list[[3]], nrow=1, ncol=3)

library(ppcor)
library(reshape2)

# save correlation between happiness in each domain & general life happiness
cortbl <- data.frame(year = sort(unique(usoc$int.year.sched)), 
                      r.sch = NA,
                      r.schwk = NA,
                      r.fam = NA,
                      r.frnd = NA,
                      r.app = NA,
                  
                      p.sch = NA,
                      p.schwk = NA,
                      p.fam = NA,
                      p.frnd = NA,
                      p.app = NA)


for (y in sort(unique(usoc$int.year.sched))){
  
# pearson's bivariate correlations
 cortbl$r.sch[cortbl$year==y] <- cor(usoc$h.life[usoc$int.year.sched==y], usoc$h.school[usoc$int.year.sched==y],use="complete.obs")
 cortbl$r.schwk[cortbl$year==y] <- cor(usoc$h.life[usoc$int.year.sched==y], usoc$h.schoolwork[usoc$int.year.sched==y],use="complete.obs")
 cortbl$r.fam[cortbl$year==y] <- cor(usoc$h.life[usoc$int.year.sched==y], usoc$h.fam[usoc$int.year.sched==y],use="complete.obs")
 cortbl$r.frnd[cortbl$year==y] <- cor(usoc$h.life[usoc$int.year.sched==y], usoc$h.friends[usoc$int.year.sched==y],use="complete.obs")
 cortbl$r.app[cortbl$year==y] <- cor(usoc$h.life[usoc$int.year.sched==y], usoc$h.appear[usoc$int.year.sched==y],use="complete.obs")
  
 # partial correlations
 p <- pcor(na.omit(usoc[usoc$int.year.sched==y,
                        c("h.life", "h.school", "h.schoolwork", "h.fam", "h.friends", "h.appear")]))$estimate
 p2 <- p
 p2[lower.tri(p)] <- NA

 p3 <- melt(p2, na.rm=T)
 plife <- p3[p3$value!=1 & p3$Var1=="h.life",]
 
 cortbl$p.sch[cortbl$year==y] <- plife$value[plife$Var2=="h.school"]
 cortbl$p.schwk[cortbl$year==y] <- plife$value[plife$Var2=="h.schoolwork"]
 cortbl$p.fam[cortbl$year==y] <- plife$value[plife$Var2=="h.fam"]
 cortbl$p.frnd[cortbl$year==y] <- plife$value[plife$Var2=="h.friends"]
 cortbl$p.app[cortbl$year==y] <- plife$value[plife$Var2=="h.appear"]
}

# plot these estimates as barchart

long.cor <- cortbl %>%
  dplyr::select(year, r.sch,  r.schwk ,r.fam ,r.frnd , r.app)%>%
  gather(key = "domain", value="corr", -year)

long.partcor <- cortbl %>%
  dplyr::select(year, p.sch,  p.schwk ,p.fam ,p.frnd , p.app)%>%
  gather(key = "domain", value="partialcorr", -year)

long.cor <- cbind(long.cor, long.partcor[,"partialcorr"])
names(long.cor)[str_detect(names(long.cor), "partialcorr")] <- "partialcorr"

long.cor$`Happiness with...` <- factor(long.cor$domain, 
                                   levels=c( "r.fam" ,"r.frnd" ,"r.sch",  "r.schwk" , "r.app"),
                                   labels=c( "Family" , "Friends", 
                                             "School", "Schoolwork",
                                             "Appearance" ))
# bivariate correlations
ggplot(data=long.cor, aes(x=year, y=corr))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.7)+
  geom_smooth(aes(x=year, y=corr), method="lm", formula=y~poly(x,2), colour="black", se=F)+
  geom_smooth(aes(x=year, y=corr), colour="grey40", se=F)+
  labs(y="Association with general life happiness (corr)\n", x="\nSurvey year", title="Bivariate correlations")+
  facet_grid(~`Happiness with...`)+
  #scale_y_continuous(limits=c(0, 0.5))+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=7, angle=60, hjust=1, vjust = 1),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# partial correlations
ggplot(data=long.cor, aes(x=year, y=partialcorr))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.7)+
  geom_smooth(aes(x=year, y=partialcorr), method="lm", formula=y~poly(x,2), colour="black", se=F)+
  geom_smooth(aes(x=year, y=partialcorr), colour="grey40", se=F)+
  labs(y="Association with general life happiness (pcorr)\n", x="\nSurvey year", title="Partial correlations")+
  facet_grid(~`Happiness with...`)+
  #scale_y_continuous(limits=c(0, 0.5))+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=7, angle=60, hjust=1, vjust = 1),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank())
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# note: these partial correlations can't be compared directly with results of the svyglm

# plot network diagram for connection between domains

Sensitivity analyses

1. Representativeness checks

# 1. PLOT THE Ns (AGAIN) TO SUPPORT OVERALL ATTRITION 
n_year <- usoc %>%
  filter(!is.na(h.life.c)) %>%
  group_by(int.year.sched) %>%
  summarise(n = n_distinct(pidp), .groups = "drop")

ggplot(n_year, aes(x = int.year.sched, y = n)) +
  geom_col(fill = "#4477AA", alpha = 0.85) +
  geom_text(aes(label = n), vjust = -0.4, size = 3) +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Fig 1. Sample size by year",
       subtitle = "N with complete life satisfaction data",
       x = "Year", y = "N participants") +
  theme_classic(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

#  2. VISUALISE THE CHANGES IN DEMOGRAPHY

# --- 2a. Age distribution ---
age_year <- usoc %>%
  group_by(int.year.sched, dvage) %>%
  summarise(n = n_distinct(pidp), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop = n / sum(n))

fig2a <- ggplot(age_year, aes(x = int.year.sched, y = prop, fill = dvage)) +
  geom_area(alpha = 0.85, colour = "white", linewidth = 0.3) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_brewer(palette = "Blues", name = "Age") +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Age composition", x = "Year", y = "% of sample") +
  theme_classic(base_size = 11)

# --- 2b. Sex distribution ---
sex_year <- usoc %>%
  filter(!is.na(sex.bin)) %>%
  group_by(int.year.sched, sex.bin) %>%
  summarise(n = n_distinct(pidp), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop = n / sum(n))

fig2b <- ggplot(sex_year, aes(x = int.year.sched, y = prop, fill = sex.bin)) +
  geom_area(alpha = 0.8, colour = "white", linewidth = 0.3) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_manual(values = c("Male" = "#4477AA", "Female" = "#EE6677"), name = "Sex") +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Sex composition", x = "Year", y = "% of sample") +
  theme_classic(base_size = 11)

# --- 2c. Ethnicity distribution ---
eth_year <- usoc %>%
  filter(!is.na(ethnicity)) %>%
  group_by(int.year.sched, ethnicity) %>%
  summarise(n = n_distinct(pidp), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop = n / sum(n))

fig2c <- ggplot(eth_year, aes(x = int.year.sched, y = prop, fill = ethnicity)) +
  geom_area(alpha = 0.85, colour = "white", linewidth = 0.3) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_brewer(palette = "Set2", name = "Ethnicity") +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Ethnicity composition", x = "Year", y = "% of sample") +
  theme_classic(base_size = 11) +
  theme(legend.text = element_text(size = 8),
        legend.position = "right",
        axis.text.x = element_text(angle = 30, hjust = 1))


# --- 2d. Income (mean equivalised hsd income by year; capped at 8000pm for visualisation) ---
inc_year <- usoc %>%
  filter(!is.na(equivinc.capped)) %>%
  group_by(int.year.sched) %>%
  summarise(mean_inc  = mean(equivinc.capped, na.rm = TRUE),
            se_inc    = sd(equivinc.capped, na.rm = TRUE) / sqrt(n()),
            .groups   = "drop") %>%
  mutate(lo = mean_inc - 1.96 * se_inc,
         hi = mean_inc + 1.96 * se_inc)

fig2d <- ggplot(inc_year, aes(x = int.year.sched, y = mean_inc)) +
  geom_ribbon(aes(ymin = lo, ymax = hi), fill = "#AABB99", alpha = 0.4) +
  geom_line(colour = "#446633", linewidth = 1) +
  geom_point(colour = "#446633", size = 2) +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Household income (mean equiv.)",
       x = "Year", y = "£ per month (equivalised)") +
  theme_classic(base_size = 11)

fig2 <- ggarrange(fig2a, fig2b, fig2c, fig2d, ncol = 2, nrow = 2,
                  labels = c("A", "B", "C", "D"))
fig2

eth_year <- usoc %>%
  filter(!is.na(ethnicity)) %>%
  group_by(int.year.sched, ethnicity) %>%
  summarise(n = n_distinct(pidp), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop = n / sum(n))

fig1 <- ggplot(eth_year, aes(x = int.year.sched, y = prop, fill = ethnicity)) +
  geom_area(alpha = 0.85, colour = "white", linewidth = 0.3) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_brewer(palette = "Set2", name = "Ethnicity") +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(x = "Year", y = "% of sample") +
  theme_classic(base_size = 10) +
  theme(legend.text = element_text(size = 9),
        axis.text.x = element_text(angle = 30, hjust = 1))

# weighted by youth weight
eth_weighted1 <- usoc %>%
  filter(!is.na(ethnicity), !is.na(wgt.rescaled), wgt.rescaled > 0) %>%
  group_by(int.year.sched, ethnicity) %>%
  summarise(wgt_n = sum(wgt.rescaled), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop_wtd = wgt_n / sum(wgt_n))

fig2<-ggplot(eth_weighted1, aes(x = int.year.sched, y = prop_wtd, fill = ethnicity)) +
  geom_area(alpha = 0.85, colour = "white", linewidth = 0.3) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_brewer(palette = "Set2", name = "Ethnicity") +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(
       x = "Year", y = "% of sample (weighted)") +
  theme_classic(base_size = 10)+
  theme(legend.text = element_text(size = 9),
        axis.text.x = element_text(angle = 30, hjust = 1))

fig <- ggarrange(fig1, fig2,  ncol = 2, nrow = 1,
                  labels = c("Unweighted", "Weighted"), common.legend = T, legend = "right")
fig <- annotate_figure(fig, top = text_grob("Ethnic diversity over time",
                                               face = "bold", size = 13))
fig

We can see that the % of minority ethnicities “peak” around 2015 and 2023 around the times of IEM sample boosting. The weights seem to adjust for that design-related bias. Even with the weighting, the proportion of ethnic minorities (all others not British/Irish) rise over time, and rise steeply from 2021.

The increase in proportion of ethnic minorities reflects census data in the UK, at least from the late 2011 to 2021: https://www.bbc.co.uk/bitesize/guides/z334nbk/revision/5

2021 was the most recent census. The next UK census will be in 2031.

# 3a. Mean life satisfaction by year 
life_year <- usoc %>%
  filter(!is.na(h.life.c)) %>%
  group_by(int.year.sched) %>%
  summarise(mean_life = mean(h.life.c, na.rm = TRUE),
            se_life   = sd(h.life.c, na.rm = TRUE) / sqrt(n()),
            .groups   = "drop") %>%
  mutate(lo = mean_life - 1.96 * se_life,
         hi = mean_life + 1.96 * se_life)

fig3a <- ggplot(life_year, aes(x = int.year.sched, y = mean_life)) +
  geom_ribbon(aes(ymin = lo, ymax = hi), fill = "#E78AC3", alpha = 0.35) +
  geom_line(colour = "#C6007E", linewidth = 1) +
  geom_point(colour = "#C6007E", size = 2) +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Mean life satisfaction over time",
       x = "Year", y = "Mean h.life.c (centred)") +
  theme_classic(base_size = 11)

# 3b. Stacked proportions of response categories 
# h.life.c is the centred version (-3 to +3); use the raw response for categories
# The raw variable is h.life (1–7 scale) or the centred version with integer values
life_dist <- usoc %>%
  filter(!is.na(h.life.c)) %>%
  mutate(response = factor(h.life.c,
                           levels = sort(unique(h.life.c)),
                           labels = as.character(sort(unique(h.life.c))))) %>%
  group_by(int.year.sched, response) %>%
  summarise(n = n(), .groups = "drop") %>%
  group_by(int.year.sched) %>%
  mutate(prop = n / sum(n))

fig3b <- ggplot(life_dist, aes(x = int.year.sched, y = prop, fill = response)) +
  geom_area(alpha = 0.85, colour = "white", linewidth = 0.2) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_fill_brewer(palette = "RdYlGn", name = "Response\n(–3 to +3)",
                    direction = 1) +
  scale_x_continuous(breaks = seq(2009, 2024, 2), minor_breaks = seq(2010, 2024, 2)) +
  labs(title = "Response distribution over time",
       x = "Year", y = "% of sample") +
  theme_classic(base_size = 11) +
  theme(legend.text = element_text(size = 8))



# 3c. % at lowest response (most unhappy = –3)
# This directly addresses whether floor effects / zero-inflation are growing
life_floor <- usoc %>%
  filter(!is.na(h.life.c)) %>%
  group_by(int.year.sched) %>%
  summarise(prop_floor = mean(h.life.c == min(h.life.c, na.rm = TRUE), na.rm = TRUE),
            .groups    = "drop")

fig3c <- ggplot(life_floor, aes(x = int.year.sched, y = prop_floor)) +
  geom_col(fill = "#AA3333", alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE, colour = "#AA3333",
              linewidth = 0.8, linetype = "dashed") +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "% selecting lowest response (most unhappy)",
       subtitle = "Rising trend suggesting floor-effect growth, not just mean shift",
       x = "Year", y = "% of sample") +
  theme_classic(base_size = 11)

fig3 <- ggarrange(fig3a, fig3b, fig3c, ncol = 2, nrow=2, labels = c("A", "B", "C"))
## `geom_smooth()` using formula = 'y ~ x'
fig3 <- annotate_figure(fig3, top = text_grob(
  "Fig 3. Life satisfaction over time", face = "bold", size = 13))


print(fig3)

#  4. DEFINE ATTRITION & VISUALISE RATE 
# "Attrition" = a participant who was age-eligible for the next wave (age < 15)
# but did NOT appear in that next wave.
# We identify this by checking, for each pidp x wave observation (where age < 15),
# whether that pidp appears in the immediately following wave.


usoc_sorted <- usoc %>%
  arrange(pidp, wave.n)

# For each participant, list of waves they appeared in
wave_presence <- usoc_sorted %>%
  dplyr::select(pidp, wave.n, dvage.n) %>%
  distinct()

# create atttrition indicator at participant-wave level
wave_presence <- wave_presence %>%
  group_by(pidp) %>%
  arrange(wave.n) %>%
  mutate(
    next_wave      = wave.n + 1,
    appeared_next  = next_wave %in% wave.n,         # did they come back?
    age_eligible   = dvage.n < 15,                  # still young enough to return?
    attrited       = age_eligible & !appeared_next  # dropped out while still eligible
  ) %>%
  ungroup()

# Merge back to get year info
wave_presence <- wave_presence %>%
  left_join(usoc %>% 
              dplyr::select(pidp, wave.n, int.year.sched) %>% 
              distinct(), by = c("pidp", "wave.n"))

# attrition rate by yr (among those still age-eligible)
att_year <- wave_presence %>%
  filter(age_eligible) %>%
  group_by(int.year.sched) %>%
  summarise(n_eligible  = n(),
            n_attrited  = sum(attrited, na.rm = TRUE),
            att_rate    = n_attrited / n_eligible,
            .groups     = "drop")

ggplot(att_year[att_year$int.year.sched<2024,], aes(x = int.year.sched, y = att_rate)) +
  geom_col(fill = "#BB5522", alpha = 0.8) +
  geom_text(aes(label = paste0(round(att_rate * 100, 1), "%")), vjust = -0.4, size = 3) +
  scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1)) +
  scale_x_continuous(breaks = seq(2009, 2024, 2)) +
  labs(title = "Year-to-year attrition rate",
       subtitle = "Among participants still eligible (age < 15) in each year",
       x = "Year", y = "Attrition rate") +
  theme_classic(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

# 5. LOGISTIC REGRESSION: PREDICTORS OF ATTRITION

# Predictors: age, sex, ethnicity, income, country, life satisfaction

# create analytic dataset (one row per eligible participant-wave)
model_df <- wave_presence %>%
  filter(age_eligible) %>%
  left_join(
    usoc %>%
      dplyr::select(pidp, wave.n, sex.bin, ethnicity, country, equivinc.capped,
             h.life.c) %>%
      distinct(),
    by = c("pidp", "wave.n")
  ) %>%
  mutate(
    attrited_01 = as.integer(attrited),
    # Scale continuous predictors for comparability
    age_z       = scale(dvage.n)[, 1],
    year_c      = scale(int.year.sched, scale=F), # centre year ~2014
    inc_z       = scale(equivinc.capped)[, 1],
    life_z      = scale(h.life.c)[, 1]
  ) %>%
  filter(!is.na(attrited_01), !is.na(sex.bin), !is.na(ethnicity))

# Two models:
#   mod_base — full sample; life satisfaction is a key predictor alongside demographics
#   mod_int — full sample; added intercation between time and life satisfaction

mod_base <- glm(attrited_01 ~ age_z + sex.bin + ethnicity + country + inc_z + life_z,
                data   = model_df,
                family = binomial(link = "logit"))

mod_int <- glm(attrited_01 ~ age_z + sex.bin + ethnicity + country + inc_z + life_z*year_c,
                data   = model_df,
                family = binomial(link = "logit"))

# print results of main model
cat("\n--- Main model (life satisfaction + demographics) ---\n")
## 
## --- Main model (life satisfaction + demographics) ---
print(summary(mod_base))
## 
## Call:
## glm(formula = attrited_01 ~ age_z + sex.bin + ethnicity + country + 
##     inc_z + life_z, family = binomial(link = "logit"), data = model_df)
## 
## Coefficients:
##                                     Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                         -0.89770    0.01905 -47.125  < 2e-16 ***
## age_z                                0.06453    0.01121   5.759 8.47e-09 ***
## sex.binMale                          0.05657    0.02224   2.543  0.01099 *  
## ethnicityAny other white background  0.37656    0.07854   4.794 1.63e-06 ***
## ethnicitySouth or East Asian         0.38224    0.03360  11.375  < 2e-16 ***
## ethnicityAfrican or Caribbean        0.68029    0.05022  13.547  < 2e-16 ***
## ethnicityMixed background            0.22875    0.04935   4.635 3.56e-06 ***
## ethnicityOther                       0.41224    0.07118   5.791 6.99e-09 ***
## countryWales                         0.13370    0.05070   2.637  0.00836 ** 
## countryScotland                      0.01855    0.04215   0.440  0.65980    
## countryNorthern Ireland              0.04578    0.04813   0.951  0.34158    
## inc_z                                0.06084    0.01114   5.463 4.68e-08 ***
## life_z                              -0.05230    0.01112  -4.703 2.57e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 47211  on 37517  degrees of freedom
## Residual deviance: 46832  on 37505  degrees of freedom
##   (549 observations deleted due to missingness)
## AIC: 46858
## 
## Number of Fisher Scoring iterations: 4
or_base <- clean_labels(extract_ors(mod_base, "Main model"))
or_int <- clean_labels(extract_ors(mod_int, "Interaction model"))

# just plot predictors of attrition from or_base main effects model
ggplot(or_base, aes(x = or, y = reorder(term, or),
                             colour = sig)) +
  geom_vline(xintercept = 1, linetype = "dashed", colour = "grey50") +
  geom_errorbarh(aes(xmin = lo, xmax = hi), height = 0.3, linewidth = 0.7) +
  geom_point(size = 3.5) +
  scale_colour_manual(values = c("p < .05" = "#BB5522", "ns" = "#888888"), name = NULL) +
  scale_shape_manual(values  = c("Life satisfaction" = 18, "Other" = 16), guide = "none") +
  scale_x_log10() +
  labs(title    = "Predictors of attrition (OR, 95% CI)",
       subtitle = paste0("Analytic sample (n = ", nrow(model_df[!is.na(model_df$attrited_01),]), ").\n",
                         "Outcome = dropped out of next wave while still eligible (age < 15).\n",
                         "Continuous predictors standardised. "),
       x = "Odds Ratio (log scale) ±95% CI", y = NULL) +
  theme_classic(base_size = 12) +
  theme(plot.title    = element_text(face = "bold"),
        legend.position = "bottom")

ggplot(or_int, aes(x = or, y = reorder(term, or),
                             colour = sig)) +
  geom_vline(xintercept = 1, linetype = "dashed", colour = "grey50") +
  geom_errorbarh(aes(xmin = lo, xmax = hi), height = 0.3, linewidth = 0.7) +
  geom_point(size = 3.5) +
  scale_colour_manual(values = c("p < .05" = "#BB5522", "ns" = "#888888"), name = NULL) +
  scale_shape_manual(values  = c("Life satisfaction" = 18, "Other" = 16), guide = "none") +
  scale_x_log10() +
  labs(title    = "Predictors of attrition (OR, 95% CI)",
       subtitle = paste0("Analytic sample (n = ", nrow(model_df[!is.na(model_df$attrited_01),]), ").\n",
                         "Outcome = dropped out of next wave while still eligible (age < 15).\n",
                         "Continuous predictors standardised. "),
       x = "Odds Ratio (log scale) ±95% CI", y = NULL) +
  theme_classic(base_size = 12) +
  theme(plot.title    = element_text(face = "bold"),
        legend.position = "bottom")

#  6. LIFE SATISFACTION AS PREDICTOR OF ATTRITION – predicted probabilities
#
# Predicted probability of attrition across the life satisfaction range,
# holding all other predictors constant..

life_vals <- model_df$h.life.c
life_z_seq <- seq(min(scale(life_vals)[,1], na.rm = TRUE),
                  max(scale(life_vals)[,1], na.rm = TRUE), length.out = 100)

newdat_life <- data.frame(
  life_z    = life_z_seq,
  age_z     = 0,
  sex.bin     = factor("Female", levels = c("Male", "Female")),
  ethnicity = levels(usoc$ethnicity)[1],
  country   = levels(factor(model_df$country))[1],
  inc_z     = 0
)

# Get predicted probabilities with proper 95% CI via se.fit
pred_life      <- predict(mod_base, newdata = newdat_life, type = "link", se.fit = TRUE)
newdat_life$pred_prob <- plogis(pred_life$fit)
newdat_life$lo        <- plogis(pred_life$fit - 1.96 * pred_life$se.fit)
newdat_life$hi        <- plogis(pred_life$fit + 1.96 * pred_life$se.fit)

# Back-transform z-score to raw h.life.c for axis
life_mean <- mean(life_vals, na.rm = TRUE)
life_sd   <- sd(life_vals,   na.rm = TRUE)
newdat_life$life_raw <- life_z_seq * life_sd + life_mean

ggplot(newdat_life, aes(x = life_raw, y = pred_prob)) +
  geom_ribbon(aes(ymin = lo, ymax = hi), fill = "#E78AC3", alpha = 0.3) +
  geom_line(colour = "#C6007E", linewidth = 1.2) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_x_continuous(breaks = sort(unique(round(life_vals)))) +
  labs(title    = "Fig 6. Predicted probability of attrition by life satisfaction score",
       subtitle = "Other predictors held at mean/modal value. Shaded = 95% CI.",
       x        = "Life satisfaction (h.life.c, centred)",
       y        = "Predicted probability of attrition") +
  theme_classic(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

# Fig 6. shows those with lower life satisfaction are more likely to attrit. The adjusted difference in probability of attriiton isnt massive though: ~6% difference between those at extreme ends of the satisfaction spectrum.

2. SDQ as alternative to Life satisfaction

Use SDQ total as correlate of satisfaction with aspects of life

sdq.coefs.wgts <- c() # data frame to store B coef and SEs of domain onto SDQ total, for each wave of data


 for (year.x in sort(unique(usoc$int.year.sched)) ){

  # extract cross-sec data for wave x
  xs <- usoc[usoc$int.year.sched==year.x,]

  # exclude participants with missing data on any life satis var OR the SDQ (note: will remove many participants as only done every 2nd wave)
  xs <- xs[!is.na(xs$ypsdqtd_dv) & !is.na(xs$h.school.c) & !is.na(xs$h.schoolwork.c) & !is.na(xs$h.friends.c) & !is.na(xs$h.fam.c) & !is.na(xs$h.appear.c), ]
  
  # set svy design for this wave
  usoc.svy <- svydesign(id=~psu + hidp, 
                        strata=~strata, 
                        weights=~wgt.rescaled, 
                        data=xs,
                        nest=TRUE)
  
  options(survey.lonely.psu = "adjust") # tells the survey package to handle singleton PSUs by scaling their variance estimates
  
  svyglm1 <- svyglm(formula = ypsdqtd_dv ~ h.school.c + h.schoolwork.c + h.friends.c + h.fam.c + h.appear.c,
          design = usoc.svy)
  
  x= as.data.frame(summary(svyglm1)[["coefficients"]])
  
  sdq.coefs.wgts <- rbind(sdq.coefs.wgts, c(as.numeric(coef(svyglm1)), # betas
                                    as.numeric(x[,2])) ) # SEs
 }  



sdq.coefs.wgts <- cbind(sort(unique(usoc$int.year.sched)), as.data.frame(round(sdq.coefs.wgts,4)))

names(sdq.coefs.wgts) <- c("year", "B.intercept", "B.school", "B.schoolwork", "B.friends", "B.family", "B.appearance",
                       "SE.intercept", "SE.school", "SE.schoolwork", "SE.friends", "SE.family", "SE.appearance")


# re-structure to long-format data
long.wgts <- sdq.coefs.wgts %>%
  dplyr::select(year, B.school, B.schoolwork, B.friends, B.family, B.appearance)%>%
  gather(key = "domain", value="Beta", -year)
long.se <- sdq.coefs.wgts %>%
  dplyr::select(year, SE.school, SE.schoolwork, SE.friends, SE.family, SE.appearance)%>%
  gather(key = "domain", value="SE", -year)

sdq.wgts.long <- cbind(long.wgts, long.se[,"SE"])
names(sdq.wgts.long)[4] <- "SE"
sdq.wgts.long$year.n <- as.numeric(sdq.wgts.long$year)-2009

sdq.wgts.long$`Happiness with...` <- factor(sdq.wgts.long$domain, 
                                   levels=c( "B.family" , "B.friends", 
                                             "B.school", "B.schoolwork",
                                             "B.appearance" ),
                                   labels=c( "Family" , "Friends", 
                                             "School", "Schoolwork",
                                             "Appearance" ))
# save meta-regression output here:
sdq.wgt.avgs <- c()
sdq.wgt.trends  <- c()
#wgt.trends.5ywin  <- c()

library(metafor)

for (domain in levels(as.factor(sdq.wgts.long$domain))) {

  dat <- sdq.wgts.long[sdq.wgts.long$domain==domain, ]
  
  D <- diag(dat$SE, 16, 16)
  V <- D %*% R %*% D
  
  # intercept-only M.A: What is the pooled mean across all my waves?
  rma.ave <- rma.mv(yi = Beta,
                    V = V,
                    data=dat) 
       
        # save
        sdq.wgt.avgs <- rbind(sdq.wgt.avgs, data.frame(
                                      domain = domain,
                                      beta = as.numeric(rma.ave[["beta"]]), 
                                      se = as.numeric(rma.ave[["se"]]),
                                      ci.low = as.numeric(rma.ave[["ci.lb"]]), 
                                      ci.hi = as.numeric(rma.ave[["ci.ub"]])) )
  
  # include time as moderator...
  # full time period (waves 1-14)
  rma.trend.full <- rma.mv(yi = Beta,
                           V = V,
                           mods = ~ year.n,
                           data = dat)
         # save
         sdq.wgt.trends <- rbind(sdq.wgt.trends, data.frame(
                                           domain=domain,
                                           beta=as.numeric(rma.trend.full[["beta"]][2]),
                                           se=as.numeric(rma.trend.full[["se"]][2]),
                                           ci.low=as.numeric(rma.trend.full[["ci.lb"]][2]),
                                           ci.hi=as.numeric(rma.trend.full[["ci.ub"]][2]),
                                           p=round(as.numeric(rma.trend.full[["pval"]][2]),4)))
         
    # Note: no analysis within 5-year windows. There's so much year-to-year fluctuation with this data, I don't think we should be looking at within-5 year changes. Perhaps we could show *average* associations within each 5-year window, to average out some of those fluctuations.

}

sdq.wgt.trends[,c(2:6)] <- round(sdq.wgt.trends[,c(2:6)], 3)
sdq.wgt.trends
##         domain   beta    se ci.low  ci.hi     p
## 1 B.appearance -0.053 0.011 -0.075 -0.031 0.000
## 2     B.family -0.049 0.016 -0.080 -0.018 0.002
## 3    B.friends  0.021 0.016 -0.011  0.053 0.196
## 4     B.school -0.001 0.011 -0.023  0.021 0.936
## 5 B.schoolwork  0.008 0.012 -0.016  0.032 0.514
# bar chart
ggplot(data=sdq.wgts.long, aes(x=year, y=Beta))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.5)+
  geom_errorbar(aes(ymin=Beta-SE, ymax=Beta+SE, colour=`Happiness with...`), width=0)+
  geom_smooth(aes(x=year, y=Beta), method="lm", formula = y~x, colour="black", se=T, size=0.8)+
  geom_smooth(aes(x=year, y=Beta), method="loess",  colour="grey50", se=F, size=0.8)+
  labs(y="Adjusted association with SDQ total (B ± 1 
       SE)\n", x="\nSurvey year")+
  facet_grid(~`Happiness with...`)+
  scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  coord_cartesian(ylim=c(-2.0, 0))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, angle=60, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank(),
        legend.position="none")

# lots of year-to-year fluctuation-- what if we averaged in 5-year segments?
sdq.wgts.long$windows <-  cut(sdq.wgts.long$year, breaks=c(2009, 2014, 2019, 2025), labels=c(1, 2, 3))

  
# plot a bar for each 5-yr window
 sdq.wgts.long.win <- sdq.wgts.long %>%
  group_by(windows, `Happiness with...`) %>%
  summarise(m=mean(Beta),
            se=mean(SE)) # questionable average of SE
 
ggplot(data=sdq.wgts.long.win[!is.na(sdq.wgts.long.win$windows),], aes(x=windows, y=m))+
  geom_col(aes(fill=`Happiness with...`), alpha=0.5)+
  geom_errorbar(aes(ymin=m-se, ymax=m+se, colour=`Happiness with...`), width=0)+
  labs(y="Adjusted association with SDQ total (B ± 1 SE)\n", x="\n5-year Windows")+
  facet_grid(~`Happiness with...`)+
  #scale_x_continuous(breaks= seq(2010,2024,2),  limits = c(2008,2025), name = "")+
  coord_cartesian(ylim=c(-2.0, 0))+
  scale_fill_manual(values=cols5)+
  scale_colour_manual(values=cols5)+
  theme_minimal()+
  theme(axis.text.x = element_text(size=8, hjust=1, vjust = 1),
        #panel.grid.major.x = element_blank(),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank(),
        legend.position="none")

3. Removing the COVID years from analyses