From 7fffdb54b433723d399dd8af83dcce5e66d7b17b Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:12:20 -0400 Subject: [PATCH 01/29] Task 1.7a: mortality method 'none' by default; no-op corrections fail The 'peto' option returned weights unchanged while being described as a correction, and was the default. It is replaced by three explicit methods: 'none' (no correction; the data carry a mortality_correction attribute and an estimand note saying the results describe respondents who survived to be surveyed), 'mport' (primary, stops as not implemented, task 1.7b), and 'peto' (sensitivity, stops as not implemented). A configured correction that leaves every weight unchanged now stops the pipeline (assert_correction_applied), so a stub can no longer pass as a correction. README, workflow and reference docs, the rate-table schema, and config comments now state that no correction is applied yet and that outputs are estimates among survivors (protocol section 3.4.5). Tests cover all four paths and the guard. --- R/apc-model.R | 79 +++++++++++++++++++----- README.md | 2 +- config.yml | 21 ++++--- docs/development/pipeline-progress.md | 2 +- docs/development/protocol-todo.md | 2 +- docs/reference/variables.qmd | 6 +- docs/workflow/7-apc-data-preparation.qmd | 4 +- docs/workflow/8-apc-model.qmd | 4 +- schemas/cshm-rate-tables.yaml | 11 ++-- tests/testthat/test-apc-data.R | 34 +++++++++- 10 files changed, 125 insertions(+), 40 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index 944dca8..58fa55f 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -22,8 +22,9 @@ #' #' Builds long-format person-year data frames for smoking initiation (by sex) #' and cessation (by sex). Each row is either a transition event (event = 1) -#' or an at-risk person-year (event = 0). Applies mortality survival correction -#' via cfg$apc$mortality_method. +#' or an at-risk person-year (event = 0). Applies the mortality survival +#' correction selected by cfg$apc$mortality_method ("none" until MPoRT is +#' implemented; see apply_survival_correction()). #' #' @param analysis_data Output of impute_data() #' @param cfg Config object from config::get() @@ -309,32 +310,78 @@ build_cessation_data <- function(data, cfg) { } -#' Apply mortality survival correction to APC dataset +#' Apply the mortality survival correction to an APC dataset #' -#' Dispatches on cfg$apc$mortality_method. -#' "peto" — weight unchanged (Peto approximation, weight × 1.0) -#' "mport" — not yet implemented +#' Dispatches on `cfg$apc$mortality_method` (protocol section 3.4.5): +#' "none" -- no correction. Weights stay as survey weights and the result is +#' labelled so downstream outputs are reported as estimates among +#' respondents who survived to be surveyed, not as birth-cohort +#' smoking histories. +#' "mport" -- MPoRT survival-bias adjustment (primary method; not yet +#' implemented, remediation task 1.7b). +#' "peto" -- constant mortality risk ratio by smoking status (sensitivity +#' analysis; not yet implemented). #' -#' @param apc_data Data frame with weight column +#' A method other than "none" must change the weights. If it leaves every weight +#' unchanged the function stops, so a no-op can never be mistaken for a +#' correction (this is what happened when "peto" was a stub). +#' +#' @param apc_data Data frame with a `weight` column #' @param cfg Config object -#' @return apc_data with weight column adjusted +#' @return `apc_data` with the `weight` column adjusted and the attribute +#' `mortality_correction` set to the method applied apply_survival_correction <- function(apc_data, cfg) { method <- cfg$apc$mortality_method + valid <- c("none", "mport", "peto") + if (!is.character(method) || length(method) != 1 || !method %in% valid) { + stop( + "Unknown mortality_method: '", paste(method, collapse = ","), + "'. Expected one of: ", paste(valid, collapse = ", "), "." + ) + } - if (method == "peto") { - # Peto stub: weights unchanged + if (method == "none") { + attr(apc_data, "mortality_correction") <- "none" + attr(apc_data, "estimand_note") <- paste( + "No mortality correction applied: estimates describe respondents who", + "survived to be surveyed (protocol section 3.4.5)." + ) return(apc_data) } - if (method == "mport") { + corrected <- switch(method, + mport = stop( + "MPoRT mortality correction is not yet implemented (remediation task 1.7b). ", + "Set cfg$apc$mortality_method = 'none' and report results as estimates ", + "among survivors." + ), + peto = stop( + "Peto constant-risk-ratio correction is not yet implemented (sensitivity ", + "analysis). Set cfg$apc$mortality_method = 'none' and report results as ", + "estimates among survivors." + ) + ) + + assert_correction_applied(apc_data, corrected, method) + attr(corrected, "mortality_correction") <- method + corrected +} + + +#' Guard: a configured mortality correction must change the weights +#' +#' @param before,after Data frames with a `weight` column +#' @param method The method name, for the error message +#' @return `invisible(TRUE)`; stops if every weight is unchanged +assert_correction_applied <- function(before, after, method) { + if (isTRUE(all.equal(before$weight, after$weight))) { stop( - "MPoRT mortality correction not yet implemented. ", - "Set cfg$apc$mortality_method = 'peto' for current pipeline runs. ", - "See protocol-todo.md issue #4 for interaction with WTS_M." + "mortality_method = '", method, "' left every weight unchanged. ", + "A configured correction must change the weights; use 'none' to run ", + "without a correction." ) } - - stop("Unknown mortality_method: '", method, "'. Expected 'peto' or 'mport'.") + invisible(TRUE) } diff --git a/README.md b/README.md index ac6754a..26072c1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Period effects are held constant beyond the observed data range: ### Mortality adjustment -Ever-smokers have lower survival to survey date than never-smokers. Survival bias is corrected using MPoRT weights adjusted for age, smoking status, years since quitting, immigration, and sex. A sensitivity analysis uses the Peto constant mortality risk ratio, consistent with the original Holford et al. (2014) US implementation. +Ever-smokers have lower survival to survey date than never-smokers, so smoking histories reconstructed from survivors under-represent smokers who died. The protocol specifies an MPoRT-based correction as the primary method and a Peto constant risk ratio as a sensitivity analysis (protocol section 3.4.5). **Neither is implemented yet.** The pipeline runs with `mortality_method: "none"`, and every current output is an estimate among respondents who survived to be surveyed, not a birth-cohort smoking history. A configured correction that leaves the weights unchanged stops the pipeline. ### Smoking status definitions diff --git a/config.yml b/config.yml index 78bc39c..aaa0267 100644 --- a/config.yml +++ b/config.yml @@ -242,11 +242,13 @@ default: # Sensitivity: "rcs" (restricted cubic splines via rms::rcs()) spline_library: "splines2" spline_type: "nsp" - # Mortality adjustment method - # Primary: "mport" (MPoRT algorithm — see protocol §3.4.4) - # Sensitivity: "peto" (Peto constant risk ratio, consistent with Holford et al. 2014) - # NOTE: mport not yet implemented; peto used during development. - mortality_method: "peto" + # Mortality adjustment method (protocol section 3.4.5) + # "none" -- no correction; outputs are estimates among respondents who + # survived to be surveyed, and are labelled as such + # "mport" -- MPoRT survival-bias adjustment (primary; not yet implemented) + # "peto" -- constant risk ratio by smoking status (sensitivity; not yet implemented) + # A method other than "none" must change the weights or the pipeline stops. + mortality_method: "none" # Subgroup structure for model stratification # Subgroup stratification uses config keys (resolved via survey_var() at runtime) subgroups: @@ -268,11 +270,14 @@ default: alternative: "rcs" mortality_method: description: > - Test Peto constant mortality risk ratio versus the primary MPoRT adjustment. - Peto is consistent with the original Holford et al. (2014) US implementation. - Override: cfg$apc$mortality_method <- "peto" + Test the Peto constant mortality risk ratio versus the primary MPoRT + adjustment. Peto is consistent with the original Holford et al. (2014) US + implementation. Neither is implemented yet (remediation task 1.7b); the + pipeline currently runs with "none" and labels outputs as estimates among + survivors. Override once implemented: cfg$apc$mortality_method <- "peto" primary: "mport" alternative: "peto" + current: "none" period_constraints: description: > Test alternative period constraint years (e.g. extending to 2003/2007 for diff --git a/docs/development/pipeline-progress.md b/docs/development/pipeline-progress.md index a312f8a..0f1678b 100644 --- a/docs/development/pipeline-progress.md +++ b/docs/development/pipeline-progress.md @@ -102,7 +102,7 @@ Table 1b (post-imputation): from `analysis_data`. - **Ontario flag:** Dropped. National model; no `ont_id`. - **Cessation:** Former daily smokers only (SMKDSTY 3 and 4). Former occasional smokers excluded — see GH#1. -- **Survival correction:** Peto stub (weight × 1.0). MPoRT stubbed with `stop()`. +- **Survival correction:** none (labelled as estimates among survivors); MPoRT and Peto stop as not implemented; no-op guard (task 1.7a, 2026-08-27). - **Spline library:** `splines2::nsp()`. RCS as config-selectable sensitivity. Added to `config.yml`. - **Period/cohort constraints:** Applied as clamp before spline basis construction (data-side, not model-side). Constraint years are sex- and model-type-specific per `config.yml`. diff --git a/docs/development/protocol-todo.md b/docs/development/protocol-todo.md index 06851ba..5922165 100644 --- a/docs/development/protocol-todo.md +++ b/docs/development/protocol-todo.md @@ -56,7 +56,7 @@ create a double-adjustment. The Peto method (weight × 1.0) avoids this issue. **Fix required:** Clarify MPoRT weight adjustment mechanism before enabling `mortality_method: "mport"`. Document the interaction in the protocol (§3.4.3). -**Status:** MPoRT stub raises `stop()`. Peto is the default. Revisit when MPoRT is implemented. +**Status (2026-08-27):** `mortality_method` defaults to `"none"`; `mport` and `peto` both stop as not implemented; a correction that changes no weights stops (task 1.7a). Estimator derivation is task 1.7b. --- diff --git a/docs/reference/variables.qmd b/docs/reference/variables.qmd index 5e46c7c..ecfa81e 100644 --- a/docs/reference/variables.qmd +++ b/docs/reference/variables.qmd @@ -300,8 +300,8 @@ for province code definitions. **Cycles:** 2001–2022 CCHS sampling and post-stratification weight. Applied during APC model fitting. -See protocol §3.4.4 for the weighting method used (Peto / case weight in logistic -regression vs. `svyglm()`). +See protocol section 3.4.5 for how the survey weight combines with the mortality +adjustment, and section 3.5 for variance estimation. --- @@ -475,7 +475,7 @@ initiation × {male, female} and cessation × {male, female}. | `rate` | Float (0–1) | Conditional probability of the smoking transition | | `rate_lower` | Float (0–1) | Lower bound of 95% confidence interval | | `rate_upper` | Float (0–1) | Upper bound of 95% confidence interval | -| `mortality_correction` | String | Method applied: `peto` (default) or `mport` (not yet implemented) | +| `mortality_correction` | String | Method applied: `none` (current default; rates are estimates among survivors), `mport` or `peto` (not yet implemented) | ### Rate definitions diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index bef5fdd..edeadab 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -75,7 +75,7 @@ Each data frame contains: | `period` | Calendar year | | `cohort` | Birth year (`period - age`) | | `init` | Event indicator (1 = initiated/quit, 0 = at risk but no event) | -| `weighting` | Survey weight × mortality correction | +| `weighting` | Survey weight; multiplied by the mortality correction once one is implemented (currently none) | | Natural spline columns | Spline basis for age, period, cohort effects | ## Key decisions @@ -84,7 +84,7 @@ Each data frame contains: **Retrospective history construction.** A current smoker at age 45 in survey year 2014 is recorded as a person-year at risk of cessation at every age from their initiation age to 45. This reconstruction assumes respondents accurately recall their age of initiation and cessation (supported by CCHS validation studies). -**Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. The `weighting` column incorporates a mortality correction. The primary method is MPoRT (not yet implemented); during development, the Peto constant risk ratio is used as a fallback (see `cfg$apc$mortality_method`). +**Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weighting` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). **APC floor ages.** Initiation probability is assumed zero before `survey_bound(cfg, "age_first_cigarette", "min")` (PUMF: 13, Master: 8). Cessation probability is assumed zero before `survey_bound(cfg, "years_since_quit", "min")` (0). In PUMF data, the practical floor for observed initiation is ~13 due to midpoint imputation of grouped categories. The Master analytical floor of 8 captures genuine early initiations. diff --git a/docs/workflow/8-apc-model.qmd b/docs/workflow/8-apc-model.qmd index cb582eb..e706f21 100644 --- a/docs/workflow/8-apc-model.qmd +++ b/docs/workflow/8-apc-model.qmd @@ -32,7 +32,7 @@ Configuration used: | `apc$cohort_constraints$cessation_from` | `1985` | Cohort effect constant from this year | | `apc$spline_library` | `"splines2"` | Primary: `splines2::nsp()` | | `apc$spline_type` | `"nsp"` | Natural spline type | -| `apc$mortality_method` | `"peto"` | Current: Peto (development); primary: MPoRT | +| `apc$mortality_method` | `"none"` | Current: none (outputs labelled as estimates among survivors); primary: MPoRT, not yet implemented | ## Code @@ -84,7 +84,7 @@ cat("Cessation women AIC: ", AIC(model_cess_women), "\n") **Cohort constraint.** The initiation cohort effect is held constant prior to 1920 (small sample, sparse data). The cessation cohort effect is held constant from 1985 forward (insufficient follow-up time for younger cohorts to accumulate cessation history). -**Mortality method: Peto (current).** The primary analysis specifies MPoRT mortality correction. MPoRT is not yet implemented; the Peto constant risk ratio is used during development. A sensitivity analysis compares Peto and MPoRT results (`cfg$apc$mortality_method`). +**Mortality method: none (current).** The protocol specifies MPoRT as the primary correction and the Peto constant risk ratio as a sensitivity analysis. Neither is implemented yet, so the pipeline runs with `mortality_method: "none"` and labels its outputs as estimates among respondents who survived to be surveyed. Selecting `"mport"` or `"peto"` stops with a not-implemented error, and a correction that leaves every weight unchanged also stops -- a no-op can no longer pass as a correction. ## Sensitivity analyses diff --git a/schemas/cshm-rate-tables.yaml b/schemas/cshm-rate-tables.yaml index 4043954..5268098 100644 --- a/schemas/cshm-rate-tables.yaml +++ b/schemas/cshm-rate-tables.yaml @@ -162,10 +162,13 @@ slots: mortality_correction: description: >- - Mortality correction method applied before model fitting. - "peto" — Peto method (survival weight proportional to 1/survival). - "mport" — MPoRT weights adjusted for age, smoking status, years since - quitting, immigration, and sex (not yet implemented). + Mortality correction method applied. + "none" -- no correction; the rates are estimates among respondents who + survived to be surveyed (current default). + "mport" -- MPoRT survival-bias adjustment by age, sex, smoking status, + years since quitting, and immigration (primary; not yet implemented). + "peto" -- constant mortality risk ratio by smoking status (sensitivity + analysis; not yet implemented). range: string # --------------------------------------------------------------------------- diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index 161a2aa..d0dc392 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -107,14 +107,44 @@ test_that("no missing weight in any output element", { expect_false(anyNA(result_cess$weight)) }) -test_that("apply_survival_correction: peto returns data unchanged", { +test_that("apply_survival_correction: none leaves weights unchanged and labels the data", { cfg <- config::get() + cfg$apc$mortality_method <- "none" df <- data.frame(age = 1:5, cohort = 1970:1974, period = 1985:1989, - event = c(1,0,0,1,0), weight = c(100, 200, 150, 300, 250)) + event = c(1, 0, 0, 1, 0), weight = c(100, 200, 150, 300, 250)) result <- apply_survival_correction(df, cfg) expect_equal(result$weight, df$weight) + expect_identical(attr(result, "mortality_correction"), "none") + expect_match(attr(result, "estimand_note"), "survived to be surveyed") +}) + +test_that("apply_survival_correction: peto raises not-implemented error", { + cfg <- config::get() + cfg$apc$mortality_method <- "peto" + + df <- data.frame(age = 1, cohort = 1970, period = 1985, event = 0, weight = 100) + expect_error(apply_survival_correction(df, cfg), "not yet implemented") +}) + +test_that("apply_survival_correction: unknown method is an error", { + cfg <- config::get() + cfg$apc$mortality_method <- "no-such-method" + + df <- data.frame(age = 1, cohort = 1970, period = 1985, event = 0, weight = 100) + expect_error(apply_survival_correction(df, cfg), "Unknown mortality_method") +}) + +test_that("assert_correction_applied: a correction that changes no weights fails", { + before <- data.frame(weight = c(100, 200, 150)) + expect_error( + assert_correction_applied(before, before, "mport"), + "left every weight unchanged" + ) + after <- before + after$weight <- after$weight * c(1.1, 1.3, 1.2) + expect_true(assert_correction_applied(before, after, "mport")) }) test_that("apply_survival_correction: mport raises not-implemented error", { From f42aedf48f8194dd86fb7634614d1dcc67e1ec63 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:17:13 -0400 Subject: [PATCH 02/29] Task 1.0: smoking-state and transition analysis specification Operational specification implementing protocol v0.4.0 section 3.4.1 and adjudication A1: state definitions with their CCHS variables, the two modelled transitions, event-time conventions (one-year steps, one spell per person, cessation risk from first cigarette, two-year durability, recent quitters current and censored), target population and immigration entry, and the output contract with the smoking-history generator. Records one open decision for the PI: the same-age initiation/cessation rule (proposed: one-year spell as primary, exclusion as sensitivity). Names the code consequence that the cessation model must move from the ever-daily universe and time_quit_smoking_daily to all ever-smokers and time_quit_smoking_complete. --- docs/development/estimand-specification.md | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/development/estimand-specification.md diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md new file mode 100644 index 0000000..2fb8de6 --- /dev/null +++ b/docs/development/estimand-specification.md @@ -0,0 +1,78 @@ +# Smoking states and transitions: analysis specification + +**Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). +**Status:** draft, 2026-08-27. Items marked **[decision]** need the PI's ratification before model fitting; everything else restates the protocol in operational terms. + +## 1. Why this document exists + +The pipeline built its initiation model on one definition of a smoker (anyone who has smoked a whole cigarette) and its cessation model on another (people who have smoked daily). The two rate tables therefore described different populations. This note fixes one set of definitions and states, for each, which CCHS variable carries it. Tasks 1.2, 1.3, 1.9, 1.8c, and 1.7b implement against this note; they do not define their own. + +## 2. States + +Each person is in exactly one state at each age. + +| State | Definition | How it is observed at survey | +|---|---|---| +| Never | Has never smoked a whole cigarette | `SMKDSTY_original` = never smoked | +| Current | Has smoked a whole cigarette and has not stopped smoking completely (daily or occasional) | `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | +| Former | Smoked a whole cigarette in the past and has stopped smoking completely | `SMKDSTY_original` = former daily or former occasional | + +Two points follow from the definitions. Stopping daily smoking while still smoking occasionally is not a transition; the person stays current. How much a current smoker smokes, and whether they smoke daily, are characteristics of the current state (protocol section 3.4.4), not states of their own. + +## 3. Transitions modelled + +| Transition | From | To | Event age | CCHS variable (cchsflow v3) | +|---|---|---|---|---| +| Initiation | Never | Current | Age at first whole cigarette | `age_first_cigarette` | +| Cessation | Current | Former | Age at which the person stopped smoking completely | survey age minus `time_quit_smoking_complete` | + +Not modelled as transitions: relapse (Former back to Current) and daily onset (Current to a daily sub-state). The CCHS records one quit per person, so relapse cannot be estimated from it; its influence is a sensitivity analysis (protocol section 3.5). Daily onset (`age_start_smoking`) is retained as a characteristic of current smokers and for the intensity model. + +**Consequence for the code.** The cessation model currently uses the ever-daily universe and `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the universe is all ever-smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. + +## 4. Event-time conventions + +- **Time step.** One year. Ages are whole years; each person contributes one row per age at which they are at risk of the transition being modelled. +- **One spell per person.** A person enters Current once and leaves it at most once. +- **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. +- **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. +- **Durable cessation.** The primary definition of cessation is a quit that has lasted at least two years at the survey. A person who quit less than two years before the survey is a current smoker at the survey; in the cessation model they contribute person-years up to the reported quit age and are then censored, with no event. +- **Same-age initiation and cessation [decision].** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Proposed primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* exclude same-age spells from the cessation model (treat the person as never having established smoking). The choice matters little for the rate tables but must be fixed before fitting. +- **Reported ages that cannot be right** (initiation after survey age, cessation before initiation) are treated as missing and enter the imputation procedure (task 1.8c). No person is silently reclassified. + +## 5. Target population and risk-set entry + +- The Canadian household population covered by the CCHS, aged 12 and over at survey (the study analyses respondents aged 18 and over; `age_exclusion_min`). +- **Immigration.** Person-years before immigration are excluded (protocol section 3.3). In the Master files the year of immigration is exact. In the PUMF only immigrant status and, in some cycles, a grouped time-since-immigration variable are available; task 1.9 specifies the PUMF approximation and documents its error. +- A person's risk set for either transition begins at the later of the state-entry age (section 4) and the age at entry into the Canadian population. + +## 6. Output contract with the smoking-history generator + +The generator (shg-rcpp) consumes the rate tables and produces, for each simulated person, a state at each age. The contract: + +- Rate tables carry, per `model_type` (initiation, cessation), `sex`, `province`, `age`, `period`, and `cohort`: `rate`, `rate_lower`, `rate_upper`, and `mortality_correction` (`schemas/cshm-rate-tables.yaml`). `rate` is the annual probability of the transition among people in the source state at that age. +- The generator applies initiation to Never and cessation to Current. Former is absorbing (no relapse). At every age each simulated person is in exactly one of the three states. +- Intensity (cigarettes per day) and daily status are attributes attached to Current, drawn from the intensity model; they never change a person's state. +- `mortality_correction = "none"` means the rates describe respondents who survived to be surveyed. The generator must carry that label through to its outputs until a correction is implemented (task 1.7b). + +## 7. Decisions recorded here + +| Item | Decision | Source | +|---|---|---| +| State model | Established-smoking model: Never, Current, Former | Adjudication A1, 2026-08-07 | +| Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | +| Exit event | Stopped smoking completely (`time_quit_smoking_complete`) | A1; protocol 3.4.1 | +| Durability | Two years; more recent quitters are current at survey | Protocol 3.4.1 | +| Same-age rule | One-year spell (primary); exclusion (sensitivity) | **[decision]** proposed here | +| Relapse | Not modelled; sensitivity analysis | Protocol 3.4.1 | +| Immigration entry | Excluded before immigration; PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1 | + +## 8. What changes in the pipeline because of this note + +- [ ] 1.3: cessation universe = ever-smokers; exit variable = `time_quit_smoking_complete`; clock from `age_first_cigarette`; recent-quitter censoring; same-age rule. +- [ ] Config: replace the `years_since_quit` mapping (`time_quit_smoking_daily`) with the complete-cessation variable; keep `age_start_daily` for the intensity model only. +- [ ] Worksheets: roles for `time_quit_smoking_complete` (apc-numerator, apc-denominator); `age_start_smoking` loses its cessation role. +- [ ] 1.2: initiation window aligned to the entry event above. +- [ ] 1.9: immigration entry floor. +- [ ] 1.8c: imputation universes follow section 2 (state gates first). +- [ ] Generator contract (section 6) added to the rate-table schema description. From d053cff22107afc924f6b1d310bbf3357b86be49 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:23:32 -0400 Subject: [PATCH 03/29] Task 1.0: same-age rule ratified (one-year spell primary; exclusion sensitivity); add count diagnostic --- docs/development/estimand-specification.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 2fb8de6..a635e31 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -1,7 +1,7 @@ # Smoking states and transitions: analysis specification **Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). -**Status:** draft, 2026-08-27. Items marked **[decision]** need the PI's ratification before model fitting; everything else restates the protocol in operational terms. +**Status:** ratified 2026-08-27 (PI). The same-age rule, the one item that was open, is decided below; everything else restates the protocol in operational terms. ## 1. Why this document exists @@ -37,7 +37,7 @@ Not modelled as transitions: relapse (Former back to Current) and daily onset (C - **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. - **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. - **Durable cessation.** The primary definition of cessation is a quit that has lasted at least two years at the survey. A person who quit less than two years before the survey is a current smoker at the survey; in the cessation model they contribute person-years up to the reported quit age and are then censored, with no event. -- **Same-age initiation and cessation [decision].** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Proposed primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* exclude same-age spells from the cessation model (treat the person as never having established smoking). The choice matters little for the rate tables but must be fixed before fitting. +- **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* exclude same-age spells from the cessation model (treat the person as never having established smoking). Few respondents, possibly none, are expected to meet this condition; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. - **Reported ages that cannot be right** (initiation after survey age, cessation before initiation) are treated as missing and enter the imputation procedure (task 1.8c). No person is silently reclassified. ## 5. Target population and risk-set entry @@ -63,7 +63,7 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat | Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | | Exit event | Stopped smoking completely (`time_quit_smoking_complete`) | A1; protocol 3.4.1 | | Durability | Two years; more recent quitters are current at survey | Protocol 3.4.1 | -| Same-age rule | One-year spell (primary); exclusion (sensitivity) | **[decision]** proposed here | +| Same-age rule | One-year spell (primary); exclusion (sensitivity) | PI decision, 2026-08-27 | | Relapse | Not modelled; sensitivity analysis | Protocol 3.4.1 | | Immigration entry | Excluded before immigration; PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1 | @@ -72,6 +72,7 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat - [ ] 1.3: cessation universe = ever-smokers; exit variable = `time_quit_smoking_complete`; clock from `age_first_cigarette`; recent-quitter censoring; same-age rule. - [ ] Config: replace the `years_since_quit` mapping (`time_quit_smoking_daily`) with the complete-cessation variable; keep `age_start_daily` for the intensity model only. - [ ] Worksheets: roles for `time_quit_smoking_complete` (apc-numerator, apc-denominator); `age_start_smoking` loses its cessation role. +- [ ] 1.3 diagnostic: report the unweighted and weighted number of same-age spells per cycle. - [ ] 1.2: initiation window aligned to the entry event above. - [ ] 1.9: immigration entry floor. - [ ] 1.8c: imputation universes follow section 2 (state gates first). From b58ed304f70259a998ef49f4c41b3d49550ebd71 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:53:01 -0400 Subject: [PATCH 04/29] Task 1.7a: carry the survivor label into fitted models; structural no-op guard; fix stale docs Review findings on PR #4. fit_apc_model() now copies the mortality_correction and estimand_note attributes from the APC dataset onto the fitted model, with a test, so Stage 8 outputs cannot pass as corrected. assert_correction_applied() checks that a correction changed only the weights: same row count, identical age/period/cohort/event in the same order, finite positive weights, and at least one weight changed; tests cover dropped rows, reordering, and non-finite weights. apc-plan.md is marked superseded in part and its two stale 'peto' passages updated. The Stage 7 workflow page now lists the real columns (event, weight), notes that spline columns are built in Stage 8, and documents the two attributes. --- R/apc-model.R | 33 ++++++++++++- docs/development/apc-plan.md | 8 ++-- docs/workflow/7-apc-data-preparation.qmd | 7 +-- tests/testthat/test-apc-data.R | 59 ++++++++++++++++++------ 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index 58fa55f..427fd6f 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -368,12 +368,37 @@ apply_survival_correction <- function(apc_data, cfg) { } -#' Guard: a configured mortality correction must change the weights +#' Guard: a configured mortality correction must change the weights and nothing else +#' +#' Checks that `after` is the same person-year table as `before` -- same rows, in +#' the same order, with the same `age`, `period`, `cohort`, and `event` values -- +#' that every weight is finite and positive, and that at least one weight changed. #' #' @param before,after Data frames with a `weight` column #' @param method The method name, for the error message -#' @return `invisible(TRUE)`; stops if every weight is unchanged +#' @return `invisible(TRUE)`; stops on any violation assert_correction_applied <- function(before, after, method) { + if (!"weight" %in% names(before) || !"weight" %in% names(after)) { + stop("mortality_method = '", method, "': both datasets must have a weight column.") + } + if (nrow(before) != nrow(after)) { + stop( + "mortality_method = '", method, "' changed the number of rows (", + nrow(before), " -> ", nrow(after), "). A correction may only change weights." + ) + } + keys <- intersect(c("age", "period", "cohort", "event"), names(before)) + for (k in keys) { + if (!identical(before[[k]], after[[k]])) { + stop( + "mortality_method = '", method, "' changed or reordered column '", k, + "'. A correction may only change weights." + ) + } + } + if (any(!is.finite(after$weight)) || any(after$weight <= 0)) { + stop("mortality_method = '", method, "' produced non-finite or non-positive weights.") + } if (isTRUE(all.equal(before$weight, after$weight))) { stop( "mortality_method = '", method, "' left every weight unchanged. ", @@ -419,6 +444,10 @@ fit_apc_model <- function(apc_dataset, model_type, sex, cfg) { attr(fit, "model_type") <- model_type attr(fit, "spline_type") <- cfg$apc$spline_type attr(fit, "sex") <- sex + # Carry the mortality-correction label and estimand note from the APC dataset + # so Stage 8 outputs cannot be mistaken for corrected models. + attr(fit, "mortality_correction") <- attr(apc_dataset, "mortality_correction") %||% cfg$apc$mortality_method + attr(fit, "estimand_note") <- attr(apc_dataset, "estimand_note") fit } diff --git a/docs/development/apc-plan.md b/docs/development/apc-plan.md index cf955f9..942b0d0 100644 --- a/docs/development/apc-plan.md +++ b/docs/development/apc-plan.md @@ -1,6 +1,6 @@ # APC implementation plan: Stages 7 and 8 -**Status:** Ready for implementation (all design issues resolved) +**Status:** Superseded in part (2026-08-27). This plan guided the first implementation; the remediation plan (revision 3) and the estimand specification (`estimand-specification.md`) now govern Phase 1 changes. Where this document and those disagree, they win. Known divergences: the mortality method defaults to `"none"` and a no-op correction is an error (task 1.7a); the cessation universe and exit variable change under task 1.3. **Covers:** Stage 7 (`apc_data`) and Stage 8 (`apc_models`) **Source references:** - `resources/legacy-code/Modeling2013.sas` (gitignored; SAS macros `%holford_init`, `%holford_cess`) @@ -215,8 +215,8 @@ prepare_apc_data(analysis_data, cfg) │ └── apply_survival_correction(data, cfg) # Dispatches on cfg$apc$mortality_method - # "peto": weight unchanged (multiply by 1.0) - # "mport": stop("MPoRT correction not yet implemented") + # "none": no correction; data labelled as estimates among survivors (current default) + # "mport", "peto": stop("not yet implemented"); a no-op correction is an error ``` **Columns in each output data frame:** @@ -370,7 +370,7 @@ Add to `config.yml` default profile (under `apc:` and at top level): spline_type: "nsp" # sensitivity analysis: "rcs" # Mortality correction - mortality_method: "peto" # primary; sensitivity: "mport" + mortality_method: "none" # current default; primary "mport" and sensitivity "peto" not yet implemented # Subgroups subgroups: diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index edeadab..cafdd49 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -74,9 +74,10 @@ Each data frame contains: | `age` | Age at risk (years) | | `period` | Calendar year | | `cohort` | Birth year (`period - age`) | -| `init` | Event indicator (1 = initiated/quit, 0 = at risk but no event) | -| `weighting` | Survey weight; multiplied by the mortality correction once one is implemented (currently none) | -| Natural spline columns | Spline basis for age, period, cohort effects | +| `event` | Event indicator (1 = initiated/quit, 0 = at risk but no event) | +| `weight` | Survey weight; multiplied by the mortality correction once one is implemented (currently none) | + +The spline basis columns are built in Stage 8 (`build_spline_basis()`), not stored here. Each data frame carries two attributes set by `apply_survival_correction()`: `mortality_correction` (currently `"none"`) and `estimand_note`. ## Key decisions diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index d0dc392..7ebd4de 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -1,7 +1,7 @@ test_that("derive_survey_year returns correct integer year for all 11 cycles", { cfg <- config::get() cycle_col <- survey_var(cfg, "cycle") - age_col <- survey_var(cfg, "age") + age_col <- survey_var(cfg, "age") data <- setNames( data.frame( @@ -22,7 +22,7 @@ test_that("derive_survey_year returns correct integer year for all 11 cycles", { test_that("derive_survey_year computes cohort as survey_year - round(age)", { cfg <- config::get() cycle_col <- survey_var(cfg, "cycle") - age_col <- survey_var(cfg, "age") + age_col <- survey_var(cfg, "age") data <- setNames( data.frame( @@ -41,7 +41,7 @@ test_that("derive_survey_year computes cohort as survey_year - round(age)", { test_that("derive_survey_year stops on unknown cycle code", { cfg <- config::get() cycle_col <- survey_var(cfg, "cycle") - age_col <- survey_var(cfg, "age") + age_col <- survey_var(cfg, "age") data <- setNames( data.frame(factor("99", levels = "99"), 40), @@ -52,7 +52,7 @@ test_that("derive_survey_year stops on unknown cycle code", { }) test_that("build_initiation_data: no numerator rows with age < initiation floor", { - cfg <- config::get() + cfg <- config::get() data <- make_apc_test_data(cfg) sex_col <- survey_var(cfg, "sex") @@ -63,7 +63,7 @@ test_that("build_initiation_data: no numerator rows with age < initiation floor" }) test_that("build_initiation_data: no rows with cohort < cohort_min", { - cfg <- config::get() + cfg <- config::get() data <- make_apc_test_data(cfg) result <- build_initiation_data(data, cfg) @@ -71,19 +71,19 @@ test_that("build_initiation_data: no rows with cohort < cohort_min", { }) test_that("build_initiation_data: denominator period within [period_min, period_max]", { - cfg <- config::get() + cfg <- config::get() data <- make_apc_test_data(cfg) sex_col <- survey_var(cfg, "sex") result <- build_initiation_data(data[data[[sex_col]] == 1, ], cfg) - denom <- result[result$event == 0, ] + denom <- result[result$event == 0, ] expect_true(all(denom$period >= cfg$apc$period_min)) expect_true(all(denom$period <= cfg$apc$period_max)) }) test_that("build_cessation_data: only ever-daily smokers in cessation data", { - cfg <- config::get() + cfg <- config::get() data <- make_apc_test_data(cfg) # build_cessation_data accepts current daily (1), occ former daily (2), and former daily (4) @@ -97,7 +97,7 @@ test_that("build_cessation_data: only ever-daily smokers in cessation data", { }) test_that("no missing weight in any output element", { - cfg <- config::get() + cfg <- config::get() data <- make_apc_test_data(cfg) result_init <- build_initiation_data(data, cfg) @@ -111,8 +111,10 @@ test_that("apply_survival_correction: none leaves weights unchanged and labels t cfg <- config::get() cfg$apc$mortality_method <- "none" - df <- data.frame(age = 1:5, cohort = 1970:1974, period = 1985:1989, - event = c(1, 0, 0, 1, 0), weight = c(100, 200, 150, 300, 250)) + df <- data.frame( + age = 1:5, cohort = 1970:1974, period = 1985:1989, + event = c(1, 0, 0, 1, 0), weight = c(100, 200, 150, 300, 250) + ) result <- apply_survival_correction(df, cfg) expect_equal(result$weight, df$weight) @@ -137,7 +139,10 @@ test_that("apply_survival_correction: unknown method is an error", { }) test_that("assert_correction_applied: a correction that changes no weights fails", { - before <- data.frame(weight = c(100, 200, 150)) + before <- data.frame( + age = 20:22, period = 2000:2002, cohort = 1980L, event = c(0L, 1L, 0L), + weight = c(100, 200, 150) + ) expect_error( assert_correction_applied(before, before, "mport"), "left every weight unchanged" @@ -147,6 +152,35 @@ test_that("assert_correction_applied: a correction that changes no weights fails expect_true(assert_correction_applied(before, after, "mport")) }) +test_that("assert_correction_applied: a correction may change only the weights", { + before <- data.frame( + age = 20:22, period = 2000:2002, cohort = 1980L, event = c(0L, 1L, 0L), + weight = c(100, 200, 150) + ) + dropped <- before[-2, ] + dropped$weight <- dropped$weight * 1.2 + expect_error(assert_correction_applied(before, dropped, "mport"), "number of rows") + + reordered <- before[c(3, 1, 2), ] + reordered$weight <- reordered$weight * 1.2 + expect_error(assert_correction_applied(before, reordered, "mport"), "reordered") + + bad <- before + bad$weight <- c(110, NA, 160) + expect_error(assert_correction_applied(before, bad, "mport"), "non-finite") +}) + +test_that("fit_apc_model carries the mortality-correction label and estimand note", { + cfg <- config::get() + cfg$apc$mortality_method <- "none" + apc_data <- prepare_apc_data(make_apc_test_data(cfg), cfg) + ds <- apc_data$initiation_men + expect_identical(attr(ds, "mortality_correction"), "none") + fit <- fit_apc_model(ds, "initiation", "men", cfg) + expect_identical(attr(fit, "mortality_correction"), "none") + expect_match(attr(fit, "estimand_note"), "survived to be surveyed") +}) + test_that("apply_survival_correction: mport raises not-implemented error", { cfg <- config::get() cfg$apc$mortality_method <- "mport" @@ -154,4 +188,3 @@ test_that("apply_survival_correction: mport raises not-implemented error", { df <- data.frame(age = 1, cohort = 1970, period = 1985, event = 0, weight = 100) expect_error(apply_survival_correction(df, cfg), "not yet implemented") }) - From 4c0a3462a5acb577c58eceb69799267444bac74d Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:53:38 -0400 Subject: [PATCH 05/29] Task 1.7a: fix fit_apc_model test to pass the sex code (1), not the label --- tests/testthat/test-apc-data.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index 7ebd4de..b32acbe 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -176,7 +176,7 @@ test_that("fit_apc_model carries the mortality-correction label and estimand not apc_data <- prepare_apc_data(make_apc_test_data(cfg), cfg) ds <- apc_data$initiation_men expect_identical(attr(ds, "mortality_correction"), "none") - fit <- fit_apc_model(ds, "initiation", "men", cfg) + fit <- fit_apc_model(ds, "initiation", 1, cfg) # sex is coded 1 = men expect_identical(attr(fit, "mortality_correction"), "none") expect_match(attr(fit, "estimand_note"), "survived to be surveyed") }) From fcec3ece4d297c0124ce252e6dfadf45bff8e493 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:55:06 -0400 Subject: [PATCH 06/29] Task 1.0: established-smoker gate, per-transition immigration entry, interval conventions, recent-quitter semantics Revisions from external review of PR #6. The 100-cigarette gate (Manuel et al. 2020) now defines membership; experimental smokers are Never; age at first cigarette dates entry only for those who pass the gate (smoked_100_lifetime; SMK_01A for 2022). Delayed entry for immigrants is stated per transition (arriving Never, Current, or Former). The annual interval is defined (age row = year from the a-th birthday; event row in the risk set; initiation before cessation within a year; survey year included). Recent quitters are separated into observed status (Former), modelled state (Current up to survey), and risk-set contribution (censored at quit age), so each person has one modelled state at each age. The same-age sensitivity removes the person from both transition models. The dependency on PR #4 for the 'none' correction value is declared. The PI's 2026-08-27 decision on 2001 (complete-cessation timing is NA(c), imputed) is recorded. --- docs/development/estimand-specification.md | 43 +++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index a635e31..95b3434 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -1,7 +1,7 @@ # Smoking states and transitions: analysis specification **Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). -**Status:** ratified 2026-08-27 (PI). The same-age rule, the one item that was open, is decided below; everything else restates the protocol in operational terms. +**Status:** ratified 2026-08-27 (PI), revised the same day after external review. One item marked **[confirm]** restates a rule inherited from Manuel et al. (2020) and needs the PI's explicit confirmation. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. ## 1. Why this document exists @@ -11,11 +11,15 @@ The pipeline built its initiation model on one definition of a smoker (anyone wh Each person is in exactly one state at each age. +**The established-smoker gate [confirm].** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. + | State | Definition | How it is observed at survey | |---|---|---| -| Never | Has never smoked a whole cigarette | `SMKDSTY_original` = never smoked | -| Current | Has smoked a whole cigarette and has not stopped smoking completely (daily or occasional) | `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | -| Former | Smoked a whole cigarette in the past and has stopped smoking completely | `SMKDSTY_original` = former daily or former occasional | +| Never | Has not smoked 100 cigarettes: never smoked a whole cigarette, or smoked fewer than 100 (experimental smoker) | `smoked_100_lifetime` = no; includes `SMKDSTY_original` never smoked and, from 2015, the SMKDVSTY experimental-smoker category | +| Current | Passed the gate and has not stopped smoking completely (daily or occasional) | `smoked_100_lifetime` = yes and `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | +| Former | Passed the gate and has stopped smoking completely | `smoked_100_lifetime` = yes and `SMKDSTY_original` = former daily or former occasional | + +The observed status at survey is the starting point, not the modelled state: the durability rule in section 4 moves people who quit less than two years before the survey from observed Former to modelled Current. Two points follow from the definitions. Stopping daily smoking while still smoking occasionally is not a transition; the person stays current. How much a current smoker smokes, and whether they smoke daily, are characteristics of the current state (protocol section 3.4.4), not states of their own. @@ -23,28 +27,36 @@ Two points follow from the definitions. Stopping daily smoking while still smoki | Transition | From | To | Event age | CCHS variable (cchsflow v3) | |---|---|---|---|---| -| Initiation | Never | Current | Age at first whole cigarette | `age_first_cigarette` | +| Initiation | Never | Current | Age at first whole cigarette, for people who pass the gate | `age_first_cigarette` | | Cessation | Current | Former | Age at which the person stopped smoking completely | survey age minus `time_quit_smoking_complete` | Not modelled as transitions: relapse (Former back to Current) and daily onset (Current to a daily sub-state). The CCHS records one quit per person, so relapse cannot be estimated from it; its influence is a sensitivity analysis (protocol section 3.5). Daily onset (`age_start_smoking`) is retained as a characteristic of current smokers and for the intensity model. -**Consequence for the code.** The cessation model currently uses the ever-daily universe and `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the universe is all ever-smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. +The CCHS does not ask the age at which the 100th cigarette was smoked, so the age at first whole cigarette is the entry age for everyone who passes the gate. This follows Manuel et al. (2020) and is a known approximation: entry is dated to the start of smoking, not to the point at which it became established. + +**Consequence for the code.** The cessation model currently uses the ever-daily universe and `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the universe is all established smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. + +**The 2001 cycle (decided 2026-08-27).** `time_quit_smoking_complete` is derived from questions first asked in 2003. For 2001 the timing of complete cessation is treated as not asked in that cycle (NA(c)) and handled by the imputation procedure for cycle-level absence (Appendix D); the 2001 `time_quit_smoking` variable, which lacks the "stopped completely" question, is not used as a substitute. ## 4. Event-time conventions -- **Time step.** One year. Ages are whole years; each person contributes one row per age at which they are at risk of the transition being modelled. +- **Time step and interval.** One year. The row for age *a* covers the year from the person's *a*-th birthday to the day before the next one. An event at age *a* happened during that year. The event row is part of the risk set: it carries one trial, with the event. Within a year, initiation is applied before cessation, so a person who starts and stops at the same age has a one-year spell (below). The year of the survey is the last observed row for everyone; it is treated as a full year of exposure, a simplification shared with the Manuel and Holford implementations. - **One spell per person.** A person enters Current once and leaves it at most once. - **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. - **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. -- **Durable cessation.** The primary definition of cessation is a quit that has lasted at least two years at the survey. A person who quit less than two years before the survey is a current smoker at the survey; in the cessation model they contribute person-years up to the reported quit age and are then censored, with no event. -- **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* exclude same-age spells from the cessation model (treat the person as never having established smoking). Few respondents, possibly none, are expected to meet this condition; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. +- **Durable cessation and recent quitters.** The primary definition of cessation is a quit that has lasted at least two years at the survey. Three things are distinguished for a person who quit less than two years before the survey. *Observed status:* Former (`SMKDSTY_original`). *Modelled state:* Current at every age up to the survey, because the quit is not yet known to be durable; this is the state used for prevalence and passed to the generator. *Cessation risk set:* person-years from entry to the reported quit age, then censored with no event; the years between the quit age and the survey are not in the risk set because whether the quit will hold cannot yet be observed. The person therefore has exactly one modelled state at each age (Current) while contributing to the risk set only up to the quit age. Risk-set membership describes what can be observed about the outcome; it is not the state. +- **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* remove the person from both transition models -- no initiation event and no cessation spell -- treating them as never having established smoking, so that reconstructed prevalence does not acquire an initiation without its cessation. Few respondents, possibly none, are expected to meet this condition; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. - **Reported ages that cannot be right** (initiation after survey age, cessation before initiation) are treated as missing and enter the imputation procedure (task 1.8c). No person is silently reclassified. ## 5. Target population and risk-set entry - The Canadian household population covered by the CCHS, aged 12 and over at survey (the study analyses respondents aged 18 and over; `age_exclusion_min`). - **Immigration.** Person-years before immigration are excluded (protocol section 3.3). In the Master files the year of immigration is exact. In the PUMF only immigrant status and, in some cycles, a grouped time-since-immigration variable are available; task 1.9 specifies the PUMF approximation and documents its error. -- A person's risk set for either transition begins at the later of the state-entry age (section 4) and the age at entry into the Canadian population. +- **Delayed entry is defined per transition**, because an immigrant can arrive in any state: + - Arrives Never (no smoking before immigration): initiation risk begins at the later of the study floor age and the age at immigration; if they later initiate, cessation risk begins at that initiation age. + - Arrives Current (initiated before immigration): the initiation event is excluded, because it occurred outside the target population; the person is Current from the age at immigration for prevalence, and cessation risk begins at the age at immigration, not at the initiation age. + - Arrives Former (initiated and quit before immigration): contributes to neither risk set; Former from the age at immigration for prevalence. + - Canadian-born: state-entry ages as in section 4. ## 6. Output contract with the smoking-history generator @@ -53,27 +65,32 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat - Rate tables carry, per `model_type` (initiation, cessation), `sex`, `province`, `age`, `period`, and `cohort`: `rate`, `rate_lower`, `rate_upper`, and `mortality_correction` (`schemas/cshm-rate-tables.yaml`). `rate` is the annual probability of the transition among people in the source state at that age. - The generator applies initiation to Never and cessation to Current. Former is absorbing (no relapse). At every age each simulated person is in exactly one of the three states. - Intensity (cigarettes per day) and daily status are attributes attached to Current, drawn from the intensity model; they never change a person's state. -- `mortality_correction = "none"` means the rates describe respondents who survived to be surveyed. The generator must carry that label through to its outputs until a correction is implemented (task 1.7b). +- `mortality_correction = "none"` (introduced by task 1.7a, private PR #4) means the rates describe respondents who survived to be surveyed. The generator must carry that label through to its outputs until a correction is implemented (task 1.7b). ## 7. Decisions recorded here | Item | Decision | Source | |---|---|---| | State model | Established-smoking model: Never, Current, Former | Adjudication A1, 2026-08-07 | +| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020, per A1 -- **[confirm]** | +| 2001 cycle | Complete-cessation timing is NA(c); imputed | PI decision, 2026-08-27 | +| Interval convention | Age row = year from the *a*-th birthday; event row in risk set; initiation before cessation within a year; survey year included as a full year | Specified here, 2026-08-27 | | Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | | Exit event | Stopped smoking completely (`time_quit_smoking_complete`) | A1; protocol 3.4.1 | | Durability | Two years; more recent quitters are current at survey | Protocol 3.4.1 | | Same-age rule | One-year spell (primary); exclusion (sensitivity) | PI decision, 2026-08-27 | | Relapse | Not modelled; sensitivity analysis | Protocol 3.4.1 | -| Immigration entry | Excluded before immigration; PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1 | +| Immigration entry | Per-transition delayed entry (section 5); PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1 | ## 8. What changes in the pipeline because of this note - [ ] 1.3: cessation universe = ever-smokers; exit variable = `time_quit_smoking_complete`; clock from `age_first_cigarette`; recent-quitter censoring; same-age rule. - [ ] Config: replace the `years_since_quit` mapping (`time_quit_smoking_daily`) with the complete-cessation variable; keep `age_start_daily` for the intensity model only. +- [ ] Gate: add `smoked_100_lifetime` (with `SMK_01A` for 2022) to the variables sheet as the universe variable for both transitions; experimental smokers map to Never. +- [ ] 2001: complete-cessation timing tagged NA(c) and routed to the cycle-level imputation path. - [ ] Worksheets: roles for `time_quit_smoking_complete` (apc-numerator, apc-denominator); `age_start_smoking` loses its cessation role. - [ ] 1.3 diagnostic: report the unweighted and weighted number of same-age spells per cycle. - [ ] 1.2: initiation window aligned to the entry event above. -- [ ] 1.9: immigration entry floor. +- [ ] 1.9: per-transition delayed entry (section 5), including exclusion of pre-immigration initiation events. - [ ] 1.8c: imputation universes follow section 2 (state gates first). - [ ] Generator contract (section 6) added to the rate-table schema description. From 7df6550b41c436820f60ca8adbc57c37e462545e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 12:55:33 -0400 Subject: [PATCH 07/29] Task 1.0: style pass (decision tag wording; remove antithesis) --- docs/development/estimand-specification.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 95b3434..1319c74 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -1,7 +1,7 @@ # Smoking states and transitions: analysis specification **Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). -**Status:** ratified 2026-08-27 (PI), revised the same day after external review. One item marked **[confirm]** restates a rule inherited from Manuel et al. (2020) and needs the PI's explicit confirmation. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. +**Status:** ratified 2026-08-27 (PI), revised the same day after external review. One item marked **[PI to ratify]** restates a rule inherited from Manuel et al. (2020) and needs the PI's explicit decision. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. ## 1. Why this document exists @@ -11,7 +11,7 @@ The pipeline built its initiation model on one definition of a smoker (anyone wh Each person is in exactly one state at each age. -**The established-smoker gate [confirm].** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. +**The established-smoker gate [PI to ratify].** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. | State | Definition | How it is observed at survey | |---|---|---| @@ -19,7 +19,7 @@ Each person is in exactly one state at each age. | Current | Passed the gate and has not stopped smoking completely (daily or occasional) | `smoked_100_lifetime` = yes and `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | | Former | Passed the gate and has stopped smoking completely | `smoked_100_lifetime` = yes and `SMKDSTY_original` = former daily or former occasional | -The observed status at survey is the starting point, not the modelled state: the durability rule in section 4 moves people who quit less than two years before the survey from observed Former to modelled Current. +The table gives the observed status at survey. The modelled state can differ from it: the durability rule in section 4 moves people who quit less than two years before the survey from observed Former to modelled Current. Two points follow from the definitions. Stopping daily smoking while still smoking occasionally is not a transition; the person stays current. How much a current smoker smokes, and whether they smoke daily, are characteristics of the current state (protocol section 3.4.4), not states of their own. @@ -72,7 +72,7 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat | Item | Decision | Source | |---|---|---| | State model | Established-smoking model: Never, Current, Former | Adjudication A1, 2026-08-07 | -| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020, per A1 -- **[confirm]** | +| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020, per A1 -- **[PI to ratify]** | | 2001 cycle | Complete-cessation timing is NA(c); imputed | PI decision, 2026-08-27 | | Interval convention | Age row = year from the *a*-th birthday; event row in risk set; initiation before cessation within a year; survey year included as a full year | Specified here, 2026-08-27 | | Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | From 7deb2228ea662661a892a51802a73848de21c41d Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 13:49:51 -0400 Subject: [PATCH 08/29] Protocol v0.4.1: editorial revisions from PI review of the Word render Wording and plain-language edits throughout; no methodological change. Ethics statement rewritten (secondary analysis of de-identified data; Statistics Canada Open Licence). Study team updated. NHIS typo fixed. Edits were made in Word and merged back by hand: the docstyle harvest remaps citekeys when field-codes.json entries lack a Zotero URI or share one, so the harvested file was used only as a source of text changes. The edited .docx is kept under docs/protocol/source/ for provenance. --- docs/protocol/full-protocol.qmd | 57 +++++++++--------- .../source/full-protocol-2026-08-27-dm.docx | Bin 0 -> 64591 bytes 2 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 docs/protocol/source/full-protocol-2026-08-27-dm.docx diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index e5a0abd..e80336a 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -2,9 +2,12 @@ title: "A Canadian Smoking Histories Model: a study protocol to generate smoking cohorts from 1940 and project to 2050" status: "Draft" version-summary: - date: "2026-08-07" - version: "0.4.0" + date: "2026-08-27" + version: "0.4.1" version-history: + - version: "0.4.1" + date: "2026-08-27" + description: "Editorial revisions from PI review of the Word render (wording and plain-language edits throughout; no methodological change). Ethics statement rewritten to describe secondary analysis of de-identified data and the Statistics Canada Open Licence. Study team updated (co-led with BC Cancer Research Institute; Canadian Partnership Against Cancer named among knowledge users). Fixed NHIS typo." - version: "0.4.0" date: "2026-08-07" description: "Methodological amendments following the review (2026-08-07). New section 3.4.1 defines the smoking states and transitions: never, current, and former, following Holford et al. (2014) and Manuel et al. (2020); initiation is the first whole cigarette; cessation is stopping smoking completely; daily smoking and intensity are characteristics of current smokers; one smoking spell per person, with cessation risk starting at the age of entry; a two-year definition of cessation; the rule for same-age initiation and cessation to be set before fitting, with a sensitivity analysis. Section 3.4.3: the split of the linear trend between age, period, and cohort is stated as an explicit assumption (cohort linear trend set to zero, following Manuel et al. 2020; the alternative allocation is a sensitivity analysis); the rules that extend the model for projection are distinguished from the rule that fixes it; the spline basis is saved at fitting and reused; each fitted model is checked before use (Holford 2006). Section 3.5: uncertainty from CCHS bootstrap replicate weights in the Master-file analysis, combined across imputations and carried through to the rate tables; an approximate bootstrap for public-use analyses; simulated weights (MockData) test code only. Section 3.4.5: MPoRT remains the primary mortality adjustment if it passes life-table and simulation checks; where the adjustment enters the calculation to be settled before implementation; unadjusted results labelled as estimates among survivors. Section 3.4.2 and Appendix D: imputation follows the structure of the smoking questions, with ordering rules applied within the procedure, every imputed dataset analysed, and the number of imputations set by stability (at least five). Validation: within the CCHS, using held-out cycles and recall-consistency checks; no comparison with other surveys in this version, with reasons given. Subsections of 3.4 renumbered." @@ -44,11 +47,11 @@ This study aims to develop a Canadian Smoking Histories Model (CSHM) that recons **Methods** -Using harmonized data from over one million respondents in the Canadian Community Health Survey (2001--2023), we will apply age-period-cohort models to estimate smoking initiation, cessation, and intensity. Mortality-adjusted estimates using the MPoRT algorithm will address survival bias. Estimates will be produced for each province and territory, and projections will extend to 2050. +Using harmonized data from more than 1 million respondents in the Canadian Community Health Survey (2001--2023), we will apply age-period-cohort models to estimate smoking initiation, cessation, and intensity. Mortality-adjusted estimates using the MPoRT algorithm will address survival bias. Estimates will be produced for each province and territory, and projections will extend to 2050. **Expected Outcomes** -The model will be publicly accessible and used for policy evaluation and disease modelling in Canada, including projecting future smoking patterns and evaluating historic or proposed tobacco policy. +The model will be publicly accessible and used for policy evaluation and disease modelling in Canada, including projecting future smoking patterns and evaluating historic or proposed tobacco policy :::: {.section-body line-numbers="continuous"} ::: page-break @@ -60,7 +63,7 @@ Smoking behaviour varies across birth cohorts in ways that affect projections of The Smoking History Generator (SHG) framework, developed by the National Cancer Institute (NCI) CISNET Smoking Working Group, characterizes these patterns in the United States using Age-Period-Cohort (APC) models [@Holford_AJPM_2014]. This approach was adapted for Ontario [@Manuel_HR_2020], demonstrating that Canadian Community Health Survey (CCHS) data can be used to reconstruct birth-cohort-specific smoking histories from 1965 to 2013. A pan-Canadian equivalent covering all provinces and territories has not yet been developed. -Three gaps motivate the CSHM. First, Canada’s tobacco control landscape occurs at both the national and regional levels, with provincial variation in taxation, smoke-free legislation, and cessation support. Second, Canadian smoking initiation and cessation patterns differ from those in other countries, particularly in the timing of peak prevalence and the rate of decline among younger cohorts. Third, Canadian health policy models (e.g., OncoSim, POHEM, SimSmoke) rely on behaviour transition parameters from the 1994--2004 National Population Health Survey (NPHS), now over 20 years old, with no planned replacement [@hennessy2015; @gauvreau2017; @chaiton2021]; these parameters no longer reflect contemporary patterns, particularly among younger cohorts, immigrant populations, and lower-income groups. The CCHS [@beland2002], with over 1.4 million respondents and annual data collection since 2001, provides an unparalleled opportunity to address this gap. The methods and infrastructure developed here will also expand knowledge for many countries facing the same challenge: large cross-sectional surveys but lacking population-based panel data [@kopasker2023; @vasquezlavin2022]. This study addresses these gaps with a unified, open-source framework for smoking history generation across all Canadian jurisdictions. +Three gaps motivate the CSHM. First, Canada’s tobacco control landscape occurs at both the national and regional levels, with provincial variation in taxation, smoke-free legislation, and cessation support. Second, Canadian smoking initiation and cessation patterns differ from those in other countries, particularly in the timing of peak prevalence and the rate of decline among younger cohorts. Third, Canadian health policy models (e.g., OncoSim, POHEM, SimSmoke) rely on behaviour transition parameters from the 1994--2004 National Population Health Survey (NPHS), now over 20 years old, with no planned replacement [@hennessy2015; @gauvreau2017; @chaiton2021]; these parameters no longer reflect contemporary patterns, particularly among younger cohorts, immigrant populations, and lower-income groups. The CCHS [@beland2002], with over 1.4 million respondents and annual data collection since 2001, provides an unparalleled opportunity to address this gap. The methods and infrastructure developed here build on those of other similar studies [@kopasker2023; @vasquezlavin2022]. This study addresses these gaps by introducing an open-source framework for generating smoking histories across all Canadian jurisdictions. # 2. Objectives @@ -69,7 +72,7 @@ This study will develop a Canadian Smoking Histories Model (CSHM) that describes > Note: the starting date (1965) may need revision based on model performance. 1. **Estimate historical and current smoking parameters:** Reconstruct rates of smoking initiation, cessation, and intensity (cigarettes per day) by birth cohort (1890--2030) for each province and territory. -2. **Examine regional and temporal variations:** Analyse how smoking patterns have evolved across Canadian jurisdictions in response to differing policy environments and social trends. +2. **Examine regional and temporal variations:** Analyze how smoking patterns have evolved across Canadian jurisdictions in response to differing policy environments and social trends. 3. **Project future smoking prevalence:** Model future trends in smoking prevalence and related parameters through 2050 under status quo policy conditions. 4. **Develop an open-access model:** Create a publicly-accessible, reproducible R-based model that can be used and updated by researchers, policy analysts, and health system planners. @@ -89,14 +92,14 @@ The conceptual framework for the CSHM is structured as a discrete-time Markov st - **Current Smoker** (daily or occasional use) - **Former Smoker** (quit for $\ge$ 1 year). -Individuals transition between these states based on annual probabilities of **initiation** (Never to Current) and **cessation** (Current to Former). To ensure precision in tobacco "dose" estimation, the Current Smoker compartment is further characterized by smoking intensity (cigarettes per day). +Individuals transition between these states according to annual probabilities of **initiation** (from Never to Current) and **cessation** (from Current to Former). To ensure precision in tobacco "dose" estimation, the Current Smoker compartment is further characterized by smoking intensity (cigarettes per day). > ISPOR-SMDM: Model structure; STRESS: Conceptualization -The APC framework is implemented to separate temporal trends into three distinct components: age effects (biological and developmental influences), period effects (influences such as policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: +The APC framework is used to separate temporal trends into three distinct components: age effects (biological and developmental influences), period effects (e.g., policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: 1. **Identifiability:** It addresses the fundamental APC identity ($cohort = period - age$) through constrained natural cubic splines, enabling the estimation of unique generational trends. -2. **Historical Reconstruction:** It allows for the back-calculation of smoking rates for birth cohorts, effectively using current survivors to understand historical patterns. +2. **Historical Reconstruction:** It enables the back-calculation of smoking rates for birth cohorts, using current survivors to understand historical patterns. 3. **Survival Bias Mitigation:** It provides a method to correct for differential mortality (the "healthy survivor" effect), ensuring that estimated historical initiation and cessation rates reflect the original population rather than only those who survived to be surveyed. > ISPOR-SMDM: Analytical approach; GATHER: Methods overview @@ -148,9 +151,9 @@ The CSHM uses a two-stage analytical framework. In the first stage, we estimate ### 3.4.1 Smoking states and transition estimands -The model estimates transitions between three smoking states: **never**, **current**, and **former**. Each person is in exactly one state at each age. This is the framework of Holford et al. (2014) [@Holford_AJPM_2014], which has been applied in several jurisdictions, and of Manuel et al. (2020) [@Manuel_HR_2020] for Ontario using the CCHS. This study follows the Manuel et al. implementation because it uses the same survey. A person enters the current state (initiation) at the age they smoked their first whole cigarette, provided they meet the study definition of an ever-smoker. A person leaves the current state (cessation) when they stop smoking completely; stopping daily smoking while continuing to smoke occasionally is not cessation. Starting to smoke daily, and the number of cigarettes smoked per day, are treated as characteristics of current smokers (section 3.4.4), not as separate transitions. +The model estimates transitions between three smoking states: **never**, **current**, and **former**. Each person is in exactly one state at each age. This is the framework of Holford et al. (2014) [@Holford_AJPM_2014], which has been applied in several jurisdictions, and of Manuel et al. (2020) [@Manuel_HR_2020] for Ontario using the CCHS. This study follows the Manuel et al. implementation because it uses the same survey. A person enters the current state (initiation) at the age they smoked their first whole cigarette, provided they meet the study definition of an ever-smoker. A person leaves the current state (cessation) when they stop smoking completely; stopping daily smoking while continuing to smoke occasionally is not cessation. Starting to smoke daily and the number of cigarettes smoked per day are treated as characteristics of current smokers (section 3.4.4), not as separate transitions. -The model works in one-year steps and gives each person at most one period of smoking, from initiation to cessation. A person's cessation risk begins at the age they entered the current state. The primary definition of cessation is the most recent quit that lasted at least two years. People who quit less than two years before the survey have not yet met this definition: they are classified as current smokers at the survey, and in the cessation model they contribute time at risk up to their reported quit age and are then censored, with no cessation event recorded. This follows the Holford and Manuel implementations. When initiation and cessation are reported at the same whole-year age, the data cannot show which came first within the year. We will state the rule for these cases in the analysis specification before fitting the models, and we will run a prespecified sensitivity analysis that excludes or interval-censors them. The CCHS records one quit per person, so relapse cannot be modelled directly; its influence is examined in sensitivity analyses (section 3.5). +The model operates in one-year steps and assigns each person at most one smoking period, from initiation to cessation. A person's cessation risk begins at the age they entered the current state. The primary definition of cessation is the most recent quit that lasted at least two years. People who quit less than two years before the survey have not yet met this definition: they are classified as current smokers at the survey, and in the cessation model they contribute time at risk up to their reported quit age, after which they are censored, with no cessation event recorded. This follows the Holford and Manuel implementations. When initiation and cessation are reported at the same whole-year age, the data cannot show which came first within the year. We will state the rule for these cases in the analysis specification before fitting the models, and we will run a prespecified sensitivity analysis that excludes or interval-censors them. The CCHS records one quit per person, so relapse cannot be modelled directly; its influence is examined in sensitivity analyses (section 3.5). The target population is the Canadian household population covered by the CCHS. For immigrants, the years before immigration are excluded from the reconstructed history (section 3.3), so no one contributes time before entering the Canadian population. Respondents whose age at initiation is missing or implausible are handled by multiple imputation (section 3.4.2 and Appendix D); they are not excluded, and they are not reclassified as never-smokers. @@ -168,11 +171,11 @@ Analysis datasets will be produced through a standardized pipeline. Three separate APC models will be fitted by sex (men and women) to characterize smoking life-course trajectories: one for initiation, one for cessation, and one for smoking intensity (cigarettes per day). The primary analysis produces national estimates stratified by sex, as well as separate estimates for each province and territory. -All models use natural cubic splines to describe how rates change with age, calendar period, and birth cohort. Because period equals age plus cohort, the data cannot tell apart the straight-line (linear) trends in age, period, and cohort: any amount of linear trend can be moved from one to another without changing the fitted rates [@Holford_SM_2006]. We therefore fix this split as an explicit modelling assumption: the linear trend in birth cohort is set to zero, so any straight-line change over time is attributed to calendar period, and age keeps its own linear term. This matches the Manuel et al. (2020) implementation [@Manuel_HR_2020]. We fit the model in terms of quantities the data can estimate: the shape of the age curve, the curvature (departure from a straight line) in period and in cohort, and the net drift. As a sensitivity analysis we attribute the linear trend to cohort instead, with the period linear trend set to zero, and report how much the published rate tables change. +All models use natural cubic splines to describe how rates change with age, calendar period, and birth cohort. Because period equals age plus cohort, the data cannot distinguish the straight-line (linear) trends in age, period, and cohort: any amount of linear trend can be shifted from one to the other without changing the fitted rates [@Holford_SM_2006]. We therefore fix this split as an explicit modelling assumption: the linear trend in birth cohort is set to zero, so any straight-line change over time is attributed to calendar period, and age keeps its own linear term. This matches the Manuel et al. (2020) implementation [@Manuel_HR_2020]. We fit the model in terms of quantities that the data can estimate: the shape of the age curve, the curvature (departure from linearity) in period and cohort, and the net drift. As a sensitivity analysis, we attribute the linear trend to the cohort instead, set the period linear trend to zero, and report how much the published rate tables change. -The model uses two kinds of rule, and they do different jobs. The first kind fixes the model, by stating how the linear trend is split between age, period, and cohort, as described above. The second kind extends the model beyond the years we observe: for projection, period effects are held at their most recent observed values (for initiation and cessation alike), and cohort effects are held constant before 1920 and after 1985, following Manuel et al. (2020). These extension rules cannot fix the model on their own, because whether they happen to do so depends on which respondents fall at the edges of the observed data. +The model uses two kinds of rules. The first kind fixes the model by stating how the linear trend is split between age, period, and cohort, as described above. The second kind extends the model beyond the years we observe: for projection, period effects are held at their most recent observed values (for initiation and cessation alike), and cohort effects are held constant before 1920 and after 1985, following Manuel et al. (2020). These extension rules cannot fix the model on their own, because whether they happen to do so depends on which respondents fall at the edges of the observed data. -The spline basis (the knots, boundary knots, and centring values) is part of the model definition. The basis built when the model is fitted is saved and reused for every prediction and projection; it is never rebuilt from the prediction grid. Before any fitted model is used, we check that it has the expected rank and that the quantities we report can be estimated from it. Predictions for age, period, and cohort combinations outside the observed data are reported separately from those the data support. +The spline basis (the knots, boundary knots, and centring values) is part of the model definition. The basis built when the model is fitted is saved and reused for every prediction and projection; it is never rebuilt from the prediction grid. Before any fitted model is used, we check that it has the expected rank and that the quantities we report can be estimated from it. Predictions for age, period, and cohort combinations outside the observed data are reported separately from those supported by the data. Knot placement for the splines follows established standards: @@ -185,13 +188,13 @@ Knot placement for the splines follows established standards: We will use a dual approach to characterize smoking intensity (cigarettes per day; CPD): 1. **Simple descriptives:** We will first calculate mean CPD and intensity distributions by age, sex, and survey year. This descriptive analysis will identify broad temporal shifts and assess whether intensity has remained stagnant across cohorts, as observed in other jurisdictions such as Brazil [@Tam_AJPM_2023]. -2. **APC intensity model:** We will fit an APC model to estimate the expected CPD by birth cohort. These parameters are the "tobacco dose" input for the simulation stage, allowing for the reconstruction of pack-year histories. For occasional smokers, we will apply a standardized CPD adjustment based on their reported frequency of use. +2. **APC intensity model:** We will fit an APC model to estimate the expected CPD by birth cohort. These parameters are the "tobacco dose" inputs for the simulation stage, enabling the reconstruction of pack-year histories. For occasional smokers, we will apply a standardized CPD adjustment based on their reported frequency of use. ### 3.4.5 Mortality adjustment -To address survival bias (ever-smokers having lower survival to survey date than never-smokers), we will apply mortality adjustments using the Mortality Population Risk Tool (MPoRT) [@Manuel_HR_2020]. MPoRT weights are adjusted for age, sex, smoking status, years since quitting, and immigration status. For each respondent, the one-year probability of death for each historical year up to the survey date is calculated; the survival bias weight is the proportion of ever-smokers who would have died prior to the survey date. This adjustment ensures that the reconstructed historical prevalence reflects the original population rather than only the survivors, following the approach of Manuel et al. (2020) [@Manuel_HR_2020]. +To address survival bias (ever-smokers having lower survival to survey date than never-smokers), we will apply mortality adjustments using the Mortality Population Risk Tool (MPoRT) [@Manuel_HR_2020]. MPoRT weights are adjusted for age, sex, smoking status, years since quitting, and immigration status. For each respondent, the one-year probability of death for each historical year up to the survey date is calculated; the survival-bias weight is the proportion of ever-smokers who would have died before the survey date. This adjustment ensures that the reconstructed historical prevalence reflects the original population rather than only the survivors, following the approach of Manuel et al. (2020) [@Manuel_HR_2020]. -The exact form of the adjustment will be written down and checked before it is implemented. The main open question is where the adjustment enters the calculation: as an adjustment to each respondent's weight before the APC models are fitted (as described above), or as a correction to the fitted transition probabilities afterward (as in the original Ontario code). The two approaches do not necessarily give the same answer, and whichever is chosen must apply the correction once only. Before use, the adjustment will be checked in three ways: predicted deaths by age, sex, and calendar year compared with Canadian life tables; the size and stability of the adjustment factors, with limits set in advance; and recovery of known rates from simulated cohorts in which smokers die at higher rates (section 3.5). MPoRT remains the primary method if it passes these checks [@Manuel_HR_2020]. Until an adjustment is in place, unadjusted results are labelled as estimates among survivors and are not presented as birth-cohort smoking histories. +The exact form of the adjustment will be documented and reviewed before it is implemented. The main open question is where the adjustment enters the calculation: as an adjustment to each respondent's weight before the APC models are fitted (as described above), or as a correction to the fitted transition probabilities afterward (as in the original Ontario code). The two approaches do not necessarily give the same answer, and whichever is chosen must apply the correction once only. Before use, the adjustment will be checked in three ways: predicted deaths by age, sex, and calendar year compared with Canadian life tables; the size and stability of the adjustment factors, with limits set in advance; and recovery of known rates from simulated cohorts in which smokers die at higher rates (section 3.5). MPoRT remains the primary method if it passes these checks [@Manuel_HR_2020]. Until an adjustment is in place, unadjusted results are labelled as estimates among survivors and are not presented as birth-cohort smoking histories. A sensitivity analysis will be conducted using the Peto approach -- a constant mortality risk ratio by smoking status -- consistent with the original Holford et al. (2014) US implementation [@Holford_AJPM_2014]. This quantifies the influence of the mortality adjustment method on historical prevalence estimates. @@ -204,7 +207,7 @@ CCHS sampling weights (`WTS_M` in Master files; equivalent PUMF weights) are app Estimated initiation, cessation, and intensity rates will be extrapolated to 2050. The projection engine will support the following scenarios: - **Status quo scenarios:** Period effects are held constant at the most recent observed values. This scenario can be interpreted as... with the assumptions... -- **Period effect trend continue:** Period effects are extrapolated based on the historic trend. This scenario can be interpreted as... with the assumptions... +- **Period effect trend continues:** Period effects are extrapolated from the historical trend. This scenario can be interpreted as... with the assumptions... ### 3.4.7 Provincial and territorial estimates @@ -216,15 +219,15 @@ Separate APC models will be estimated for each province and territory, stratifie The model will be validated through: -1. **Internal validation:** Comparing smoking prevalence reconstructed by the model with prevalence observed in the CCHS. Some survey cycles or subsamples will be held out of model fitting, so that this comparison is not made against the data the model was fitted to. We will also check recall consistency: whether the same birth cohort reports a consistent smoking history when asked in different survey cycles. +1. **Internal validation:** Comparing smoking prevalence reconstructed by the model with prevalence observed in the CCHS. Some survey cycles or subsamples will be held out of model fitting, so that this comparison is not made against the data the model was fitted to. We will also check recall consistency: whether the same birth cohort reports a consistent smoking history across different survey cycles. > ISPOR-SMDM: Internal validation; STRESS: Validation - internal -2. **External validation:** None in this version. Other surveys have their own measurement and response errors. For example, smoking prevalence in the CCHS is typically higher than in tobacco-specific surveys, which most likely reflects the CCHS's broader sampling frame and higher response rate rather than error in the CCHS. Comparing sources without a model of the errors in each would mislead more than it would validate, and these errors are often overlooked. We therefore defer the comparison. A future version may use the measurement error estimated from Canadian Health Measures Survey biomarkers (item 4) to adjust the estimates rather than only compare them. Validating within the CCHS cannot detect a bias that affects the whole survey; we accept and state this limitation. +2. **External validation:** None in this version. Other surveys have their own measurement and response errors. For example, smoking prevalence in the CCHS is typically higher than in tobacco-specific surveys, likely reflecting the CCHS's broader sampling frame and higher response rate rather than measurement error. Comparing sources without a model of the errors in each would mislead more than it would validate, and these errors are often overlooked. We therefore defer the comparison. A future version may use the measurement error estimated from Canadian Health Measures Survey biomarkers (item 4) to adjust the estimates rather than only compare them. Validating within the CCHS cannot detect bias that affects the entire survey; we acknowledge this limitation. > ISPOR-SMDM: External validation; STRESS: Validation - external -3. **Uncertainty analysis:** In the Master-file analysis, uncertainty is estimated with the CCHS bootstrap replicate weights [@StatCan_CCHS_UserGuide_2022; @Rao_SM_1992]. Each replicate reweights whole respondents, so a respondent's full reconstructed history is kept together. The replicate estimates are repeated within each completed imputation dataset, and the variance from the survey design is combined with the variance between imputations using a rule set in advance and checked by simulation. Uncertainty is carried through to the published rate tables and projections, not reported only for model coefficients. The public-use files do not include replicate weights or design variables. For analyses of those files we use a bootstrap that resamples respondents, which approximates the survey design as far as the file allows, and we check the results against Statistics Canada's published approximate coefficients of variation for descriptive estimates. All intervals from public-use analyses are labelled approximate; the definitive intervals come from the Master-file analysis. Simulated replicate weights, generated with the public MockData package, are used to test the replicate-weight code in the public repository. They test the code only; results based on them are never reported. +3. **Uncertainty analysis:** In the Master-file analysis, uncertainty is estimated with the CCHS bootstrap replicate weights [@StatCan_CCHS_UserGuide_2022; @Rao_SM_1992]. Each replicate reweights whole respondents, so a respondent's full reconstructed history is kept together. The replicate estimates are repeated within each completed imputation dataset, and the variance from the survey design is combined with the variance between imputations using a rule set that is specified in advance and checked by simulation. Uncertainty is carried through to the published rate tables and projections, not reported only for model coefficients. The public-use files do not include replicate weights or design variables. For analyses of those files, we use a bootstrap that resamples respondents, approximating the survey design as much as the file allows, and we check the results against Statistics Canada's published approximate coefficients of variation for descriptive estimates. All intervals from public-use analyses are labelled approximate; the definitive intervals come from the Master-file analysis. Simulated replicate weights, generated with the public MockData package, are used to test the replicate-weight code in the public repository. They test only the code; results based on them are never reported. > ISPOR-SMDM: Uncertainty analysis; STRESS: Sensitivity analysis @@ -265,7 +268,7 @@ Specific outputs include: > GATHER: Results publication; ISPOR-SMDM: Reporting -The CSHM will provide inputs for established Canadian health policy models such as Oncosim (cancer) and POHEM (chronic disease), supporting the evaluation of tobacco control policies and estimation of smoking-attributable disease burden (e.g., cancer, cardiovascular disease, and dementia) at national and provincial levels. This follows the approach to model-based policy evaluation used in other jurisdictions [@Meza_J_2021]. +The CSHM will provide input to established Canadian health policy models, such as Oncosim (cancer) and POHEM (chronic disease), to support the evaluation of tobacco control policies and the estimation of smoking-attributable disease burden (e.g., cancer, cardiovascular disease, and dementia) at national and provincial levels. This follows the model-based policy evaluation approach used in other jurisdictions [@Meza_J_2021]. > ISPOR-SMDM: Model application; CHEERS: Model-based evaluation @@ -273,7 +276,7 @@ The CSHM will provide inputs for established Canadian health policy models such ## Governance and collaboration -The CSHM is led by the Ottawa Hospital Research Institute in collaboration with the Canadian Population Attributable Causes (CPAC) consortium, the BC Centre for Disease Control and Prevention (BCCDC), Statistics Canada's Health Analysis and Modelling Division, and academic partners. The core modelling team is responsible for analytical decisions. Partner representatives will review key milestone outputs and the final model prior to dissemination. Roles and responsibilities are documented in the project repository. +The CSHM is co-led by the Ottawa Hospital Research Institute, the BC Centre for Disease Control and Prevention (BCCDC), and Statistics Canada's Health Analysis and Modelling Division. The core modelling team is responsible for analytical decisions. Partner representatives, including the Canadian Partnership Against Cancer, will review key milestone outputs and the final model prior to dissemination. Roles and responsibilities are documented in the project repository. > ISPOR-SMDM: Project management; STRESS: Project planning @@ -293,13 +296,13 @@ All analyses described in this protocol are prespecified. Any deviations from th ## Resource and access considerations -Model development uses publicly available PUMF data in an open R environment hosted on GitHub. The codebase is designed to run on both PUMF and Master File data without modification, controlled by a configuration profile. Production estimates using CCHS Master Files will be produced within a Statistics Canada RDC. RDC analysis requires institutional affiliation, an approved project proposal, and compliance with Statistics Canada disclosure control protocols. The lead analyst will apply for RDC access upon protocol approval. +This study involves the secondary analysis of de-identified survey data. Analysis of CCHS Master Files will be conducted within the secure environment of Statistics Canada's Regional Data Centres, following all protocols for data privacy and disclosure control. No individual-level data that could identify respondents will be released. The use of PUMF data for model development is subject to the Statistics Canada Open License agreement. > ISPOR-SMDM: Project management; STRESS: Implementation details # 6. Ethical considerations -This study involves the secondary analysis of de-identified survey data. Analysis of CCHS Master Files will be conducted within the secure environment of Statistics Canada’s Regional Data Centres, following all protocols for data privacy and disclosure control. No individual-level data that could identify respondents will be released. The use of PUMF data for model development follows the Statistics Canada Open Licence agreement. +This study involves the secondary analysis of de-identified survey data. Analysis of CCHS Master Files will be conducted within the secure environment of Statistics Canada’s Regional Data Centres, following all protocols for data privacy and disclosure control. No individual-level data that could identify respondents will be released. The use of PUMF data for model development is subject to the Statistics Canada Open License agreement. > GATHER: Ethics; STROBE: Ethics @@ -341,11 +344,11 @@ The CCHS PUMF files are publicly accessible and available without special permis ## Canadian Community Health Survey -- Master Files -The CCHS Master Files, used for the production model estimates, are available at Statistics Canada's Research Data Centres (RDCs). Access is restricted to researchers affiliated with academic, government, or other recognized institutions. Researchers must apply for access through one of the RDCs across Canada; these secure facilities provide access to confidential microdata under the confidentiality provisions of the *Statistics Act*. Master files provide exact continuous values for key variables not available in the PUMF. Further information about access procedures, eligibility, and application requirements is available on the [Statistics Canada website](https://www.statcan.gc.ca/en/microdata/data-centres). +The CCHS Master Files, used for the production model estimates, are available at Statistics Canada's Research Data Centres (RDCs). Access is restricted to researchers affiliated with academic, government, or other recognized institutions. Researchers must apply for access through one of the RDCs across Canada; these secure facilities provide access to confidential microdata under the confidentiality provisions of the *Statistics Act*. Master files provide exact, continuous values for key variables that are not available in the PUMF. Further information about access procedures, eligibility, and application requirements is available on the [Statistics Canada website](https://www.statcan.gc.ca/en/microdata/data-centres). ## US National Health Interview Survey (NHIS) -The NHIS is the data source for the US Smoking History Generator. It is not used to validate the CSHM in this version (section 3.5); descriptive comparisons of Canadian and US cohort patterns may be reported. The NHIS and is conducted by the National Center for Health Statistics (NCHS). Public-use NHIS data files are freely available without special permissions and can be downloaded directly from the [NCHS website](https://www.cdc.gov/nchs/nhis/data-questionnaires-documentation.htm). The public-use files contain de-identified data. For researchers requiring access to more detailed data not available in the public-use files, the NCHS Research Data Center offers access to restricted-use files subject to an application process and strict confidentiality protocols. Further information is available at . +The NHIS is the data source for the US Smoking History Generator. It is not used to validate the CSHM in this version (section 3.5); descriptive comparisons of Canadian and US cohort patterns may be reported. The NHIS is conducted by the National Center for Health Statistics (NCHS). Public-use NHIS data files are freely available without special permissions and can be downloaded directly from the [NCHS website](https://www.cdc.gov/nchs/nhis/data-questionnaires-documentation.htm). The public-use files contain de-identified data. For researchers requiring access to more detailed data not available in the public-use files, the NCHS Research Data Center offers access to restricted-use files subject to an application process and strict confidentiality protocols. Further information is available at . > *Note: Any NHIS comparison analyses are planned and not yet implemented. The text below is retained as a placeholder pending integration of NHIS data into the study.* diff --git a/docs/protocol/source/full-protocol-2026-08-27-dm.docx b/docs/protocol/source/full-protocol-2026-08-27-dm.docx new file mode 100644 index 0000000000000000000000000000000000000000..46046d5888b5b17874ebfea64ad8c167995ecf73 GIT binary patch literal 64591 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 literal 0 HcmV?d00001 From 82ddd9acad9ed3afbe380af90bf39b0856d03f0e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 13:49:51 -0400 Subject: [PATCH 09/29] Protocol: docstyle subproject for Word rendering The protocol's Word configuration was lost when docs/_quarto.yml was removed in the documentation restructure (4c96354); _docstyle/page-config.json was a fossil of it. Restore it as docs/protocol/_quarto.yml, mirroring the manuscript subproject, with the original header, footer, TOC and version-history settings and the popcorn-base/pop-draft-manuscript CSS. The docstyle tooling resolves _extensions/ and _docstyle/ relative to the project directory only, so the subproject has an _extensions symlink and its own _docstyle/ sidecar. A nested _quarto.yml does not remove the protocol pages from the root website build (checked with quarto inspect and a root render). Output goes to docs/protocol/output/ (gitignored). --- .gitignore | 3 + CLAUDE.md | 14 + docs/protocol/.gitignore | 2 + docs/protocol/_docstyle/field-codes.json | 496 ++++++++++++++++++++ docs/protocol/_docstyle/page-config.json | 75 +++ docs/protocol/_docstyle/reference.docx | Bin 0 -> 9885 bytes docs/protocol/_docstyle/reference.docx.hash | 1 + docs/protocol/_docstyle/section-map.json | 122 +++++ docs/protocol/_extensions | 1 + docs/protocol/_quarto.yml | 97 ++++ 10 files changed, 811 insertions(+) create mode 100644 docs/protocol/.gitignore create mode 100644 docs/protocol/_docstyle/field-codes.json create mode 100644 docs/protocol/_docstyle/page-config.json create mode 100644 docs/protocol/_docstyle/reference.docx create mode 100644 docs/protocol/_docstyle/reference.docx.hash create mode 100644 docs/protocol/_docstyle/section-map.json create mode 120000 docs/protocol/_extensions create mode 100644 docs/protocol/_quarto.yml diff --git a/.gitignore b/.gitignore index 59563ea..050b2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ resources/ manuscript/.quarto/ **/*.quarto_ipynb + +# Protocol rendered Word output (docstyle) +docs/protocol/output/ diff --git a/CLAUDE.md b/CLAUDE.md index 32790fa..8c8df66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,19 @@ quarto render # Render manuscript to Word (docstyle) quarto render manuscript/manuscript.qmd + +# Render protocol documents to Word (docstyle subproject docs/protocol/_quarto.yml; +# output in docs/protocol/output/, gitignored) +quarto render docs/protocol/full-protocol.qmd +quarto render docs/protocol/study-summary.qmd +``` + +```r +# Round-trip PI edits from Word back into the protocol QMD (save the edited +# .docx under docs/protocol/source/ first). Check the citekeys in the diff: +# harvest re-derives keys from Zotero URIs, and items with missing/duplicate +# URIs in field-codes.json come back with wrong keys. +docstyle::docx_to_qmd("docs/protocol/source/.docx", "docs/protocol/full-protocol.qmd") ``` ## Developer context @@ -78,6 +91,7 @@ The pipeline follows the DemPoRT-V2-dev pattern (`~/github/DemPoRT-V2-dev`); its |----------|---------| | [docs/protocol/full-protocol.qmd](docs/protocol/full-protocol.qmd) | Prespecified study protocol | | [docs/protocol/study-summary.qmd](docs/protocol/study-summary.qmd) | One-page protocol summary | +| [docs/protocol/_quarto.yml](docs/protocol/_quarto.yml) | Docstyle (Word) config for the protocol documents; `_extensions` symlink and local `_docstyle/` sidecar because the docstyle tooling resolves both relative to the project directory. Uses `popcorn-base.css` + `pop-draft-manuscript.css` at the repo root | | [docs/workflow/](docs/workflow/) | Step QMDs — one per pipeline stage (Stages 1–8) | | [manuscript/manuscript.qmd](manuscript/manuscript.qmd) | Study manuscript (all numbers inline R from pipeline) | | [docs/how-to/](docs/how-to/) | Task-oriented guides | diff --git a/docs/protocol/.gitignore b/docs/protocol/.gitignore new file mode 100644 index 0000000..ad29309 --- /dev/null +++ b/docs/protocol/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/docs/protocol/_docstyle/field-codes.json b/docs/protocol/_docstyle/field-codes.json new file mode 100644 index 0000000..65e2941 --- /dev/null +++ b/docs/protocol/_docstyle/field-codes.json @@ -0,0 +1,496 @@ +{ + "documentProperties": { + "custom": [] + }, + "citations": { + "HealthCanada_SmokingMortality_2024": { + "itemData": { + "id": 80521, + "type": "document", + "citation-key": "HealthCanada_SmokingMortality_2024", + "language": "en", + "title": "Smoking and mortality", + "URL": "https://www.canada.ca/en/health-canada/services/health-concerns/tobacco/legislation/tobacco-product-labelling/smoking-mortality.html", + "author": [ + { + "literal": "Health Canada" + } + ], + "issued": { + "date-parts": [["2024"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/BIVEE78X" + ] + }, + "CSUCH_2023": { + "itemData": { + "id": 80520, + "type": "report", + "citation-key": "CanadianSubstanceUseCostsandHarmsScientificWorkingGroup_CanadianSubstanceUse_2023", + "language": "en", + "publisher": "Canadian Institute for Substance Use Research and Canadian Centre on Substance Use and Addiction", + "publisher-place": "Victoria, BC", + "title": "Canadian substance use costs and harms 2007-2020", + "URL": "https://csuch.ca/publications/csuch-report/", + "author": [ + { + "literal": "Canadian Substance Use Costs and Harms Scientific Working Group" + } + ], + "issued": { + "date-parts": [["2023"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/JERACS4C" + ] + }, + "mitra2015": { + "itemData": { + "id": 99901, + "type": "article-journal", + "citation-key": "mitra2015", + "container-title": "Health Reports", + "DOI": "10.25318/82-003-x201500614195-eng", + "ISSN": "0840-6529", + "issue": "6", + "page": "12-20", + "title": "Social determinants of lung cancer incidence in Canada: A 13-year prospective study", + "volume": "26", + "author": [ + {"family": "Mitra", "given": "Dipjyoti"}, + {"family": "Shaw", "given": "Amanda"}, + {"family": "Tjepkema", "given": "Michael"}, + {"family": "Peters", "given": "Paul"} + ], + "issued": { + "date-parts": [["2015", "6"]] + } + } + }, + "hennessy2015": { + "itemData": { + "id": 4042, + "type": "article-journal", + "citation-key": "hennessy2015", + "container-title": "Population Health Metrics", + "DOI": "10.1186/s12963-015-0057-x", + "ISSN": "1478-7954", + "issue": "1", + "page": "24", + "title": "The Population Health Model (POHEM): an overview of rationale, methods and applications", + "volume": "13", + "author": [ + {"family": "Hennessy", "given": "Deirdre A."}, + {"family": "Flanagan", "given": "William M."}, + {"family": "Tanuseputro", "given": "Peter"}, + {"family": "Bennett", "given": "Carol"}, + {"family": "Tuna", "given": "Meltem"}, + {"family": "Kopec", "given": "Jacek"}, + {"family": "Wolfson", "given": "Michael C."}, + {"family": "Manuel", "given": "Douglas G."} + ], + "issued": { + "date-parts": [["2015"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/PVTTRS4W" + ] + }, + "gauvreau2017": { + "itemData": { + "id": 3626, + "type": "article-journal", + "citation-key": "gauvreauOncoSimModelDevelopment2017", + "container-title": "Current Oncology", + "issue": "6", + "page": "401", + "title": "The OncoSim model: development and use for better decision-making in Canadian cancer control", + "volume": "24", + "author": [ + {"family": "Gauvreau", "given": "C. L."}, + {"family": "Fitzgerald", "given": "N. R."}, + {"family": "Memon", "given": "S."}, + {"family": "Flanagan", "given": "W. M."}, + {"family": "Nadeau", "given": "C."}, + {"family": "Asakawa", "given": "K."}, + {"family": "Garner", "given": "R."}, + {"family": "Miller", "given": "A. B."}, + {"family": "Evans", "given": "W. K."}, + {"family": "Popadiuk", "given": "C. M."} + ], + "issued": { + "date-parts": [["2017"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/RHRBI9I6" + ] + }, + "chaiton2021": { + "itemData": { + "id": 80251, + "type": "article-journal", + "citation-key": "Chaiton_F_2021", + "container-title": "Forecasting", + "DOI": "10.3390/forecast3020017", + "ISSN": "2571-9394", + "issue": "2", + "page": "267-275", + "title": "Tobacco endgame simulation modelling: assessing the impact of policy changes on smoking prevalence in 2035", + "volume": "3", + "author": [ + {"family": "Chaiton", "given": "Michael"}, + {"family": "Dubray", "given": "Jolene"}, + {"family": "Guindon", "given": "G. Emmanuel"}, + {"family": "Schwartz", "given": "Robert"} + ], + "issued": { + "date-parts": [["2021", 4, 13]] + } + }, + "uris": [ + "http://zotero.org/groups/5363837/items/7HY84EER" + ] + }, + "beland2002": { + "itemData": { + "id": 7459, + "type": "article-journal", + "citation-key": "belandCanadianCommunityHealth2002", + "container-title": "Health Reports", + "issue": "2", + "page": "9-14", + "title": "Canadian Community Health Survey - Methodological overview", + "volume": "13", + "author": [ + {"family": "Beland", "given": "Y."} + ], + "issued": { + "date-parts": [["2002"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/AJA8XSK2" + ] + }, + "kopasker2023": { + "itemData": { + "id": 47313, + "type": "article-journal", + "citation-key": "Kopasker_TLRH-E_2023", + "container-title": "The Lancet Regional Health - Europe", + "DOI": "10.1016/j.lanepe.2023.100758", + "ISSN": "2666-7762", + "PMID": "37876527", + "PMCID": "PMC10590730", + "title": "Microsimulation as a flexible tool to evaluate policies and their impact on socioeconomic inequalities in health", + "volume": "34", + "author": [ + {"family": "Kopasker", "given": "Daniel"}, + {"family": "Katikireddi", "given": "Srinivasa Vittal"}, + {"family": "Santos", "given": "João Vasco"}, + {"family": "Richiardi", "given": "Matteo"}, + {"family": "Bronka", "given": "Patryk"}, + {"family": "Rostila", "given": "Mikael"}, + {"family": "Cecchini", "given": "Michele"}, + {"family": "Ali", "given": "Shehzad"}, + {"family": "Emmert-Fees", "given": "Karl"}, + {"family": "Bambra", "given": "Clare"}, + {"family": "Hoven", "given": "Hanno"}, + {"family": "Backhaus", "given": "Insa"}, + {"family": "Balaj", "given": "Mirza"}, + {"family": "Eikemo", "given": "Terje Andreas"} + ], + "issued": { + "date-parts": [["2023"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/UV6ASDG2" + ] + }, + "vasquezlavin2022": { + "itemData": { + "id": 80230, + "type": "article-journal", + "citation-key": "[@Vasquez-Lavin_AE_2022]", + "container-title": "Applied Economics", + "DOI": "10.1080/00036846.2021.2019186", + "issue": "34", + "page": "3972-3988", + "title": "Assessing the use of pseudo-panels to estimate the value of statistical life", + "volume": "54", + "author": [ + {"family": "Vasquez-Lavin", "given": "Felipe"}, + {"family": "Bratti", "given": "Luna"}, + {"family": "Orrego", "given": "Sergio"}, + {"family": "Barrientos", "given": "Manuel"} + ], + "issued": { + "date-parts": [["2022"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/IZGGUTPK" + ] + }, + "Holford_AJPM_2014": { + "itemData": { + "id": 2357, + "type": "article-journal", + "citation-key": "Holford_AJPM_2014", + "container-title": "American Journal of Preventive Medicine", + "DOI": "10.1016/j.amepre.2013.10.022", + "issue": "2", + "page": "e31-7", + "title": "Patterns of birth cohort-specific smoking histories, 1965-2009", + "volume": "46", + "author": [ + {"family": "Holford", "given": "T. R."}, + {"family": "Levy", "given": "D. T."}, + {"family": "McKay", "given": "L. A."}, + {"family": "Clarke", "given": "L."}, + {"family": "Racine", "given": "B."}, + {"family": "Meza", "given": "R."}, + {"family": "Land", "given": "S."}, + {"family": "Jeon", "given": "J."}, + {"family": "Feuer", "given": "E. J."} + ], + "issued": { + "date-parts": [["2014"]] + } + } + }, + "Manuel_HR_2020": { + "itemData": { + "id": 2358, + "type": "article-journal", + "citation-key": "Manuel_HR_2020", + "container-title": "Health Reports", + "DOI": "10.25318/82-003-x202001100002-eng", + "ISSN": "0840-6529", + "issue": "11", + "page": "16-31", + "title": "Smoking patterns based on birth-cohort-specific histories from 1965 to 2013, with projections to 2041", + "URL": "https://www150.statcan.gc.ca/n1/pub/82-003-x/2020011/article/00002-eng.htm", + "volume": "31", + "author": [ + {"family": "Manuel", "given": "Douglas G."}, + {"family": "Wilton", "given": "Andrew S."}, + {"family": "Bennett", "given": "Carol"}, + {"family": "Dass", "given": "Rohit"}, + {"family": "Laporte", "given": "Audrey"}, + {"family": "Holford", "given": "Theodore R."} + ], + "issued": { + "date-parts": [["2020"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/BIVEE78X" + ] + }, + "Tam_AJPM_2023": { + "itemData": { + "id": 80522, + "type": "article-journal", + "citation-key": "Tam_AJPM_2023", + "container-title": "American Journal of Preventive Medicine", + "DOI": "10.1016/j.amepre.2022.12.002", + "issue": "4", + "page": "S63-S71", + "title": "Patterns of birth cohort-specific smoking histories in Brazil", + "volume": "64", + "author": [ + {"family": "Tam", "given": "Jamie"}, + {"family": "Jaffri", "given": "Mohammed A."}, + {"family": "Mok", "given": "Yoonseo"}, + {"family": "Jeon", "given": "Jihyoun"}, + {"family": "Szklo", "given": "André S."}, + {"family": "Souza", "given": "Mirian C."}, + {"family": "Holford", "given": "Theodore R."}, + {"family": "Levy", "given": "David T."}, + {"family": "Cao", "given": "Pianpian"}, + {"family": "Sánchez-Romero", "given": "Luz M."}, + {"family": "Meza", "given": "Rafael"} + ], + "issued": { + "date-parts": [["2023"]] + } + } + }, + "gagne2017": { + "itemData": { + "id": 99903, + "type": "article-journal", + "citation-key": "gagne2017", + "container-title": "Canadian Journal of Public Health", + "DOI": "10.17269/CJPH.108.5895", + "ISSN": "0008-4263", + "PMID": "28910259", + "PMCID": "PMC6972049", + "issue": "3", + "page": "e331-e334", + "title": "Estimation of smoking prevalence in Canada: Implications of survey characteristics in the CCHS and CTUMS/CTADS", + "volume": "108", + "author": [ + {"family": "Gagné", "given": "Tara"} + ], + "issued": { + "date-parts": [["2017"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/FZNFHVCK" + ] + }, + "chen2020joinpoint": { + "itemData": { + "id": 99904, + "type": "article-journal", + "citation-key": "chen2020joinpoint", + "container-title": "Journal of Official Statistics", + "DOI": "10.2478/jos-2020-0003", + "PMCID": "PMC7380682", + "issue": "1", + "page": "49-62", + "title": "The Joinpoint-Jump and Joinpoint-Comparability Ratio Model for Trend Analysis with Applications to Coding Changes in Health Statistics", + "volume": "36", + "author": [ + {"family": "Chen", "given": "Huann-Sheng"}, + {"family": "Zeichner", "given": "Samantha"}, + {"family": "Anderson", "given": "Robert N."}, + {"family": "Espey", "given": "Donald K."}, + {"family": "Kim", "given": "Hyune-Ju"}, + {"family": "Feuer", "given": "Eric J."} + ], + "issued": { + "date-parts": [["2020"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/9QEJUJRX" + ] + }, + "opazobretton2022": { + "itemData": { + "id": 99905, + "type": "article-journal", + "citation-key": "opazobretton2022", + "container-title": "Addiction", + "DOI": "10.1111/add.15696", + "ISSN": "0965-2140", + "PMID": "34590368", + "issue": "5", + "page": "1392-1403", + "title": "Understanding long-term trends in smoking in England, 1972–2019: an age-period-cohort approach", + "volume": "117", + "author": [ + {"family": "Opazo Breton", "given": "Magdalena"}, + {"family": "Gillespie", "given": "Duncan"}, + {"family": "Pryce", "given": "Robert"}, + {"family": "Bogdanovica", "given": "Ilze"}, + {"family": "Angus", "given": "Colin"}, + {"family": "Brennan", "given": "Alan"}, + {"family": "Britton", "given": "John"} + ], + "issued": { + "date-parts": [["2022"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/TZ6UPAGW" + ] + }, + "wade2025": { + "itemData": { + "id": 99906, + "type": "article-journal", + "citation-key": "wade2025", + "container-title": "Statistical Methods in Medical Research", + "DOI": "10.1177/09622802241310326", + "ISSN": "0962-2802", + "PMCID": "PMC11951451", + "title": "Using Bayesian evidence synthesis to quantify uncertainty in population trends in smoking behaviour", + "author": [ + {"family": "Wade", "given": "Stephanie"}, + {"family": "Sarich", "given": "Patricia"}, + {"family": "Vaneckova", "given": "Petra"} + ], + "issued": { + "date-parts": [["2025"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/X73KCFT7" + ] + }, + "backinger2008": { + "itemData": { + "id": 99902, + "type": "article-journal", + "citation-key": "backinger2008", + "container-title": "Epidemiologic Perspectives & Innovations", + "DOI": "10.1186/1742-5573-5-8", + "ISSN": "1742-5573", + "PMID": "19055824", + "PMCID": "PMC2627846", + "page": "8", + "title": "Using the National Health Interview Survey to understand and address the impact of tobacco in the United States: past perspectives and future considerations", + "volume": "5", + "author": [ + {"family": "Backinger", "given": "Cathy L."}, + {"family": "Lawrence", "given": "Deirdre"}, + {"family": "Swan", "given": "Judith"}, + {"family": "Winn", "given": "Deborah M."}, + {"family": "Breen", "given": "Nancy"}, + {"family": "Hartman", "given": "Anne"}, + {"family": "Grana", "given": "Rachel"}, + {"family": "Tran", "given": "David"}, + {"family": "Farrell", "given": "Samantha"} + ], + "issued": { + "date-parts": [["2008"]] + } + }, + "uris": [ + "http://zotero.org/users/6858935/items/SKNEJQ5C" + ] + }, + "Meza_J_2021": { + "itemData": { + "id": 80523, + "type": "article-journal", + "citation-key": "Meza_J_2021", + "container-title": "JAMA", + "DOI": "10.1001/jama.2021.1077", + "issue": "10", + "page": "988", + "title": "Evaluation of the benefits and harms of lung cancer screening with low-dose computed tomography: modeling study for the US preventive services task force", + "volume": "325", + "author": [ + {"family": "Meza", "given": "Rafael"}, + {"family": "Jeon", "given": "Jihyoun"}, + {"family": "Toumazis", "given": "Iakovos"}, + {"family": "Ten Haaf", "given": "Kevin"}, + {"family": "Cao", "given": "Pianpian"}, + {"family": "Bastani", "given": "Mehrad"}, + {"family": "Han", "given": "Summer S."}, + {"family": "Blom", "given": "Erik F."}, + {"family": "Jonas", "given": "Daniel E."}, + {"family": "Feuer", "given": "Eric J."}, + {"family": "Plevritis", "given": "Sylvia K."}, + {"family": "De Koning", "given": "Harry J."}, + {"family": "Kong", "given": "Chung Yin"} + ], + "issued": { + "date-parts": [["2021"]] + } + } + } + } +} diff --git a/docs/protocol/_docstyle/page-config.json b/docs/protocol/_docstyle/page-config.json new file mode 100644 index 0000000..752e345 --- /dev/null +++ b/docs/protocol/_docstyle/page-config.json @@ -0,0 +1,75 @@ +{ + "footer": { + "enabled": true, + "first_page": false, + "style": "footer", + "rPr_xml": "<\/w:rPr>", + "left": "DRAFT", + "center": "", + "right": "Page {page} of {pages}" + }, + "header": { + "enabled": true, + "first_page": false, + "style": "header", + "rPr_xml": "<\/w:rPr>", + "left": "CSHM Study Protocol", + "center": "", + "right": "" + }, + "table_styles": { + "table-formal": { + "borders": { + "top": { + "val": "single", + "sz": "8", + "color": "7F7F7F" + }, + "bottom": { + "val": "single", + "sz": "8", + "color": "7F7F7F" + } + }, + "header_shading": "D9D9D9", + "header_bold": false, + "font_size_half_pts": 22 + }, + "table-grid": { + "borders": { + "top": { + "val": "single", + "sz": "8", + "color": "000000" + }, + "bottom": { + "val": "single", + "sz": "8", + "color": "000000" + }, + "left": { + "val": "single", + "sz": "8", + "color": "000000" + }, + "right": { + "val": "single", + "sz": "8", + "color": "000000" + }, + "insideH": { + "val": "single", + "sz": "8", + "color": "000000" + }, + "insideV": { + "val": "single", + "sz": "8", + "color": "000000" + } + }, + "header_bold": true, + "font_size_half_pts": 22 + } + } +} diff --git a/docs/protocol/_docstyle/reference.docx b/docs/protocol/_docstyle/reference.docx new file mode 100644 index 0000000000000000000000000000000000000000..f4bfc36fb68ae2618c6bfb71ebe24693b1a4b93c GIT binary patch literal 9885 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 literal 0 HcmV?d00001 diff --git a/docs/protocol/_docstyle/reference.docx.hash b/docs/protocol/_docstyle/reference.docx.hash new file mode 100644 index 0000000..26a7f44 --- /dev/null +++ b/docs/protocol/_docstyle/reference.docx.hash @@ -0,0 +1 @@ +35f6b93bf4138dfd8b3564314afa95f1958b243e288ab042f47f34977e8ac004 diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json new file mode 100644 index 0000000..9729398 --- /dev/null +++ b/docs/protocol/_docstyle/section-map.json @@ -0,0 +1,122 @@ +{ + "docstyle_version": "0.19.0", + "sections": [ + { + "index": 0, + "section_class": "section-body", + "para_position": 13, + "is_closing": false, + "line_numbers": "continuous", + "field_code_payload": [] + }, + { + "index": 1, + "section_class": "section-body", + "para_position": 199, + "is_closing": true, + "line_numbers": "continuous", + "field_code_payload": { + "type": "section", + "version": 2, + "line-numbers": "continuous", + "class": "section-body" + } + }, + { + "index": 2, + "section_class": "section-body", + "para_position": 199, + "is_closing": false, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "line-numbers": "continuous", + "class": "section-body-end" + } + }, + { + "index": 3, + "section_class": "section-body", + "para_position": 243, + "is_closing": true, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "page-break": true, + "class": "section-body" + } + }, + { + "index": 4, + "section_class": "section-body", + "para_position": 243, + "is_closing": false, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "class": "section-body-end" + } + }, + { + "index": 5, + "section_class": "section-body", + "para_position": 249, + "is_closing": true, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "page-break": true, + "class": "section-body" + } + }, + { + "index": 6, + "section_class": "section-body", + "para_position": 249, + "is_closing": false, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "class": "section-body-end" + } + }, + { + "index": 7, + "section_class": "section-body", + "para_position": 254, + "is_closing": true, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "page-break": true, + "class": "section-body" + } + }, + { + "index": 8, + "section_class": "final-cascade", + "para_position": null, + "is_closing": true, + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "class": "section-body-end" + } + } + ], + "body_section": { + "line_numbers": "none", + "field_code_payload": { + "type": "section", + "version": 2, + "class": "section-body-end" + } + } +} diff --git a/docs/protocol/_extensions b/docs/protocol/_extensions new file mode 120000 index 0000000..74119e3 --- /dev/null +++ b/docs/protocol/_extensions @@ -0,0 +1 @@ +../../_extensions \ No newline at end of file diff --git a/docs/protocol/_quarto.yml b/docs/protocol/_quarto.yml new file mode 100644 index 0000000..ee182d2 --- /dev/null +++ b/docs/protocol/_quarto.yml @@ -0,0 +1,97 @@ +# Docstyle subproject for the protocol documents (Word output). +# The website build at the repo root still includes these QMDs (a nested +# _quarto.yml does not exclude them); this file applies only when a protocol +# QMD is rendered directly: +# quarto render docs/protocol/full-protocol.qmd # -> docs/protocol/output/*.docx +# Round-trip after Word edits: +# docstyle::docx_to_qmd("docs/protocol/source/.docx", "docs/protocol/full-protocol.qmd") +# The docstyle tooling resolves _extensions/ and _docstyle/ relative to this +# directory, hence the _extensions symlink and the local _docstyle/ sidecar. +project: + type: default + output-dir: output + pre-render: _extensions/docstyle/generate-reference.R + post-render: _extensions/docstyle/update-field-codes.R + +bibliography: ../../references.bib + +format: + docstyle-docx: + toc: false + number-sections: false + reference-doc: _docstyle/reference.docx + +docstyle: + css: + - ../../popcorn-base.css + - ../../pop-draft-manuscript.css + sidecar-dir: _docstyle + header: + enabled: true + left: "CSHM Study Protocol" + first-page: false + style: header + footer: + enabled: true + left: "DRAFT" + right: "Page {page} of {pages}" + first-page: false + style: footer + toc: + title: "Table of contents" + levels: "1-3" + author-plate: + enabled: false + version-history: + enabled: true + title: "Version history" + style: "table-formal" + +authors: + - name: + given: "Douglas" + family: "Manuel" + email: "dmanuel@ohri.ca" + orcid: "0000-0003-0912-0845" + corresponding: true + affiliations: + - ref: ohri + - name: + given: "Rafael" + family: "Meza" + orcid: "0000-0002-1076-5037" + affiliations: + - ref: bccancer + - ref: ubc + - name: + given: "Rochelle E." + family: "Garner" + email: "rochelle.garner@statcan.gc.ca" + affiliations: + - ref: statscan + - name: + given: "Maikol" + family: "Diasparra" + email: "maikol.diasparra@statcan.gc.ca" + affiliations: + - ref: statscan + +affiliations: + - id: ohri + name: "Ottawa Hospital Research Institute" + city: "Ottawa" + country: "Canada" + - id: bccancer + name: "BC Cancer Research Institute" + city: "Vancouver" + country: "Canada" + - id: ubc + name: "University of British Columbia" + department: "School of Population and Public Health" + city: "Vancouver" + country: "Canada" + - id: statscan + name: "Statistics Canada" + department: "Health Analysis Division" + city: "Ottawa" + country: "Canada" From 1e1193878d7f12018e919d750fde6ae21615cc80 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 13:52:11 -0400 Subject: [PATCH 10/29] Task 1.0: PI ratifies the established-smoker gate and immigrant censoring; specification fully decided --- docs/development/estimand-specification.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 1319c74..fd008ef 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -1,7 +1,7 @@ # Smoking states and transitions: analysis specification **Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). -**Status:** ratified 2026-08-27 (PI), revised the same day after external review. One item marked **[PI to ratify]** restates a rule inherited from Manuel et al. (2020) and needs the PI's explicit decision. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. +**Status:** ratified in full by the PI, 2026-08-27, after external review: the established-smoker gate (100 cigarettes; experimental smokers are Never) and the other state gates, the per-transition treatment of immigrants (censored before arrival in Canada), the same-age rule, and the 2001 decision. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. ## 1. Why this document exists @@ -11,7 +11,7 @@ The pipeline built its initiation model on one definition of a smoker (anyone wh Each person is in exactly one state at each age. -**The established-smoker gate [PI to ratify].** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. +**The established-smoker gate (ratified 2026-08-27).** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. | State | Definition | How it is observed at survey | |---|---|---| @@ -72,7 +72,7 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat | Item | Decision | Source | |---|---|---| | State model | Established-smoking model: Never, Current, Former | Adjudication A1, 2026-08-07 | -| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020, per A1 -- **[PI to ratify]** | +| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020; PI decision, 2026-08-27 | | 2001 cycle | Complete-cessation timing is NA(c); imputed | PI decision, 2026-08-27 | | Interval convention | Age row = year from the *a*-th birthday; event row in risk set; initiation before cessation within a year; survey year included as a full year | Specified here, 2026-08-27 | | Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | @@ -80,7 +80,7 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat | Durability | Two years; more recent quitters are current at survey | Protocol 3.4.1 | | Same-age rule | One-year spell (primary); exclusion (sensitivity) | PI decision, 2026-08-27 | | Relapse | Not modelled; sensitivity analysis | Protocol 3.4.1 | -| Immigration entry | Per-transition delayed entry (section 5); PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1 | +| Immigration entry | Censored before arrival in Canada; per-transition delayed entry (section 5); PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1; PI decision, 2026-08-27 | ## 8. What changes in the pipeline because of this note From bbbf5b850a2780274e02dabb6e49b682403ac314 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:06:01 -0400 Subject: [PATCH 11/29] Protocol: second round of PI text edits; Word spacing and list fix Text edits from the second review of the Word render merged into the QMD (same 0.4.1 entry; history description updated). The edited .docx is kept under docs/protocol/source/. The "STROBE: Potential bias" placeholder sat between two items of a nested list, so Pandoc turned the remaining items into an indented code block (literal ** and hard-wrapped text in Word). The placeholder now follows the list, combined with the STRESS tag. New docs/protocol/protocol.css layers spacing over the base theme: more space above level-1 and level-2 headings, space before and after the reporting-guideline blockquotes, and a Dateline paragraph style (custom-style div) for the date and version line under the title. --- docs/protocol/_docstyle/reference.docx | Bin 9885 -> 9943 bytes docs/protocol/_docstyle/reference.docx.hash | 2 +- docs/protocol/_docstyle/section-map.json | 14 ++++----- docs/protocol/_quarto.yml | 1 + docs/protocol/full-protocol.qmd | 21 +++++++------- docs/protocol/protocol.css | 27 ++++++++++++++++++ .../source/full-protocol-2026-08-27-dm-2.docx | Bin 0 -> 62732 bytes 7 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 docs/protocol/protocol.css create mode 100644 docs/protocol/source/full-protocol-2026-08-27-dm-2.docx diff --git a/docs/protocol/_docstyle/reference.docx b/docs/protocol/_docstyle/reference.docx index f4bfc36fb68ae2618c6bfb71ebe24693b1a4b93c..06da082309d8b55eae6d97f9c458e0940894df64 100644 GIT binary patch delta 3247 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 diff --git a/docs/protocol/_docstyle/reference.docx.hash b/docs/protocol/_docstyle/reference.docx.hash index 26a7f44..07043ce 100644 --- a/docs/protocol/_docstyle/reference.docx.hash +++ b/docs/protocol/_docstyle/reference.docx.hash @@ -1 +1 @@ -35f6b93bf4138dfd8b3564314afa95f1958b243e288ab042f47f34977e8ac004 +c26c2b952fa60edcaffe6c80229d5a29fcffd2872c33b4ba1e2acafa95372560 diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json index 9729398..b85a86a 100644 --- a/docs/protocol/_docstyle/section-map.json +++ b/docs/protocol/_docstyle/section-map.json @@ -12,7 +12,7 @@ { "index": 1, "section_class": "section-body", - "para_position": 199, + "para_position": 202, "is_closing": true, "line_numbers": "continuous", "field_code_payload": { @@ -25,7 +25,7 @@ { "index": 2, "section_class": "section-body", - "para_position": 199, + "para_position": 202, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -38,7 +38,7 @@ { "index": 3, "section_class": "section-body", - "para_position": 243, + "para_position": 246, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -51,7 +51,7 @@ { "index": 4, "section_class": "section-body", - "para_position": 243, + "para_position": 246, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -63,7 +63,7 @@ { "index": 5, "section_class": "section-body", - "para_position": 249, + "para_position": 252, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -76,7 +76,7 @@ { "index": 6, "section_class": "section-body", - "para_position": 249, + "para_position": 252, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -88,7 +88,7 @@ { "index": 7, "section_class": "section-body", - "para_position": 254, + "para_position": 257, "is_closing": true, "line_numbers": "none", "field_code_payload": { diff --git a/docs/protocol/_quarto.yml b/docs/protocol/_quarto.yml index ee182d2..8b3f46e 100644 --- a/docs/protocol/_quarto.yml +++ b/docs/protocol/_quarto.yml @@ -25,6 +25,7 @@ docstyle: css: - ../../popcorn-base.css - ../../pop-draft-manuscript.css + - protocol.css sidecar-dir: _docstyle header: enabled: true diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index e80336a..6527b69 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -7,7 +7,7 @@ version-summary: version-history: - version: "0.4.1" date: "2026-08-27" - description: "Editorial revisions from PI review of the Word render (wording and plain-language edits throughout; no methodological change). Ethics statement rewritten to describe secondary analysis of de-identified data and the Statistics Canada Open Licence. Study team updated (co-led with BC Cancer Research Institute; Canadian Partnership Against Cancer named among knowledge users). Fixed NHIS typo." + description: "Editorial revisions from two rounds of PI review of the Word render (wording and plain-language edits throughout; no methodological change). Ethics statement rewritten to describe secondary analysis of de-identified data and the Statistics Canada Open Licence. Study team updated (co-led with BC Cancer Research Institute; Canadian Partnership Against Cancer named among knowledge users). Fixed NHIS typo." - version: "0.4.0" date: "2026-08-07" description: "Methodological amendments following the review (2026-08-07). New section 3.4.1 defines the smoking states and transitions: never, current, and former, following Holford et al. (2014) and Manuel et al. (2020); initiation is the first whole cigarette; cessation is stopping smoking completely; daily smoking and intensity are characteristics of current smokers; one smoking spell per person, with cessation risk starting at the age of entry; a two-year definition of cessation; the rule for same-age initiation and cessation to be set before fitting, with a sensitivity analysis. Section 3.4.3: the split of the linear trend between age, period, and cohort is stated as an explicit assumption (cohort linear trend set to zero, following Manuel et al. 2020; the alternative allocation is a sensitivity analysis); the rules that extend the model for projection are distinguished from the rule that fixes it; the spline basis is saved at fitting and reused; each fitted model is checked before use (Holford 2006). Section 3.5: uncertainty from CCHS bootstrap replicate weights in the Master-file analysis, combined across imputations and carried through to the rate tables; an approximate bootstrap for public-use analyses; simulated weights (MockData) test code only. Section 3.4.5: MPoRT remains the primary mortality adjustment if it passes life-table and simulation checks; where the adjustment enters the calculation to be settled before implementation; unadjusted results labelled as estimates among survivors. Section 3.4.2 and Appendix D: imputation follows the structure of the smoking questions, with ordering rules applied within the procedure, every imputed dataset analysed, and the number of imputations set by stability (at least five). Validation: within the CCHS, using held-out cycles and recall-consistency checks; no comparison with other surveys in this version, with reasons given. Subsections of 3.4 renumbered." @@ -31,7 +31,9 @@ version-history: description: "Initial 1-page outline." --- +::: {custom-style="Dateline"} [{{< meta version-summary.date >}}]{.date} \| Version: [{{< meta version-summary.version >}}]{.version} +::: # Abstract @@ -96,7 +98,7 @@ Individuals transition between these states according to annual probabilities of > ISPOR-SMDM: Model structure; STRESS: Conceptualization -The APC framework is used to separate temporal trends into three distinct components: age effects (biological and developmental influences), period effects (e.g., policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: +The APC framework separates temporal trends into three distinct components: age effects (biological and developmental influences), period effects (e.g., policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: 1. **Identifiability:** It addresses the fundamental APC identity ($cohort = period - age$) through constrained natural cubic splines, enabling the estimation of unique generational trends. 2. **Historical Reconstruction:** It enables the back-calculation of smoking rates for birth cohorts, using current survivors to understand historical patterns. @@ -111,7 +113,7 @@ The primary data source for the CSHM is the Canadian Community Health Survey (CC The study will use two distinct computing environments and data formats: 1. **Development Environment (Public Use Microdata Files - PUMF):** Initial model development, coding, and internal validation will be performed using PUMF data (cycles 2001--2022). These files contain about 1 million respondents and are used for the open-source implementation of the model. Continuous variables in PUMF data (e.g., age at initiation) are often grouped; we will use midpoint-estimated pseudo-continuous values for these parameters. -2. **Production Environment (Master Files):** The final model parameters will be estimated using CCHS Master Files (2001--2023) available within Statistics Canada’s secure Regional Data Centres (RDCs). Master files provide exact continuous values for key parameters for all survey years, include age, age of smoking intitation, and age of smoking cessation. +2. **Production Environment (Master Files):** The final model parameters will be estimated using CCHS Master Files (2001--2023) available within Statistics Canada’s secure Regional Data Centres (RDCs). Master files provide exact, continuous values of key parameters across all survey years, including age, age at smoking initiation, and age at smoking cessation. Secondary data sources for model calibration include: @@ -137,7 +139,7 @@ The following sociodemographic covariates will also be extracted for use in impu - **Age:** Continuous age at survey (`DHHGAGE_cont`), derived from grouped categorical responses in PUMF using midpoint imputation; exact continuous age available in Master files. - **Sex:** Binary (`DHH_SEX`), available in all cycles. - **Ethnicity:** In Master files, detailed ethnicity categories are available. In PUMF files, the analysis will use a binary white/non-white classification consistent with Manuel et al. (2020) [@Manuel_HR_2020]. -- **Immigration status:** Born in Canada vs. immigrant (`SDCFIMM`). Following Manuel et al. (2020), pre-immigration periods will be excluded from smoking history reconstruction for immigrants. In PUMF files, a binary Canadian-born/born outside Canada classification will be used; detailed immigration year is available in Master files only. +- **Immigration status:** Born in Canada vs. immigrant (`SDCFIMM`). Following Manuel et al. (2020), pre-immigration periods will be excluded from the reconstruction of smoking histories for immigrants. In PUMF files, a binary Canadian-born/born outside Canada classification will be used; detailed immigration year is available in Master files only. - **Education:** Three-category classification (less than high school, high school graduate, postsecondary graduate), used in imputation models. Note that education variables differ across cycles and between PUMF and Master files; harmonization will follow the `cchsflow` approach using `EDUDR03`. - **Province:** Used for provincial stratification and regional imputation models (`GEOGPRV`). @@ -164,8 +166,8 @@ The target population is the Canadian household population covered by the CCHS. Analysis datasets will be produced through a standardized pipeline. 1. **Cleaning:** Distribution checks and truncation of extreme values for continuous variables (e.g., smoking intensity). -2. **Imputation:** Item non-response (don't know or refused; tagged NA(b)) on smoking status, initiation age, and cessation timing will be handled with multiple imputation by chained equations (MICE), with at least *m* = 5 imputations. The final number of imputations will be chosen so that the rate-table estimates are stable across repeated runs. Only item non-response is imputed. Values that are missing by design are kept as missing: not applicable (NA(a), for example initiation age for never-smokers) and not asked in that cycle (NA(c)). Imputation follows the structure of the smoking questions. Where a person's smoking status is itself missing, status is imputed first; initiation age and quit timing are then imputed only if the imputed status makes those questions applicable, using the other smoking-history variables as predictors. Ordering rules (first cigarette no later than cessation; cessation no later than survey age) are applied within the imputation procedure, not corrected afterward. The imputation model includes the survey design variables (cycle, sampling weight), the sociodemographic variables (age, sex, province, education), and the smoking-history variables, so that it is at least as detailed as the analysis models. It also includes auxiliary variables (marital status, alcohol use, body mass index, self-rated general and mental health, life stress, community belonging, chronic conditions, and physical activity), which improve imputation and describe the study base for planned related studies. Derived variables such as pack-years are recalculated from the imputed variables rather than imputed directly. Every completed dataset is carried through person-year expansion and model fitting, and estimates are pooled across imputations (section 3.5). The full specification, including the diagnostic and sensitivity plan, is in [Appendix D: Missing data and imputation plan](appendix-imputation.qmd). -3. **Descriptive statistics:** Baseline characteristics will be reported pre-imputation (Table 1a), which discloses the amount and type of missing data, and post-imputation (Table 1b), averaged across the *m* completed datasets. Both tables present unweighted n with survey-weighted percentages (and weighted median/IQR for continuous variables) -- the unweighted n discloses the information content of each cell while the weighted statistics describe the population; a fully unweighted variant is provided as an appendix table. Tables 1 will include stratification for sex and survey year. Descriptive statistics will be examined for potential discontinuities across CCHS design eras (2001--2005, 2007--2014, 2015--2021, 2022+), which reflect major changes to the survey frame, recruitment, and collection mode [@gagne2017; @backinger2008]. Although smoking variables can be harmonized across cycles, changes to the sampling frame and mode may introduce measurement differences that are not fully correctable through harmonization alone [@chen2020joinpoint]. +2. **Imputation:** Item non-response (don't know or refused; tagged NA(b)) on smoking status, initiation age, and cessation timing will be handled with multiple imputation by chained equations (MICE), with at least *m* = 5 imputations. The final number of imputations will be chosen so that the rate-table estimates are stable across repeated runs. Only item non-response is imputed. Values that are missing by design are kept as missing: not applicable (NA(a), for example initiation age for never-smokers) and not asked in that cycle (NA(c)). Imputation follows the structure of the smoking questions. Where a person's smoking status is itself missing, status is imputed first; initiation age and quit timing are then imputed only if the imputed status makes those questions applicable, using the other smoking-history variables as predictors. Ordering rules (first cigarette no later than cessation; cessation no later than survey age) are applied within the imputation procedure, not corrected afterward. The imputation model includes the survey design variables (cycle, sampling weight), the sociodemographic variables (age, sex, province, education), and the smoking-history variables, making it at least as detailed as the analysis models. It also includes auxiliary variables (marital status, alcohol use, body mass index, self-rated general and mental health, life stress, community belonging, chronic conditions, and physical activity), which improve imputation and describe the study base for planned related studies. Derived variables such as pack-years are recalculated from the imputed variables rather than imputed directly. Every completed dataset is carried through person-year expansion and model fitting, and estimates are pooled across imputations (section 3.5). The full specification, including the diagnostic and sensitivity plan, is in [Appendix D: Missing data and imputation plan](appendix-imputation.qmd). +3. **Descriptive statistics:** Baseline characteristics will be reported pre-imputation (Table 1a), which discloses the amount and type of missing data, and post-imputation (Table 1b), averaged across the *m* completed datasets. Both tables present unweighted n with survey-weighted percentages (and weighted median/IQR for continuous variables) -- the unweighted n reflects the information content of each cell, while the weighted statistics describe the population; a fully unweighted variant is provided in the appendix. Table 1 will include stratification for sex and survey year. Descriptive statistics will be examined for potential discontinuities across CCHS design eras (2001--2005, 2007--2014, 2015--2021, 2022+), which reflect major changes to the survey frame, recruitment, and collection mode [@gagne2017; @backinger2008]. Although smoking variables can be harmonized across cycles, changes to the sampling frame and mode may introduce measurement differences that are not fully correctable through harmonization alone [@chen2020joinpoint]. ### 3.4.3 Age-period-cohort modelling @@ -194,7 +196,7 @@ We will use a dual approach to characterize smoking intensity (cigarettes per da To address survival bias (ever-smokers having lower survival to survey date than never-smokers), we will apply mortality adjustments using the Mortality Population Risk Tool (MPoRT) [@Manuel_HR_2020]. MPoRT weights are adjusted for age, sex, smoking status, years since quitting, and immigration status. For each respondent, the one-year probability of death for each historical year up to the survey date is calculated; the survival-bias weight is the proportion of ever-smokers who would have died before the survey date. This adjustment ensures that the reconstructed historical prevalence reflects the original population rather than only the survivors, following the approach of Manuel et al. (2020) [@Manuel_HR_2020]. -The exact form of the adjustment will be documented and reviewed before it is implemented. The main open question is where the adjustment enters the calculation: as an adjustment to each respondent's weight before the APC models are fitted (as described above), or as a correction to the fitted transition probabilities afterward (as in the original Ontario code). The two approaches do not necessarily give the same answer, and whichever is chosen must apply the correction once only. Before use, the adjustment will be checked in three ways: predicted deaths by age, sex, and calendar year compared with Canadian life tables; the size and stability of the adjustment factors, with limits set in advance; and recovery of known rates from simulated cohorts in which smokers die at higher rates (section 3.5). MPoRT remains the primary method if it passes these checks [@Manuel_HR_2020]. Until an adjustment is in place, unadjusted results are labelled as estimates among survivors and are not presented as birth-cohort smoking histories. +The exact form of the adjustment will be documented and reviewed before it is implemented. The main open question is where the adjustment enters the calculation: as an adjustment to each respondent's weight before the APC models are fitted (as described above), or as a correction to the fitted transition probabilities afterward (as in the original Ontario code). The two approaches do not necessarily yield the same answer, and whichever is chosen must apply the correction only once. Before use, the adjustment will be checked in three ways: predicted deaths by age, sex, and calendar year compared with Canadian life tables; the size and stability of the adjustment factors, with limits set in advance; and recovery of known rates from simulated cohorts in which smokers die at higher rates (section 3.5). MPoRT remains the primary method if it passes these checks [@Manuel_HR_2020]. Until an adjustment is in place, unadjusted results are labelled as estimates among survivors and are not presented as birth-cohort smoking histories. A sensitivity analysis will be conducted using the Peto approach -- a constant mortality risk ratio by smoking status -- consistent with the original Holford et al. (2014) US implementation [@Holford_AJPM_2014]. This quantifies the influence of the mortality adjustment method on historical prevalence estimates. @@ -233,16 +235,13 @@ The model will be validated through: 4. **Sensitivity testing and measurement error:** - **Self-report bias:** Assessing the impact of misclassification using Simulation Extrapolation (SIMEX) informed by CHMS biomarker (cotinine) data. Corrected parameters will be propagated through the simulation engine to quantify the impact on final projections. Consideration will also be given to sensitivity testing for measurement differences attributable to CCHS survey design changes across eras. - -> STROBE: Potential bias - - **Recall bias and relapse:** Quantifying the impact of CCHS’s single-event history capture (missing relapse cycles) by comparing net transition rates against observed longitudinal dynamics in the National Population Health Survey (NPHS) panel. - **Back-casting comparison:** Reconstructing smoking prevalence for the 1994--2000 period and comparing against observed NPHS data -- interpreted as a consistency check rather than validation, since NPHS carries its own frame and measurement error structure (see §3.5 item 2). - **Model specification:** Testing the sensitivity of results to spline knot placement and alternative constraints for the period effects in the APC models. We will also compare two spline implementations: natural splines (`splines2::nsp()`, primary analysis) versus restricted cubic splines (`splines2::rcs()`), which may better characterize the boundary behaviour of period and cohort effects. - **Mortality adjustment method:** The primary analysis uses the MPoRT algorithm for mortality adjustment. We will conduct a sensitivity analysis using the Peto approach (a constant mortality risk ratio by smoking status), consistent with the original Holford et al. (2014) US implementation, to quantify the influence of the mortality adjustment method on historical prevalence estimates. - **Vaping transitions:** Testing alternative assumptions for younger cohorts regarding the relationship between vaping initiation and subsequent smoking (gateway, displacement, or common liability scenarios). -> STRESS: Sensitivity analysis +> STROBE: Potential bias; STRESS: Sensitivity analysis ## 3.6 Computational methods diff --git a/docs/protocol/protocol.css b/docs/protocol/protocol.css new file mode 100644 index 0000000..132e558 --- /dev/null +++ b/docs/protocol/protocol.css @@ -0,0 +1,27 @@ +/* Protocol-specific Word styling. Layered after popcorn-base.css and + pop-draft-manuscript.css (later files win). Spacing only: fonts and + colours come from the base theme. */ + +/* More air above section headings */ +h1 { + margin-top: 24pt; + margin-bottom: 6pt; +} + +h2 { + margin-top: 16pt; + margin-bottom: 4pt; +} + +/* Reporting-guideline placeholders (blockquotes): separate from body text */ +blockquote { + margin-top: 8pt; + margin-bottom: 8pt; +} + +/* Date | Version line under the title (::: {custom-style="Dateline"}) */ +.dateline { + text-align: center; + margin-top: 12pt; + margin-bottom: 12pt; +} diff --git a/docs/protocol/source/full-protocol-2026-08-27-dm-2.docx b/docs/protocol/source/full-protocol-2026-08-27-dm-2.docx new file mode 100644 index 0000000000000000000000000000000000000000..c62b0184b7b0a0678bee8b3aad5dd851f60d0416 GIT binary patch literal 62732 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�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 Date: Thu, 27 Aug 2026 14:33:52 -0400 Subject: [PATCH 12/29] Record study decisions from public issue #5 (protocol 0.4.1) PUMF initiation-age floor stays at 13 (the 5-11 category, midpoint 8, is too coarse to date entry; Master uses exact ages with a floor of 8); the first CCHS cycle is assigned survey year 2001 for cohort assignment. Recorded in config comments and protocol sections 3.2 and 3.3 with a version-history entry. No change to methods or code. --- config.yml | 7 ++++--- docs/protocol/full-protocol.qmd | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/config.yml b/config.yml index aaa0267..e7d968d 100644 --- a/config.yml +++ b/config.yml @@ -156,8 +156,9 @@ default: age_first_cigarette: pumf: var: age_first_cigarette # Age smoked first whole cigarette (midpoint-estimated) - min: 13 # Study floor — NOTE: SMKG01C_cont has a 5-11 category (midpoint 8); - # 13 excludes it. Lowering to 8 is an open study decision. + min: 13 # Study floor (PI decision 2026-08-27, public issue #5): the PUMF + # 5-11 category (midpoint 8) is too coarse to date entry, so the + # PUMF floor stays at 13; Master uses exact ages with a floor of 8. max: 100 master: var: age_first_cigarette # Exact age from SMK_01C @@ -315,7 +316,7 @@ default: # Survey year lookup: integer year per SurveyCycle code (1–11) # 2-year cycles use midpoint year (e.g. 2008 for 2007-08) cycle_survey_years: - "1": 2001 + "1": 2001 # CCHS 1.1 collected Sept 2000 to Nov 2001; assigned 2001 (PI decision 2026-08-27, issue #5) "2": 2003 "3": 2005 "4": 2008 # 2007-08 diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index e5a0abd..d6abf9d 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -2,9 +2,12 @@ title: "A Canadian Smoking Histories Model: a study protocol to generate smoking cohorts from 1940 and project to 2050" status: "Draft" version-summary: - date: "2026-08-07" - version: "0.4.0" + date: "2026-08-27" + version: "0.4.1" version-history: + - version: "0.4.1" + date: "2026-08-27" + description: "Two study decisions recorded (public issue #5): the PUMF initiation-age floor stays at 13 because the 5-11 category is too coarse to date entry, with Master files using exact ages and a floor of 8 (section 3.3); the first CCHS cycle is assigned survey year 2001 for cohort assignment (section 3.2). No change to methods." - version: "0.4.0" date: "2026-08-07" description: "Methodological amendments following the review (2026-08-07). New section 3.4.1 defines the smoking states and transitions: never, current, and former, following Holford et al. (2014) and Manuel et al. (2020); initiation is the first whole cigarette; cessation is stopping smoking completely; daily smoking and intensity are characteristics of current smokers; one smoking spell per person, with cessation risk starting at the age of entry; a two-year definition of cessation; the rule for same-age initiation and cessation to be set before fitting, with a sensitivity analysis. Section 3.4.3: the split of the linear trend between age, period, and cohort is stated as an explicit assumption (cohort linear trend set to zero, following Manuel et al. 2020; the alternative allocation is a sensitivity analysis); the rules that extend the model for projection are distinguished from the rule that fixes it; the spline basis is saved at fitting and reused; each fitted model is checked before use (Holford 2006). Section 3.5: uncertainty from CCHS bootstrap replicate weights in the Master-file analysis, combined across imputations and carried through to the rate tables; an approximate bootstrap for public-use analyses; simulated weights (MockData) test code only. Section 3.4.5: MPoRT remains the primary mortality adjustment if it passes life-table and simulation checks; where the adjustment enters the calculation to be settled before implementation; unadjusted results labelled as estimates among survivors. Section 3.4.2 and Appendix D: imputation follows the structure of the smoking questions, with ordering rules applied within the procedure, every imputed dataset analysed, and the number of imputations set by stability (at least five). Validation: within the CCHS, using held-out cycles and recall-consistency checks; no comparison with other surveys in this version, with reasons given. Subsections of 3.4 renumbered." @@ -103,7 +106,7 @@ The APC framework is implemented to separate temporal trends into three distinct ## 3.2 Data sources -The primary data source for the CSHM is the Canadian Community Health Survey (CCHS), a national cross-sectional survey conducted by Statistics Canada. We will use all available survey cycles from 2001 to 2023. The CCHS employs a multistage stratified cluster design to provide representative estimates for the Canadian population aged 12 years and older, excluding individuals living on reserves, full-time members of the Canadian Forces, and institutionalized populations [@beland2002]. +The primary data source for the CSHM is the Canadian Community Health Survey (CCHS), a national cross-sectional survey conducted by Statistics Canada. We will use all available survey cycles from 2001 to 2023. Each cycle is assigned a survey year for cohort assignment (birth cohort = survey year minus age); the first cycle (CCHS 1.1, collected from September 2000 to November 2001) is assigned 2001. The CCHS employs a multistage stratified cluster design to provide representative estimates for the Canadian population aged 12 years and older, excluding individuals living on reserves, full-time members of the Canadian Forces, and institutionalized populations [@beland2002]. The study will use two distinct computing environments and data formats: @@ -125,7 +128,7 @@ Compared to the US National Health Interview Survey (NHIS) used in existing CISN Key smoking parameters to be extracted and harmonized include: - **Smoking status:** Classified into six categories: daily, occasional (former-daily), occasional (never-daily), former-daily, former-occasional, and never smoker. The distinction between former-daily and never-daily occasional smokers allows for more precise characterization of the cessation process and tobacco "dose" over the life course. For the APC initiation and cessation models, these will be collapsed into Never, Current, and Former smokers. -- **Age at initiation:** The age at which the respondent first smoked a whole cigarette (`age_first_cigarette`) and the age at which they started smoking daily (`age_start_smoking`). In PUMF data, these are derived from categorical variables using midpoint imputation. +- **Age at initiation:** The age at which the respondent first smoked a whole cigarette (`age_first_cigarette`) and the age at which they started smoking daily (`age_start_smoking`). In PUMF data, these are derived from categorical variables using midpoint imputation. The lowest PUMF category (ages 5 to 11, midpoint 8) is too coarse to date entry, so the PUMF analysis uses a floor of 13 and the Master-file analysis uses exact ages with a floor of 8; the two are reconciled in the Master-file analysis. - **Time since cessation:** The number of years since the respondent last smoked (`time_quit_smoking`), used to calculate the age at cessation. - **Smoking intensity:** The number of cigarettes smoked per day (CPD). Unlike the NHIS, the CCHS allows us to distinguish the intensity profiles of occasional smokers who were previously daily smokers from those who have always smoked occasionally. From 1d8db0d8e83962b11ddcb17b894f75c1ffbad820 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 17:30:35 -0400 Subject: [PATCH 13/29] Protocol: drop redundant page-break div before Background The body section already starts with a nextPage section break; the nested ::: page-break div added a second break in an otherwise empty paragraph, producing a blank page 2 in the Word render. --- docs/protocol/_docstyle/section-map.json | 34 ++++++++++++------------ docs/protocol/full-protocol.qmd | 2 -- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json index b85a86a..b96796a 100644 --- a/docs/protocol/_docstyle/section-map.json +++ b/docs/protocol/_docstyle/section-map.json @@ -12,46 +12,46 @@ { "index": 1, "section_class": "section-body", - "para_position": 202, + "para_position": 201, "is_closing": true, "line_numbers": "continuous", "field_code_payload": { "type": "section", "version": 2, - "line-numbers": "continuous", - "class": "section-body" + "class": "section-body", + "line-numbers": "continuous" } }, { "index": 2, "section_class": "section-body", - "para_position": 202, + "para_position": 201, "is_closing": false, "line_numbers": "none", "field_code_payload": { "type": "section", "version": 2, - "line-numbers": "continuous", - "class": "section-body-end" + "class": "section-body-end", + "line-numbers": "continuous" } }, { "index": 3, "section_class": "section-body", - "para_position": 246, + "para_position": 245, "is_closing": true, "line_numbers": "none", "field_code_payload": { "type": "section", "version": 2, - "page-break": true, - "class": "section-body" + "class": "section-body", + "page-break": true } }, { "index": 4, "section_class": "section-body", - "para_position": 246, + "para_position": 245, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -63,20 +63,20 @@ { "index": 5, "section_class": "section-body", - "para_position": 252, + "para_position": 251, "is_closing": true, "line_numbers": "none", "field_code_payload": { "type": "section", "version": 2, - "page-break": true, - "class": "section-body" + "class": "section-body", + "page-break": true } }, { "index": 6, "section_class": "section-body", - "para_position": 252, + "para_position": 251, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -88,14 +88,14 @@ { "index": 7, "section_class": "section-body", - "para_position": 257, + "para_position": 256, "is_closing": true, "line_numbers": "none", "field_code_payload": { "type": "section", "version": 2, - "page-break": true, - "class": "section-body" + "class": "section-body", + "page-break": true } }, { diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index 6527b69..5e7a00c 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -56,8 +56,6 @@ Using harmonized data from more than 1 million respondents in the Canadian Commu The model will be publicly accessible and used for policy evaluation and disease modelling in Canada, including projecting future smoking patterns and evaluating historic or proposed tobacco policy :::: {.section-body line-numbers="continuous"} -::: page-break -::: # 1. Background From afaad3f45d36960fb60463ba364725c6dbdb5f0e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 17:51:02 -0400 Subject: [PATCH 14/29] Protocol: PI edits to the QMD (background, objectives, APC framework wording) --- docs/protocol/full-protocol.qmd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index 5e7a00c..764556d 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -63,7 +63,7 @@ Smoking behaviour varies across birth cohorts in ways that affect projections of The Smoking History Generator (SHG) framework, developed by the National Cancer Institute (NCI) CISNET Smoking Working Group, characterizes these patterns in the United States using Age-Period-Cohort (APC) models [@Holford_AJPM_2014]. This approach was adapted for Ontario [@Manuel_HR_2020], demonstrating that Canadian Community Health Survey (CCHS) data can be used to reconstruct birth-cohort-specific smoking histories from 1965 to 2013. A pan-Canadian equivalent covering all provinces and territories has not yet been developed. -Three gaps motivate the CSHM. First, Canada’s tobacco control landscape occurs at both the national and regional levels, with provincial variation in taxation, smoke-free legislation, and cessation support. Second, Canadian smoking initiation and cessation patterns differ from those in other countries, particularly in the timing of peak prevalence and the rate of decline among younger cohorts. Third, Canadian health policy models (e.g., OncoSim, POHEM, SimSmoke) rely on behaviour transition parameters from the 1994--2004 National Population Health Survey (NPHS), now over 20 years old, with no planned replacement [@hennessy2015; @gauvreau2017; @chaiton2021]; these parameters no longer reflect contemporary patterns, particularly among younger cohorts, immigrant populations, and lower-income groups. The CCHS [@beland2002], with over 1.4 million respondents and annual data collection since 2001, provides an unparalleled opportunity to address this gap. The methods and infrastructure developed here build on those of other similar studies [@kopasker2023; @vasquezlavin2022]. This study addresses these gaps by introducing an open-source framework for generating smoking histories across all Canadian jurisdictions. +Three gaps motivate the CSHM. First, Canada’s tobacco control landscape occurs at both the national and regional levels, with provincial variation in taxation, smoke-free legislation, and cessation support. Second, Canadian smoking initiation and cessation patterns differ from those in other countries, particularly in the timing of peak prevalence and the rate of decline among younger cohorts. Third, Canadian health policy models (e.g., OncoSim, POHEM, SimSmoke) rely on behaviour transition parameters from the 1994--2004 National Population Health Survey (NPHS), now over 20 years old, with no planned replacement [@hennessy2015; @gauvreau2017; @chaiton2021]. These parameters no longer reflect contemporary patterns, particularly among younger cohorts, immigrant populations, and lower-income groups. The CCHS [@beland2002], with over 1.4 million respondents and annual data collection since 2001, provides an unparalleled opportunity to address this gap. The methods and infrastructure developed here build on those of other similar studies [@kopasker2023; @vasquezlavin2022]. This study addresses these gaps by introducing an open-source framework for generating smoking histories across all Canadian jurisdictions. # 2. Objectives @@ -76,7 +76,7 @@ This study will develop a Canadian Smoking Histories Model (CSHM) that describes 3. **Project future smoking prevalence:** Model future trends in smoking prevalence and related parameters through 2050 under status quo policy conditions. 4. **Develop an open-access model:** Create a publicly-accessible, reproducible R-based model that can be used and updated by researchers, policy analysts, and health system planners. -The model will provide demographic and geographic estimates (by single year of age and birth cohort) to interface with existing population health models. Historical reconstruction will cover the period 1965--2023, with projections extending from 2024 to 2050. Analysis will be conducted at the provincial level, with national estimates derived from aggregated provincial data. +The model will provide demographic and geographic estimates (by single year of age and birth cohort) to interface with existing population health models and support smoking projections and the evaluation of smoking preventive interventions, amongst other uses Historical reconstruction will cover the period 1965--2023, with projections extending from 2024 to 2050. Analysis will be conducted at the provincial level, with national estimates derived from aggregated provincial data. # 3. Methods @@ -96,11 +96,11 @@ Individuals transition between these states according to annual probabilities of > ISPOR-SMDM: Model structure; STRESS: Conceptualization -The APC framework separates temporal trends into three distinct components: age effects (biological and developmental influences), period effects (e.g., policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: +The APC framework separates temporal trends into three distinct components: age effects (developmental influences), period effects (e.g., policy shifts or social norms), and cohort effects (generational differences). This framework is used for three reasons: -1. **Identifiability:** It addresses the fundamental APC identity ($cohort = period - age$) through constrained natural cubic splines, enabling the estimation of unique generational trends. -2. **Historical Reconstruction:** It enables the back-calculation of smoking rates for birth cohorts, using current survivors to understand historical patterns. -3. **Survival Bias Mitigation:** It provides a method to correct for differential mortality (the "healthy survivor" effect), ensuring that estimated historical initiation and cessation rates reflect the original population rather than only those who survived to be surveyed. +1. **Identifiability:** It addresses the fundamental APC identity ($cohort = period - age$) through cubic splines, enabling the estimation of unique generational trends. +2. **Historical reconstruction:** It enables the back-calculation of smoking rates for birth cohorts, using current survivors to understand historical patterns. +3. **Survival bias mitigation:** It provides a method to correct for differential mortality (the "healthy survivor" effect), ensuring that estimated historical initiation and cessation rates reflect the original population rather than only those who survived to be surveyed. > ISPOR-SMDM: Analytical approach; GATHER: Methods overview From 83e38977db55c2d849143faa2c67e17b08270ee6 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 17:51:02 -0400 Subject: [PATCH 15/29] Protocol: author plate; move the three citeproc citations to Zotero fields Enable the docstyle author plate (authors and affiliations from _quarto.yml; list and order TBA, more authors to come) with the ::: author-plate ::: placeholder under the date line. Holford_SM_2006, Rao_SM_1992 and StatCan_CCHS_UserGuide_2022 were absent from field-codes.json, so Pandoc citeproc rendered them as plain text and appended its own bibliography after the version history. Added their CSL data (converted from references.bib) so all 26 citations are Zotero fields and the stray reference list disappears. --- docs/protocol/_docstyle/field-codes.json | 55 ++++++++++++++++++++++++ docs/protocol/_docstyle/section-map.json | 16 +++---- docs/protocol/_quarto.yml | 7 ++- docs/protocol/full-protocol.qmd | 6 +++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/docs/protocol/_docstyle/field-codes.json b/docs/protocol/_docstyle/field-codes.json index 65e2941..6c83ca9 100644 --- a/docs/protocol/_docstyle/field-codes.json +++ b/docs/protocol/_docstyle/field-codes.json @@ -491,6 +491,61 @@ "date-parts": [["2021"]] } } + }, + "Holford_SM_2006": { + "itemData": { + "id": 99907, + "type": "article-journal", + "citation-key": "Holford_SM_2006", + "DOI": "10.1002/sim.2253", + "author": [ + {"family": "Holford", "given": "Theodore R."} + ], + "container-title": "Statistics in Medicine", + "issue": "6", + "issued": { + "date-parts": [[2006]] + }, + "page": "977-993", + "title": "Approaches to fitting age-period-cohort models with unequal intervals", + "volume": "25" + } + }, + "Rao_SM_1992": { + "itemData": { + "id": 99908, + "type": "article-journal", + "citation-key": "Rao_SM_1992", + "author": [ + {"family": "Rao", "given": "J. N. K."}, + {"family": "Wu", "given": "C. F. J."}, + {"family": "Yue", "given": "K."} + ], + "container-title": "Survey Methodology", + "issue": "2", + "issued": { + "date-parts": [[1992]] + }, + "page": "209-217", + "title": "Some recent work on resampling methods for complex surveys", + "volume": "18" + } + }, + "StatCan_CCHS_UserGuide_2022": { + "itemData": { + "id": 99909, + "type": "report", + "citation-key": "StatCan_CCHS_UserGuide_2022", + "author": [ + {"literal": "Statistics Canada"} + ], + "issued": { + "date-parts": [[2023]] + }, + "publisher": "Statistics Canada", + "publisher-place": "Ottawa", + "title": "Canadian community health survey (CCHS) – annual component: User guide, 2022 microdata file" + } } } } diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json index b96796a..17d5890 100644 --- a/docs/protocol/_docstyle/section-map.json +++ b/docs/protocol/_docstyle/section-map.json @@ -4,7 +4,7 @@ { "index": 0, "section_class": "section-body", - "para_position": 13, + "para_position": 23, "is_closing": false, "line_numbers": "continuous", "field_code_payload": [] @@ -12,7 +12,7 @@ { "index": 1, "section_class": "section-body", - "para_position": 201, + "para_position": 211, "is_closing": true, "line_numbers": "continuous", "field_code_payload": { @@ -25,7 +25,7 @@ { "index": 2, "section_class": "section-body", - "para_position": 201, + "para_position": 211, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -38,7 +38,7 @@ { "index": 3, "section_class": "section-body", - "para_position": 245, + "para_position": 255, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -51,7 +51,7 @@ { "index": 4, "section_class": "section-body", - "para_position": 245, + "para_position": 255, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -63,7 +63,7 @@ { "index": 5, "section_class": "section-body", - "para_position": 251, + "para_position": 261, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -76,7 +76,7 @@ { "index": 6, "section_class": "section-body", - "para_position": 251, + "para_position": 261, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -88,7 +88,7 @@ { "index": 7, "section_class": "section-body", - "para_position": 256, + "para_position": 266, "is_closing": true, "line_numbers": "none", "field_code_payload": { diff --git a/docs/protocol/_quarto.yml b/docs/protocol/_quarto.yml index 8b3f46e..c91215f 100644 --- a/docs/protocol/_quarto.yml +++ b/docs/protocol/_quarto.yml @@ -42,12 +42,17 @@ docstyle: title: "Table of contents" levels: "1-3" author-plate: - enabled: false + enabled: true + show-orcid: true + show-email: true + affiliation-style: numbered version-history: enabled: true title: "Version history" style: "table-formal" +# Author list and order are TBA: more authors will be added before submission. +# Rendered by the ::: author-plate ::: placeholder in full-protocol.qmd. authors: - name: given: "Douglas" diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index 764556d..124c934 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -35,6 +35,12 @@ version-history: [{{< meta version-summary.date >}}]{.date} \| Version: [{{< meta version-summary.version >}}]{.version} ::: + + +::: author-plate +::: + # Abstract > STROBE: Clear summary of study components; ISPOR-SMDM: Overview of model purpose From fcbb5e780eb1ec7c67e6faf501ee715394ec1d6e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 18:05:07 -0400 Subject: [PATCH 16/29] Protocol: remove the author plate for now Author list and order are still to be decided; the authors block stays in _quarto.yml (not rendered) for when the plate is re-enabled. --- docs/protocol/_docstyle/section-map.json | 16 ++++++++-------- docs/protocol/_quarto.yml | 7 ++----- docs/protocol/full-protocol.qmd | 6 ------ 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json index 17d5890..b96796a 100644 --- a/docs/protocol/_docstyle/section-map.json +++ b/docs/protocol/_docstyle/section-map.json @@ -4,7 +4,7 @@ { "index": 0, "section_class": "section-body", - "para_position": 23, + "para_position": 13, "is_closing": false, "line_numbers": "continuous", "field_code_payload": [] @@ -12,7 +12,7 @@ { "index": 1, "section_class": "section-body", - "para_position": 211, + "para_position": 201, "is_closing": true, "line_numbers": "continuous", "field_code_payload": { @@ -25,7 +25,7 @@ { "index": 2, "section_class": "section-body", - "para_position": 211, + "para_position": 201, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -38,7 +38,7 @@ { "index": 3, "section_class": "section-body", - "para_position": 255, + "para_position": 245, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -51,7 +51,7 @@ { "index": 4, "section_class": "section-body", - "para_position": 255, + "para_position": 245, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -63,7 +63,7 @@ { "index": 5, "section_class": "section-body", - "para_position": 261, + "para_position": 251, "is_closing": true, "line_numbers": "none", "field_code_payload": { @@ -76,7 +76,7 @@ { "index": 6, "section_class": "section-body", - "para_position": 261, + "para_position": 251, "is_closing": false, "line_numbers": "none", "field_code_payload": { @@ -88,7 +88,7 @@ { "index": 7, "section_class": "section-body", - "para_position": 266, + "para_position": 256, "is_closing": true, "line_numbers": "none", "field_code_payload": { diff --git a/docs/protocol/_quarto.yml b/docs/protocol/_quarto.yml index c91215f..b0a6c5a 100644 --- a/docs/protocol/_quarto.yml +++ b/docs/protocol/_quarto.yml @@ -42,17 +42,14 @@ docstyle: title: "Table of contents" levels: "1-3" author-plate: - enabled: true - show-orcid: true - show-email: true - affiliation-style: numbered + enabled: false version-history: enabled: true title: "Version history" style: "table-formal" # Author list and order are TBA: more authors will be added before submission. -# Rendered by the ::: author-plate ::: placeholder in full-protocol.qmd. +# Not rendered while docstyle.author-plate.enabled is false. authors: - name: given: "Douglas" diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index 124c934..764556d 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -35,12 +35,6 @@ version-history: [{{< meta version-summary.date >}}]{.date} \| Version: [{{< meta version-summary.version >}}]{.version} ::: - - -::: author-plate -::: - # Abstract > STROBE: Clear summary of study components; ISPOR-SMDM: Overview of model purpose From b4ab566a0c9cd386551370ef3684075c7a131753 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 18:07:04 -0400 Subject: [PATCH 17/29] Issue #5 wording: the 5-11 category is too coarse for initiation events below 13; cessation entry still uses the midpoint; ratifies open decisions --- config.yml | 6 +++--- docs/protocol/full-protocol.qmd | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config.yml b/config.yml index e7d968d..eda5432 100644 --- a/config.yml +++ b/config.yml @@ -156,9 +156,9 @@ default: age_first_cigarette: pumf: var: age_first_cigarette # Age smoked first whole cigarette (midpoint-estimated) - min: 13 # Study floor (PI decision 2026-08-27, public issue #5): the PUMF - # 5-11 category (midpoint 8) is too coarse to date entry, so the - # PUMF floor stays at 13; Master uses exact ages with a floor of 8. + min: 13 # Initiation floor (PI decision 2026-08-27, public issue #5): the PUMF + # 5-11 category (midpoint 8) is too coarse to date initiation events + # below 13; cessation follow-up still starts at the reported midpoint. max: 100 master: var: age_first_cigarette # Exact age from SMK_01C diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index d6abf9d..710b821 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -7,7 +7,7 @@ version-summary: version-history: - version: "0.4.1" date: "2026-08-27" - description: "Two study decisions recorded (public issue #5): the PUMF initiation-age floor stays at 13 because the 5-11 category is too coarse to date entry, with Master files using exact ages and a floor of 8 (section 3.3); the first CCHS cycle is assigned survey year 2001 for cohort assignment (section 3.2). No change to methods." + description: "Two previously open study decisions ratified (public issue #5). The PUMF initiation-age floor stays at 13: the lowest PUMF category (ages 5 to 11, midpoint 8) is too coarse to date initiation events below that age, so initiation is modelled from 13 in the PUMF, while an established smoker who reports starting below 13 is still followed for cessation from the category midpoint of 8; Master files use exact ages with a floor of 8 (section 3.3). The first CCHS cycle is assigned survey year 2001 for cohort assignment (section 3.2). No other change to methods." - version: "0.4.0" date: "2026-08-07" description: "Methodological amendments following the review (2026-08-07). New section 3.4.1 defines the smoking states and transitions: never, current, and former, following Holford et al. (2014) and Manuel et al. (2020); initiation is the first whole cigarette; cessation is stopping smoking completely; daily smoking and intensity are characteristics of current smokers; one smoking spell per person, with cessation risk starting at the age of entry; a two-year definition of cessation; the rule for same-age initiation and cessation to be set before fitting, with a sensitivity analysis. Section 3.4.3: the split of the linear trend between age, period, and cohort is stated as an explicit assumption (cohort linear trend set to zero, following Manuel et al. 2020; the alternative allocation is a sensitivity analysis); the rules that extend the model for projection are distinguished from the rule that fixes it; the spline basis is saved at fitting and reused; each fitted model is checked before use (Holford 2006). Section 3.5: uncertainty from CCHS bootstrap replicate weights in the Master-file analysis, combined across imputations and carried through to the rate tables; an approximate bootstrap for public-use analyses; simulated weights (MockData) test code only. Section 3.4.5: MPoRT remains the primary mortality adjustment if it passes life-table and simulation checks; where the adjustment enters the calculation to be settled before implementation; unadjusted results labelled as estimates among survivors. Section 3.4.2 and Appendix D: imputation follows the structure of the smoking questions, with ordering rules applied within the procedure, every imputed dataset analysed, and the number of imputations set by stability (at least five). Validation: within the CCHS, using held-out cycles and recall-consistency checks; no comparison with other surveys in this version, with reasons given. Subsections of 3.4 renumbered." @@ -128,7 +128,7 @@ Compared to the US National Health Interview Survey (NHIS) used in existing CISN Key smoking parameters to be extracted and harmonized include: - **Smoking status:** Classified into six categories: daily, occasional (former-daily), occasional (never-daily), former-daily, former-occasional, and never smoker. The distinction between former-daily and never-daily occasional smokers allows for more precise characterization of the cessation process and tobacco "dose" over the life course. For the APC initiation and cessation models, these will be collapsed into Never, Current, and Former smokers. -- **Age at initiation:** The age at which the respondent first smoked a whole cigarette (`age_first_cigarette`) and the age at which they started smoking daily (`age_start_smoking`). In PUMF data, these are derived from categorical variables using midpoint imputation. The lowest PUMF category (ages 5 to 11, midpoint 8) is too coarse to date entry, so the PUMF analysis uses a floor of 13 and the Master-file analysis uses exact ages with a floor of 8; the two are reconciled in the Master-file analysis. +- **Age at initiation:** The age at which the respondent first smoked a whole cigarette (`age_first_cigarette`) and the age at which they started smoking daily (`age_start_smoking`). In PUMF data, these are derived from categorical variables using midpoint imputation. The lowest PUMF category (ages 5 to 11, midpoint 8) is too coarse to date initiation events below that age, so the PUMF initiation model uses a floor of 13; an established smoker who reports starting below 13 is still followed for cessation from the category midpoint of 8. The Master-file analysis uses exact ages with a floor of 8, and the two are reconciled there. - **Time since cessation:** The number of years since the respondent last smoked (`time_quit_smoking`), used to calculate the age at cessation. - **Smoking intensity:** The number of cigarettes smoked per day (CPD). Unlike the NHIS, the CCHS allows us to distinguish the intensity profiles of occasional smokers who were previously daily smokers from those who have always smoked occasionally. From 3c9c66cd22844b75f5e161cc0a25d34247e2c383 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:10:46 -0400 Subject: [PATCH 18/29] Task 1.3: cessation risk set per the estimand specification The cessation model now follows the established-smoking estimand. Universe: established smokers (100 or more cigarettes, smoked_100_lifetime, via a config-driven yes_code) across all ever-smoker statuses, replacing the ever-daily subset; the same gate applies to the initiation numerator, so experimental smokers are Never in both models. Event: stopping smoking completely, dated by time_quit_smoking_complete (new config key years_since_quit_complete; years_since_quit keeps the daily variable for intensity and sensitivity work). Clock: each person's risk starts at their own age at first whole cigarette (expand_denominator gains a per-person age_denom_min), replacing the floor of zero that started the clock at birth. Durability: quits under cfg$apc$cessation_durability_years (2) are censored at the quit age with no event. A quit at the entry age is one trial with the event. Missing entry age, entry after survey, quit before entry, and missing quit timing (all of 2001, where the stopped-completely questions were not asked) are excluded and counted per cycle, unweighted and weighted, in a cessation_diagnostics attribute pending imputation (task 1.8c). Worksheet roles updated. Tests cover the universe, the gate in both models, no person-year before entry, durable and recent quitters, the same-age spell, and each exclusion class. Docs updated. --- CLAUDE.md | 2 +- R/apc-model.R | 247 ++++++++++++++--------- R/config-utils.R | 14 ++ README.md | 3 +- config.yml | 29 +++ docs/workflow/7-apc-data-preparation.qmd | 7 +- tests/testthat/helper-apc.R | 45 +++-- tests/testthat/test-apc-data.R | 116 ++++++++++- worksheets/cshm-variables.csv | 147 +++++++------- 9 files changed, 414 insertions(+), 196 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 32790fa..9051611 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ python3 ~/github/cchsflow-docs/mcp-server/cli.py compare cchs2013_2014_p cchs201 The `variableStart` worksheet column uses cchsflow notation: `cchs2001_p::SMKA_01A, cchs2007_2008_p::SMK_01A, [SMK_01A]` — `_p` = PUMF, `_m` = Master, `[VAR]` = fallback name. -**Unified variables (preferred):** `age_first_cigarette`, `age_start_smoking`, `time_quit_smoking` +**Unified variables (preferred):** `age_first_cigarette` (entry), `smoked_100_lifetime` (established-smoker gate), `time_quit_smoking_complete` (cessation exit, 2003+), `age_start_smoking` and `time_quit_smoking_daily` (daily-smoking attributes) **Master-only continuous:** `SMK_01C`, `SMK_040`, `SMK_09C` / `SMK_06C` / `SMK_10C` diff --git a/R/apc-model.R b/R/apc-model.R index 427fd6f..4226d07 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -108,7 +108,12 @@ build_initiation_data <- function(data, cfg) { # 55 is the legitimate midpoint of the "50+ years" category among ever-smokers. # SMKDSTY_original categories: 1=daily, 2=occ(fmr daily), 3=always occ, 4=fmr daily, 5=fmr occ, 6=never smkdsty <- data[[status_col]] - ever_smoker <- !is.na(smkdsty) & smkdsty %in% 1:5 + # Established-smoker gate (estimand specification, section 2): only people who + # have smoked 100 or more cigarettes enter the smoking states. Experimental + # smokers (a whole cigarette, fewer than 100) are Never: at risk, no event. + gate <- data[[survey_var(cfg, "established_smoker")]] + gate_yes <- survey_code(cfg, "established_smoker", "yes_code") + ever_smoker <- !is.na(smkdsty) & smkdsty %in% 1:5 & !is.na(gate) & gate == gate_yes age_init_raw <- data[[age_col]] @@ -178,135 +183,191 @@ build_initiation_data <- function(data, cfg) { } -#' Expand person × period denominator with immediate at-risk filter +#' Expand denominator person-years #' -#' @param denom_source Data frame with: person_id, cohort, age_denom_max, weight +#' One row per person-year at risk, from each person's own start age to their +#' `age_denom_max`, restricted to the calendar window `period_range`. +#' +#' @param denom_source Data frame with: person_id, cohort, age_denom_max, weight, +#' and optionally `age_denom_min` (per-person start age; the cessation clock +#' starts at each person's own entry age). Rows without it use `min_age`. #' @param period_range Integer vector of calendar years -#' @param min_age Minimum age for being at risk +#' @param min_age Default minimum age for being at risk (used when +#' `age_denom_min` is absent or NA) #' @return Data frame: age, cohort, period, event=0, weight expand_denominator <- function(denom_source, period_range, min_age) { - # Vectorised approach: for each person, compute valid period range and expand - # This avoids materialising the full cross-product before filtering - rows <- vector("list", nrow(denom_source)) - - for (i in seq_len(nrow(denom_source))) { - p <- denom_source$person_id[i] + empty <- data.frame( + age = integer(0), cohort = integer(0), period = integer(0), + event = integer(0), weight = numeric(0) + ) + n <- nrow(denom_source) + if (n == 0) { + return(empty) + } + age_min <- if ("age_denom_min" %in% names(denom_source)) { + ifelse(is.na(denom_source$age_denom_min), min_age, denom_source$age_denom_min) + } else { + rep(min_age, n) + } + rows <- vector("list", n) + for (i in seq_len(n)) { co <- denom_source$cohort[i] am <- denom_source$age_denom_max[i] w <- denom_source$weight[i] - - if (is.na(co) || is.na(am)) next - - # Period range for this person: they are at risk from min_age to age_denom_max - p_min <- max(period_range[1], co + min_age) + if (is.na(am) || is.na(co)) next + # At risk from their own start age to age_denom_max, within the calendar window + p_min <- max(period_range[1], co + age_min[i]) p_max <- min(period_range[length(period_range)], co + am) - - if (p_max < p_min) next - - ps <- seq(p_min, p_max) + if (p_min > p_max) next + periods <- p_min:p_max rows[[i]] <- data.frame( - age = as.integer(ps - co), - cohort = co, - period = as.integer(ps), - event = 0L, - weight = w + age = periods - co, cohort = co, period = periods, event = 0L, weight = w ) } - - non_null <- rows[!vapply(rows, is.null, logical(1))] - if (length(non_null) == 0) { - return(data.frame( - age = integer(0), cohort = integer(0), period = integer(0), - event = integer(0), weight = numeric(0) - )) + rows <- rows[!vapply(rows, is.null, logical(1))] + if (length(rows) == 0) { + return(empty) } - do.call(rbind, non_null) + do.call(rbind, rows) } -#' Build combined cessation numerator + denominator dataset +#' Build the cessation numerator and denominator dataset #' -#' Restricted to ever-daily smokers using SMKDSTY_original categories: -#' 1 = daily, 2 = occasional (formerly daily), 4 = former daily. -#' Category 3 (always occasional) and 5 (former occasional) are excluded -#' because they never smoked daily. See GH#1. +#' Implements the estimand specification (docs/development/estimand-specification.md). +#' The universe is established smokers (100 or more cigarettes; SMKDSTY 1 to 5). +#' The event is stopping smoking completely, dated by `years_since_quit_complete`. +#' Each person's risk clock starts at their own age at first whole cigarette. +#' A quit counts only if it has lasted `cfg$apc$cessation_durability_years` at the +#' survey; otherwise the person is current at survey and censored at the quit age. +#' A quit at the entry age is a one-year spell: one trial, with the event. #' -#' @param data Data frame for one sex, with survey_year and cohort columns +#' People whose entry age is missing or later than the survey age, whose quit +#' precedes their entry, or whose quit timing is missing (including the 2001 +#' cycle, where complete-cessation timing was not asked) are excluded here and +#' counted. The counts, per cycle, are the `cessation_diagnostics` attribute. +#' Task 1.8c routes these people through imputation. +#' +#' @param data Analysis data (one row per respondent) with a `cohort` column #' @param cfg Config object -#' @return Long-format data frame: age, cohort, period, event, weight +#' @return Data frame with age, cohort, period, event, weight, plus the attribute +#' `cessation_diagnostics` (per-cycle counts, unweighted and weighted) build_cessation_data <- function(data, cfg) { status_col <- survey_var(cfg, "smoking_status") - quit_col <- survey_var(cfg, "years_since_quit") + gate_col <- survey_var(cfg, "established_smoker") + gate_yes <- survey_code(cfg, "established_smoker", "yes_code") + quit_col <- survey_var(cfg, "years_since_quit_complete") + init_col <- survey_var(cfg, "age_first_cigarette") age_col <- survey_var(cfg, "age") weight_col <- survey_var(cfg, "weight") - min_age <- survey_bound(cfg, "years_since_quit", "min") + cycle_col <- survey_var(cfg, "cycle") + floor_age <- survey_bound(cfg, "age_first_cigarette", "min") + durability <- cfg$apc$cessation_durability_years %||% 2 cohort_min <- cfg$apc$cohort_min period_min <- cfg$apc$period_min period_max <- cfg$apc$period_max - # SMKDSTY_original: 1=daily, 2=occ(fmr daily), 3=always occ, 4=fmr daily, 5=fmr occ, 6=never - # Cessation scope: ever-daily smokers only (1, 2, 4). Excludes always-occasional (3) - # and former-occasional (5) — they never smoked daily so cessation timing is undefined. - smkdsty_raw <- data[[status_col]] - in_scope <- !is.na(smkdsty_raw) & smkdsty_raw %in% c(1, 2, 4) & data$cohort >= cohort_min - data <- data[in_scope, ] - - smkdsty <- data[[status_col]] - years_quit <- data[[quit_col]] - age_survey <- data[[age_col]] - age_cessation <- age_survey - years_quit - - former_daily <- smkdsty == 4 - current_daily <- smkdsty %in% c(1, 2) - - # Issue 7: plausibility filter for former daily smokers. - # PUMF: time_quit_smoking_daily top-coded at 15 years; cessation ages below - # approximately (survey_age - 15) are not directly observed. Master has exact values. - # Source of truth for bounds: config.yml survey: years_since_quit: pumf/master: max. - implausible_cess <- former_daily & ( - is.na(age_cessation) | age_cessation < min_age | age_cessation < 0 + data <- data[!is.na(data$cohort) & data$cohort >= cohort_min, ] + + # Universe: established smokers. SMKDSTY_original 1 = daily, 2 = occasional + # (formerly daily), 3 = occasional (never daily), 4 = former daily, + # 5 = former occasional, 6 = never smoked. + smk <- data[[status_col]] + gate <- data[[gate_col]] + established <- !is.na(smk) & smk %in% 1:5 & !is.na(gate) & gate == gate_yes + d <- data[established, ] + smk <- d[[status_col]] + + age_init <- as.integer(round(d[[init_col]])) + age_survey <- as.integer(round(d[[age_col]])) + yrs_quit <- as.numeric(d[[quit_col]]) + age_quit <- as.integer(round(age_survey - yrs_quit)) + weight <- d[[weight_col]] + cycle <- as.character(d[[cycle_col]]) # observed cycles only; avoids NA sums for empty levels + + current <- smk %in% c(1, 2, 3) + former <- smk %in% c(4, 5) + + # Classification: each established smoker falls in exactly one group + missing_entry <- is.na(age_init) + entry_after_survey <- !missing_entry & age_init > age_survey + timing_missing <- former & is.na(yrs_quit) + quit_before_entry <- former & !is.na(age_quit) & !missing_entry & age_quit < age_init + excluded <- missing_entry | entry_after_survey | timing_missing | quit_before_entry + recent <- !excluded & former & yrs_quit < durability + durable <- !excluded & former & yrs_quit >= durability + same_age <- durable & age_quit == age_init + + groups <- list( + established = rep(TRUE, nrow(d)), + current_at_survey = !excluded & current, + durable_quitters = durable, + recent_quitters_censored = recent, + same_age_spells = same_age, + excluded_missing_entry = missing_entry, + excluded_entry_after_survey = entry_after_survey, + excluded_timing_missing = timing_missing, + excluded_quit_before_entry = quit_before_entry ) - n_implausible <- sum(implausible_cess, na.rm = TRUE) - if (n_implausible > 0) { - message( - "Excluding ", n_implausible, - " cessation rows with age_cessation < ", min_age, " or negative." + diag <- do.call(rbind, lapply(names(groups), function(g) { + sel <- groups[[g]] + if (length(sel) == 0) { + return(data.frame( + group = character(0), cycle = character(0), + n = integer(0), weighted = numeric(0), stringsAsFactors = FALSE + )) + } + agg_n <- tapply(as.integer(sel), cycle, sum) + agg_w <- tapply(weight * sel, cycle, sum) + agg_n[is.na(agg_n)] <- 0L + agg_w[is.na(agg_w)] <- 0 + data.frame( + group = g, cycle = names(agg_n), + n = as.integer(agg_n), weighted = as.numeric(agg_w), + stringsAsFactors = FALSE ) - } - - valid_cess <- former_daily & !implausible_cess & !is.na(age_cessation) - - # Numerator: one row per quitter - num <- data[valid_cess, ] - age_cess_int <- as.integer(round(age_cessation[valid_cess])) - numerator <- data.frame( - age = age_cess_int, - cohort = num$cohort, - period = num$cohort + age_cess_int, - event = rep(1L, nrow(num)), - weight = num[[weight_col]] + })) + totals <- vapply(groups, sum, numeric(1)) + message( + "Cessation risk set: ", totals[["established"]], " established smokers; ", + totals[["durable_quitters"]], " durable quitters (events); ", + totals[["recent_quitters_censored"]], " recent quitters censored; ", + totals[["same_age_spells"]], " same-age spells. Excluded pending imputation: ", + totals[["excluded_missing_entry"]], " missing entry age, ", + totals[["excluded_entry_after_survey"]], " entry after survey, ", + totals[["excluded_timing_missing"]], " missing quit timing, ", + totals[["excluded_quit_before_entry"]], " quit before entry." ) - # Denominator: current and valid former daily smokers at risk of cessation - in_denom <- valid_cess | current_daily - - age_denom_max <- ifelse( - valid_cess[in_denom], - as.integer(round(age_cessation[in_denom])) - 1L, - as.integer(round(age_survey[in_denom])) + # Numerator: one event row per durable quitter, at the quit age + numerator <- data.frame( + age = age_quit[durable], + cohort = d$cohort[durable], + period = d$cohort[durable] + age_quit[durable], + event = rep(1L, sum(durable)), + weight = weight[durable] ) + # Denominator: person-years at risk without an event. Current smokers are at + # risk from entry to the survey year (included as a full year). Durable and + # recent quitters are at risk from entry to the year before the quit year: the + # quit year is the event row for durable quitters and unobservable for recent + # quitters. A same-age spell has no denominator row; its one trial is the event. + in_denom <- !excluded & (current | durable | recent) + age_denom_max <- ifelse(current[in_denom], age_survey[in_denom], age_quit[in_denom] - 1L) denom_source <- data.frame( - person_id = seq_len(sum(in_denom)), - cohort = data$cohort[in_denom], + person_id = seq_len(sum(in_denom)), + cohort = d$cohort[in_denom], + age_denom_min = pmax(age_init[in_denom], floor_age), age_denom_max = age_denom_max, - weight = data[[weight_col]][in_denom] + weight = weight[in_denom] ) - period_range <- seq(period_min, period_max) - denominator <- expand_denominator(denom_source, period_range, min_age) + denominator <- expand_denominator(denom_source, period_range, floor_age) - rbind(numerator, denominator) + out <- rbind(numerator, denominator) + attr(out, "cessation_diagnostics") <- diag + out } diff --git a/R/config-utils.R b/R/config-utils.R index 60ccc22..624c723 100644 --- a/R/config-utils.R +++ b/R/config-utils.R @@ -22,6 +22,20 @@ survey_var <- function(cfg, key) { # Access a bound (min/max) for the active data source. # e.g. survey_bound(cfg, "age_first_cigarette", "min") → 13 (pumf) or 8 (master) +# Access a value code for the active data source, e.g. +# survey_code(cfg, "established_smoker", "yes_code") -> 1 +# (keys are named *_code because bare YAML keys such as `yes` parse as booleans) +survey_code <- function(cfg, key, code) { + entry <- cfg$survey[[key]] + if (is.null(entry)) stop("survey_code: unknown key '", key, "'") + src <- cfg$data_source %||% "pumf" + src_entry <- entry[[src]] + if (is.null(src_entry)) stop("survey_code: no '", src, "' entry for key '", key, "'") + val <- if (is.list(src_entry)) src_entry[[code]] else NULL + if (is.null(val)) stop("survey_code: no code '", code, "' for key '", key, "' source '", src, "'") + val +} + survey_bound <- function(cfg, key, bound) { entry <- cfg$survey[[key]] if (is.null(entry)) stop("survey_bound: unknown key '", key, "'") diff --git a/README.md b/README.md index 26072c1..d5b60bb 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ Variables are harmonized across CCHS cycles using the [cchsflow](https://github. - `age_first_cigarette` — age first smoked whole cigarette (Master: exact; PUMF: midpoint estimate) - `age_start_smoking` — age started smoking daily (Master: exact; PUMF: midpoint ±3 years) -- `time_quit_smoking` — years since quit smoking +- `time_quit_smoking_complete` — years since stopped smoking completely (the cessation event; 2003 onward) +- `smoked_100_lifetime` — smoked 100 or more cigarettes (defines who is a smoker in the model) ## Pipeline diff --git a/config.yml b/config.yml index eda5432..ea03711 100644 --- a/config.yml +++ b/config.yml @@ -173,6 +173,31 @@ default: var: age_start_smoking # Exact age from SMK_040 min: 8 max: 100 + # Established-smoker gate (estimand specification, section 2): at least 100 + # cigarettes in lifetime. Experimental smokers (fewer than 100) are Never. + # `yes_code` is the value meaning "smoked 100 or more" (CCHS SMK_01A: 1 = yes, 2 = no). + established_smoker: + pumf: + var: smoked_100_lifetime + yes_code: 1 + master: + var: smoked_100_lifetime + yes_code: 1 + # Cessation exit: years since stopped smoking COMPLETELY (estimand specification, + # section 3). Derived from questions first asked in 2003; in 2001 it is not asked + # (NA(c)) and is handled by the imputation path for cycle-level absence. + years_since_quit_complete: + pumf: + var: time_quit_smoking_complete # midpoint-estimated; top-coded at 15 + min: 0 + max: 15 # PUMF top-code + master: + var: time_quit_smoking_complete # exact years + min: 0 + max: 80 + # Years since stopped DAILY smoking. No longer the cessation exit (that is + # complete cessation, above); retained for the intensity model and for the + # daily-smoking sensitivity analysis. years_since_quit: pumf: var: time_quit_smoking_daily # Years since stopped daily (midpoint-estimated, top-coded at 15) @@ -238,6 +263,10 @@ default: period_max: 2022 # statscan profile overrides to 2023 projection_max: 2050 # rate tables and smoking histories projected to this year cohort_min: 1920 + # A quit counts as cessation only if it has lasted this many years at the + # survey; more recent quitters are current at survey and censored at the + # quit age (estimand specification, section 4; Holford et al. 2014). + cessation_durability_years: 2 # Spline implementation # Primary: "nsp" (natural splines via splines2::nsp()) # Sensitivity: "rcs" (restricted cubic splines via rms::rcs()) diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index cafdd49..e77ca56 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -25,9 +25,10 @@ Configuration used: | `survey$weight` | `config.yml` | Sampling weight | | `survey$smoking_status` | `config.yml` | 6-category smoking status | | `survey$age_first_cigarette` | `config.yml` | Age at first whole cigarette | -| `survey$years_since_quit` | `config.yml` | Years since quitting (former daily smokers) | +| `survey$years_since_quit_complete` | `config.yml` | Years since stopping smoking completely (the cessation exit) | +| `survey$established_smoker` | `config.yml` | 100-cigarette gate; `yes_code` defines the smoking universe | | `survey_bound(cfg, "age_first_cigarette", "min")` | `config.yml` | APC floor for initiation age (PUMF: 13, Master: 8) | -| `survey_bound(cfg, "years_since_quit", "min")` | `config.yml` | APC floor for cessation years since quit (0) | +| `apc$cessation_durability_years` | `config.yml` | A quit counts as cessation only after this many years (2) | | `apc$age_knots` | `config.yml` | `[10, 15, 20, 50, 60]` | | `apc$period_knots` | `config.yml` | `[1940, 1950, 1960, 1970, 1980]` | | `apc$cohort_knots` | `config.yml` | `[1930, 1940, 1945, 1950, 1955, 1960, 1965, 1970, 1975, 1980]` | @@ -87,7 +88,7 @@ The spline basis columns are built in Stage 8 (`build_spline_basis()`), not stor **Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weighting` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). -**APC floor ages.** Initiation probability is assumed zero before `survey_bound(cfg, "age_first_cigarette", "min")` (PUMF: 13, Master: 8). Cessation probability is assumed zero before `survey_bound(cfg, "years_since_quit", "min")` (0). In PUMF data, the practical floor for observed initiation is ~13 due to midpoint imputation of grouped categories. The Master analytical floor of 8 captures genuine early initiations. +**Who is in the cessation model, and when the clock starts.** The universe is established smokers (100 or more cigarettes in their lifetime, `smoked_100_lifetime`), whatever their current pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person is at risk from their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count: the person is current at survey and censored at the quit age. A quit at the entry age is a one-year spell. People with missing entry age or quit timing (including the whole 2001 cycle, where the stopped-completely questions were not asked) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. **Four separate data frames, not one.** Men and women are modelled separately (consistent with Manuel et al. 2020). Initiation and cessation are separate models. Keeping four data frames lets Stage 8 fit all four models in parallel using `{targets}` branching. diff --git a/tests/testthat/helper-apc.R b/tests/testthat/helper-apc.R index 593515e..b719a77 100644 --- a/tests/testthat/helper-apc.R +++ b/tests/testthat/helper-apc.R @@ -5,35 +5,46 @@ make_apc_test_data <- function(cfg, n = 100, seed = 42) { set.seed(seed) - cycles <- factor(sample(as.character(1:11), n, replace = TRUE), levels = as.character(1:11)) - ages <- round(runif(n, 25, 65)) - smkdsty <- sample(c(1, 2, 3, 4, 5, 6), n, replace = TRUE, - prob = c(0.25, 0.1, 0.1, 0.1, 0.05, 0.4)) + cycles <- factor(sample(as.character(1:11), n, replace = TRUE), levels = as.character(1:11)) + ages <- round(runif(n, 25, 65)) + smkdsty <- sample(c(1, 2, 3, 4, 5, 6), n, + replace = TRUE, + prob = c(0.25, 0.1, 0.1, 0.1, 0.05, 0.4) + ) age_first <- ifelse( smkdsty == 6, NA_real_, pmin(round(runif(n, 13, 25)), ages - 1) ) - # Former daily (cat 4) have years since quit; others NA + # Former daily (cat 4) have years since stopped daily; others NA yrs_quit <- ifelse(smkdsty == 4, round(runif(n, 1, 20)), NA_real_) + # Established-smoker gate: 100+ cigarettes (CCHS coding 1 = yes, 2 = no). + # Most ever-smokers pass; some are experimental (2); never smokers are NA(a). + smoked_100 <- ifelse(smkdsty == 6, NA_real_, ifelse(runif(n) < 0.85, 1, 2)) + # Years since stopped smoking completely: former smokers (4, 5) only. + yrs_quit_complete <- ifelse(smkdsty %in% c(4, 5), round(runif(n, 0, 20)), NA_real_) + # keep quit age at or after the entry age so the base data are internally consistent + yrs_quit_complete <- pmin(yrs_quit_complete, ages - age_first) # Simulate survey years (2002–2022 range) and cohorts survey_years <- sample(2002:2022, n, replace = TRUE) - cohorts <- survey_years - ages + cohorts <- survey_years - ages # Build data frame with placeholder names, then rename to config-resolved names df <- data.frame( - cycle = cycles, - sex = sample(1:2, n, replace = TRUE), - age = ages, - province = sample(10:60, n, replace = TRUE), - weight = round(runif(n, 50, 500)), - smoking_status = smkdsty, + cycle = cycles, + sex = sample(1:2, n, replace = TRUE), + age = ages, + province = sample(10:60, n, replace = TRUE), + weight = round(runif(n, 50, 500)), + smoking_status = smkdsty, age_first_cigarette = age_first, - years_since_quit = yrs_quit, - survey_year = survey_years, - cohort = cohorts, - stringsAsFactors = FALSE + years_since_quit = yrs_quit, + established_smoker = smoked_100, + years_since_quit_complete = yrs_quit_complete, + survey_year = survey_years, + cohort = cohorts, + stringsAsFactors = FALSE ) colnames(df) <- c( survey_var(cfg, "cycle"), @@ -44,6 +55,8 @@ make_apc_test_data <- function(cfg, n = 100, seed = 42) { survey_var(cfg, "smoking_status"), survey_var(cfg, "age_first_cigarette"), survey_var(cfg, "years_since_quit"), + survey_var(cfg, "established_smoker"), + survey_var(cfg, "years_since_quit_complete"), "survey_year", "cohort" ) diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index b32acbe..02619d0 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -82,18 +82,116 @@ test_that("build_initiation_data: denominator period within [period_min, period_ expect_true(all(denom$period <= cfg$apc$period_max)) }) -test_that("build_cessation_data: only ever-daily smokers in cessation data", { +cess_cfg <- function() { cfg <- config::get() + cfg$apc$mortality_method <- "none" + cfg +} + +# One-row respondent data frame with config-resolved column names +one_person <- function(cfg, status, smoked_100 = 1, age_first = 16, yrs_quit_complete = NA, + age = 50, survey_year = 2010, weight = 100, cycle = "5") { + df <- data.frame( + cycle = factor(cycle, levels = as.character(1:11)), sex = 1L, age = age, + province = 35L, weight = weight, smoking_status = status, + age_first_cigarette = age_first, years_since_quit = NA_real_, + established_smoker = smoked_100, years_since_quit_complete = yrs_quit_complete, + survey_year = survey_year, cohort = survey_year - age + ) + colnames(df) <- c( + survey_var(cfg, "cycle"), survey_var(cfg, "sex"), survey_var(cfg, "age"), + survey_var(cfg, "province"), survey_var(cfg, "weight"), + survey_var(cfg, "smoking_status"), survey_var(cfg, "age_first_cigarette"), + survey_var(cfg, "years_since_quit"), survey_var(cfg, "established_smoker"), + survey_var(cfg, "years_since_quit_complete"), "survey_year", "cohort" + ) + df +} + +test_that("build_cessation_data: universe is established smokers, all ever-smoker statuses", { + cfg <- cess_cfg() data <- make_apc_test_data(cfg) + result <- suppressMessages(build_cessation_data(data, cfg)) + diag <- attr(result, "cessation_diagnostics") + expect_true(is.data.frame(diag)) + established <- sum(diag$n[diag$group == "established"]) + gate <- data[[survey_var(cfg, "established_smoker")]] + smk <- data[[survey_var(cfg, "smoking_status")]] + expect_equal(established, sum(!is.na(gate) & gate == 1 & smk %in% 1:5 & data$cohort >= cfg$apc$cohort_min)) + expect_true(all(result$event %in% c(0L, 1L))) +}) + +test_that("build_cessation_data: experimental smokers (under 100 cigarettes) are outside the universe", { + cfg <- cess_cfg() + exp_smoker <- one_person(cfg, status = 4, smoked_100 = 2, age_first = 15, yrs_quit_complete = 10) + result <- suppressMessages(build_cessation_data(exp_smoker, cfg)) + expect_equal(nrow(result), 0) +}) + +test_that("build_cessation_data: no person-year precedes the person's own entry age", { + cfg <- cess_cfg() + cur <- one_person(cfg, status = 1, age_first = 22, age = 40, survey_year = 2005) + result <- suppressMessages(build_cessation_data(cur, cfg)) + expect_true(all(result$age >= 22)) + # at risk from entry through the survey year, inclusive + expect_equal(sort(result$age), 22:40) + expect_true(all(result$event == 0L)) +}) + +test_that("build_cessation_data: durable quitter has one event at the quit age and risk rows before it", { + cfg <- cess_cfg() + q <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 10, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(q, cfg)) + expect_equal(sum(result$event), 1L) + expect_equal(result$age[result$event == 1L], 40L) + expect_equal(sort(result$age[result$event == 0L]), 18:39) +}) + +test_that("build_cessation_data: recent quitter is censored at the quit age with no event", { + cfg <- cess_cfg() + r <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 1, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(r, cfg)) + expect_equal(sum(result$event), 0L) + expect_equal(max(result$age), 48L) # quit at 49; the quit year is not observed + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "recent_quitters_censored"]), 1L) +}) + +test_that("build_cessation_data: same-age initiation and cessation is one trial with the event", { + cfg <- cess_cfg() + s <- one_person(cfg, status = 5, age_first = 30, yrs_quit_complete = 20, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(s, cfg)) + expect_equal(nrow(result), 1L) + expect_equal(result$event, 1L) + expect_equal(result$age, 30L) + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "same_age_spells"]), 1L) +}) + +test_that("build_cessation_data: missing quit timing (e.g. 2001, NA(c)) is excluded and counted, not reclassified", { + cfg <- cess_cfg() + m <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = NA, age = 50, survey_year = 2001, cycle = "1") + result <- suppressMessages(build_cessation_data(m, cfg)) + expect_equal(nrow(result), 0L) + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "excluded_timing_missing"]), 1L) +}) + +test_that("build_cessation_data: a quit before entry is excluded and counted", { + cfg <- cess_cfg() + bad <- one_person(cfg, status = 4, age_first = 30, yrs_quit_complete = 30, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(bad, cfg)) + expect_equal(nrow(result), 0L) + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "excluded_quit_before_entry"]), 1L) +}) - # build_cessation_data accepts current daily (1), occ former daily (2), and former daily (4) - # smoking_status category 3 (always occasional) and 5 (former occasional) are excluded - result <- build_cessation_data(data, cfg) - # Cessation events (event=1) come from former daily smokers — we can't check - # smoking_status directly from the output, but we can verify the function runs without error - # and produces a valid data frame - expect_true(is.data.frame(result)) - expect_true(all(c("age", "cohort", "period", "event", "weight") %in% names(result))) +test_that("build_initiation_data: experimental smokers contribute no initiation event", { + cfg <- cess_cfg() + exp_smoker <- one_person(cfg, status = 3, smoked_100 = 2, age_first = 15, age = 40, survey_year = 2005) + result <- suppressMessages(build_initiation_data(exp_smoker, cfg)) + expect_equal(sum(result$event), 0L) + expect_true(nrow(result) > 0) # at risk, like a never smoker }) test_that("no missing weight in any output element", { diff --git a/worksheets/cshm-variables.csv b/worksheets/cshm-variables.csv index 592732b..8a59102 100644 --- a/worksheets/cshm-variables.csv +++ b/worksheets/cshm-variables.csv @@ -1,73 +1,74 @@ -"variable","label","labelLong","variableType","databaseStart","variableStart","subject","section","units","notes","description","purpose","version","lastUpdated","reviewNotes","status","versionNotes","role","source" -"SurveyCycle","Survey cycle","CCHS survey cycle","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","DerivedVar::[data_name]","demographics","Sociodemographics","N/A",NA,"Derived from dataset name; identifies CCHS cycle for each respondent","Identifies the CCHS cycle for each respondent. Used to derive survey year for the period component of the APC model and to label cycle-specific results.","0.1.0","2026-06-11","","",NA,"design, table1-stratifier, imputation-predictor","both" -"DHHGAGE_cont","Age (grouped continuous, years)","Age - continuous","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","[DHH_AGE]","demographics","Sociodemographics","Years",NA,"Continuous age; preferred over DHHGAGE_A for APC model where available","Continuous age in years (midpoint-estimated in PUMF). The primary age input to the APC model. Cohort is derived as survey_year - age.","0.1.0","2026-06-11","","",NA,"predictor, table1, apc-denominator, imputation-predictor","pumf" -"DHH_SEX","Sex","Sex","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","cchs2001_p::DHHA_SEX, cchs2003_p::DHHA_SEX, cchs2005_p::DHHA_SEX, [DHH_SEX]","demographics","Sociodemographics","N/A",NA,"Sex (1=Male 2=Female). APC models run separately by sex","Sex. APC initiation and cessation models are fit separately for men and women because smoking trends differ substantially by sex. Also used to stratify Table 1.","0.1.0","2026-06-11","","",NA,"model-stratifier, table1, apc-denominator, table1-stratifier, imputation-predictor","pumf" -"GEOGPRV","Province of residence","Province of residence","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","[GEOGPRV]","demographics","Sociodemographics","N/A",NA,"Province/territory of residence. Used for provincial APC estimates","Province of residence. Used for provincial APC stratification (Stage 8 subgroup). Territories are pooled due to small sample sizes.","0.1.0","2026-06-11","","",NA,"predictor, table1, imputation-predictor","pumf" -"WTS_M","Master survey weight","Survey sampling weight","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","cchs2001_p::WTSAM, cchs2003_p::WTSC_M, cchs2005_p::WTSE_M, [WTS_M]","demographics","Sociodemographics","N/A",NA,"CCHS sampling weight. Required for all prevalence estimates and weighted APC models","Survey sampling weight. Applied in APC logistic regression as a case weight to produce nationally representative estimates.","0.1.0","2026-06-11","","",NA,"design, apc-denominator, imputation-predictor","pumf" -"SDCFIMM","Immigrant status (2007-2014 only)","Immigrant status (D)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","[SDCFIMM]","demographics","Sociodemographics","N/A",NA,"Immigrant status derived (1=non-immigrant 2=immigrant 3=non-permanent resident). Used in MPoRT mortality adjustment","Immigrant status. Used in the MPoRT mortality correction to adjust for survival bias. Only available 2007-2014.","0.1.0","2026-06-11","","",NA,"predictor, table1","pumf" -"SDCGCGT","Cultural/racial origin (2007-2014 only)","Cultural or racial origin (D/G)","Categorical","cchs2001_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","cchs2001_p::SDCA_COB, [SDCGCGT]","demographics","Sociodemographics","N/A",NA,"Cultural/racial origin. Not in all cycles (NA(c) where absent). Used for subgroup analysis; not a primary APC model variable","Cultural/racial origin. Retained for potential subgroup analyses. Only available in select cycles; will be NA(c) elsewhere.","0.1.0","2026-06-11","Not available all cycles","",NA,"predictor, table1","pumf" -"EDUDR03","Education (3-cat)","Highest education level - 3 categories","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","[EDUDR03]","demographics","Sociodemographics","N/A",NA,"Highest education (3-category: less than high school / high school graduate / post-secondary). Used for subgroup analysis. cchsflow harmonized from EDUADR04/EDUCDR04/EDUEDR04/EHG2DVR3.","Education (3-category). Included for descriptive purposes and potential subgroup analysis. Not available in 2019-20 or 2022 PUMF.","0.1.0","2026-06-11","Not available 2019-20 or 2022","",NA,"predictor, table1, imputation-predictor","pumf" -"SMK_01A","Smoked 100+ cigs","Ever smoked 100 or more cigarettes in lifetime","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_01A, cchs2003_p::SMKC_01A, cchs2005_p::SMKE_01A, cchs2015_2016_p::SMK_020, cchs2017_2018_p::SMK_020, cchs2019_2020_p::SMK_020, cchs2022_p::CSS_15, cchs2023_p::CSS_15, cchs2001_m::SMKA_01A, cchs2003_m::SMKC_01A, cchs2005_m::SMKE_01A, cchs2015_2016_m::SMK_020, cchs2017_2018_m::SMK_020, cchs2019_2020_m::SMK_020, cchs2021_m::SMK_020, cchs2022_m::CSS_15, cchs2023_m::CSS_15, [SMK_01A]","smoking","Health behaviour","N/A",NA,"Gate question: ever smoked >=100 cigarettes lifetime. Combined with SMK_01B and SMK_202 for never/current/former classification","Intermediate: input to SMKDSTY_A derivation. Gate variable: ever smoked 100+ cigarettes. Combined with SMK_01B and SMK_202 to classify respondents as never/current/former smoker for the APC numerator.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMK_01B","Smoked 1 whole cig","Ever smoked a whole cigarette","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_01B, cchs2003_p::SMKC_01B, cchs2005_p::SMKE_01B, cchs2015_2016_p::SMK_025, cchs2017_2018_p::SMK_025, cchs2019_2020_p::SMK_025, cchs2022_p::CSS_05, cchs2023_p::CSS_05, cchs2001_m::SMKA_01B, cchs2003_m::SMKC_01B, cchs2005_m::SMKE_01B, cchs2015_2016_m::SMK_025, cchs2017_2018_m::SMK_025, cchs2019_2020_m::SMK_025, cchs2021_m::SMK_025, cchs2022_m::CSS_05, cchs2023_m::CSS_05, [SMK_01B]","smoking","Health behaviour","N/A",NA,"Second gate for never-smoker definition: never smoked a whole cigarette AND <100 lifetime cigarettes = never smoker","Intermediate: input to SMKDSTY_A derivation. Gate variable: ever smoked a whole cigarette. Used with SMK_01A to define never-smoker status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMK_202","Smoking type","Type of smoker presently (daily/occasional/not at all)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKA_202, cchs2003_p::SMKC_202, cchs2005_p::SMKE_202, cchs2015_2016_p::SMK_005, cchs2017_2018_p::SMK_005, cchs2019_2020_p::SMK_005, cchs2001_m::SMKA_202, cchs2003_m::SMKC_202, cchs2005_m::SMKE_202, cchs2015_2016_m::SMK_005, cchs2017_2018_m::SMK_005, cchs2019_2020_m::SMK_005, cchs2021_m::SMK_005, [SMK_202]","smoking","Health behaviour","N/A",NA,"Current smoking frequency. Combined with SMK_01A/SMK_01B to derive 3-category status: never / current / former","Intermediate: input to SMKDSTY_A derivation. Current smoking frequency (daily/occasional/not at all). Combined with SMK_01A/SMK_01B to derive 3-category smoking status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMKDSTY_original","Smoking status (6-cat)","Type of smoker derived - 6-category (cchsflow v3, original StatCan scheme)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2007_2008_p::SMKDSTY, cchs2009_2010_p::SMKDSTY, cchs2010_p::SMKDSTY, cchs2011_2012_p::SMKDSTY, cchs2012_p::SMKDSTY, cchs2013_2014_p::SMKDSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2007_2008_m::SMKDSTY, cchs2009_2010_m::SMKDSTY, cchs2009_m::SMKDSTY, cchs2010_m::SMKDSTY, cchs2011_2012_m::SMKDSTY, cchs2012_m::SMKDSTY, cchs2013_2014_m::SMKDSTY, cchs2014_m::SMKDSTY, DerivedVar::[SMK_202, SMK_05D, SMK_01A]","smoking","Health behaviour","N/A",NA,"cchsflow v3 harmonized 6-cat smoking status: 1=daily, 2=occ(fmr daily), 3=always occasional, 4=former daily, 5=former occasional, 6=never. Consistent categories across all cycles.","Primary smoking classification for APC numerator construction and Table 1. cchsflow v3: 2001-2014 pass-through from SMKDSTY; 2015-2021 derived from SMK_202, SMK_05D, SMK_01A. Not supported PUMF 2022/2023.","0.1.0","2026-06-11","Renamed from SMKDSTY_A per CEP-002 year-based naming","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"age_start_smoking","Age daily (unified)*","Age started smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG040_cont, SMK_040]","smoking","Health behaviour","Years","Raw SMKG040 absent from 2019-20 PUMF (DDI-confirmed): age_start_smoking unavailable for cchs2019_2020_p and cchs2022_p; Master covers 2001-2023. See cchsflow#185.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_040); PUMF: midpoint-estimated (SMKG040_cont). Primary daily initiation age","Age started smoking daily (unified cchsflow v3 variable). Used in the initiation APC model as an alternative or supplementary age measure.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023.","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Primary input for the cessation APC numerator. Covers former daily smokers.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"SMK_09A_cont","Yrs quit daily (PUMF)","Years since stopped smoking daily - former daily (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_09A, cchs2003_p::SMKC_09A, cchs2005_p::SMKE_09A, cchs2015_2016_p::SMK_080, cchs2017_2018_p::SMK_080, cchs2019_2020_p::SMK_080, cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09A, cchs2005_m::SMKE_09A, cchs2015_2016_m::SMK_080, cchs2017_2018_m::SMK_080, cchs2019_2020_m::SMK_080, cchs2021_m::SMK_080, cchs2023_m::SPU_25, [SMK_09A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former daily smokers. Feeder for time_quit_smoking and time_quit_smoking_daily (cchsflow v3). Not available 2022 or PUMF 2023 (SPU_25 is Master-only).","PUMF-derived continuous years since quit. Superseded by time_quit_smoking once cchsflow v3 merges. Keep for pre-v3 fallback","Intermediate: PUMF/Master midpoint feeder for the unified cessation variables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMK_06A_cont","Yrs quit occ (PUMF)","Years since stopped smoking - former occasional smokers (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_06A, cchs2003_p::SMKC_06A, cchs2005_p::SMKE_06A, cchs2015_2016_p::SMK_060, cchs2017_2018_p::SMK_060, cchs2019_2020_p::SMK_060, cchs2001_m::SMKA_06A, cchs2003_m::SMKC_06A, cchs2005_m::SMKE_06A, cchs2015_2016_m::SMK_060, cchs2017_2018_m::SMK_060, cchs2019_2020_m::SMK_060, cchs2021_m::SMK_060, cchs2023_m::SPU_10, [SMK_06A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former occasional smokers. Feeder for time_quit_smoking (cchsflow v3 falls back to it when SMK_09A_cont is not applicable). Not available 2022 or PUMF 2023.","PUMF-derived years since quit for former occasional smokers. Not covered by time_quit_smoking (daily only)","Intermediate: occasional-smoker feeder for time_quit_smoking (all former smokers).","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMKDGSTP_cont","Yrs since quit (all)","Years since quit smoking completely - all former smokers (continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_p::SMKCGSTP, cchs2005_p::SMKEGSTP, cchs2007_2008_p::SMKGSTP, cchs2009_2010_p::SMKGSTP, cchs2010_p::SMKGSTP, cchs2011_2012_p::SMKGSTP, cchs2012_p::SMKGSTP, cchs2013_2014_p::SMKGSTP, cchs2003_m::SMKCDSTP, cchs2005_m::SMKEDSTP, cchs2007_2008_m::SMKDSTP, cchs2009_2010_m::SMKDSTP, cchs2009_m::SMKDSTP, cchs2010_m::SMKDSTP, cchs2011_2012_m::SMKDSTP, cchs2012_m::SMKDSTP, cchs2013_2014_m::SMKDSTP, cchs2014_m::SMKDSTP, cchs2015_2016_m::SMKDVSTP, cchs2017_2018_m::SMKDVSTP, cchs2019_2020_m::SMKDVSTP, cchs2021_m::SMKDVSTP, cchs2022_m::SMKDVSTP, cchs2023_m::SMKDVSTP, [SMKDGSTP]","smoking","Health behaviour","Years",NA,"StatCan derived continuous years since quit (all former smokers). Available 2009+. Preferred where available; cross-validate with SMK_09A_cont","StatsCan derived years since quit (all former smokers). Available from 2007 onward. Cross-validates SMK_09A_cont estimates.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMK_204","Cigs/day (current)","Number of cigarettes smoked daily - current daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_204, cchs2003_p::SMKC_204, cchs2005_p::SMKE_204, cchs2015_2016_p::SMK_045, cchs2017_2018_p::SMK_045, cchs2019_2020_p::SMK_045, cchs2022_p::CSS_25, cchs2023_p::CSS_25, cchs2001_m::SMKA_204, cchs2003_m::SMKC_204, cchs2005_m::SMKE_204, cchs2015_2016_m::SMK_045, cchs2017_2018_m::SMK_045, cchs2019_2020_m::SMK_045, cchs2021_m::SMK_045, cchs2022_m::CSS_25, cchs2023_m::CSS_25, [SMK_204]","smoking","Health behaviour","Cigarettes/day",NA,"Smoking intensity - current daily smokers. Input to intensity (CPD) model","Cigarettes per day for current daily smokers. Input to smoking intensity descriptive tables and future intensity model.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMK_208","Cigs/day (former)","Number of cigarettes smoked daily - former daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_208, cchs2003_p::SMKC_208, cchs2005_p::SMKE_208, cchs2015_2016_p::SMK_075, cchs2017_2018_p::SMK_075, cchs2019_2020_p::SMK_075, cchs2001_m::SMKA_208, cchs2003_m::SMKC_208, cchs2005_m::SMKE_208, cchs2015_2016_m::SMK_075, cchs2017_2018_m::SMK_075, cchs2019_2020_m::SMK_075, cchs2021_m::SMK_075, cchs2022_m::SPU_20, cchs2023_m::SPU_20, [SMK_208]","smoking","Health behaviour","Cigarettes/day",NA,"Smoking intensity - former daily smokers (peak CPD while smoking). Input to intensity model","Cigarettes per day for former daily smokers (peak while smoking). Input to smoking intensity descriptive tables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMKG01C_cont","Age first cigarette (PUMF grouped)","","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKAG01C, cchs2003_p::SMKCG01C, cchs2005_p::SMKEG01C, cchs2015_2016_p::SMKG035, cchs2017_2018_p::SMKG035, cchs2019_2020_p::SMKG035, cchs2022_p::CSS_10, cchs2023_p::CSS_10, cchs2001_m::SMKA_01C, cchs2003_m::SMKC_01C, cchs2005_m::SMKE_01C, cchs2007_2008_m::SMK_01C, cchs2009_2010_m::SMK_01C, cchs2009_m::SMK_01C, cchs2010_m::SMK_01C, cchs2011_2012_m::SMK_01C, cchs2012_m::SMK_01C, cchs2013_2014_m::SMK_01C, cchs2014_m::SMK_01C, cchs2015_2016_m::SMK_035, cchs2017_2018_m::SMK_035, cchs2019_2020_m::SMK_035, cchs2021_m::SMK_035, cchs2022_m::CSS_10, cchs2023_m::CSS_10, [SMKG01C]","smoking","Health behaviour","Years",NA,"PUMF grouped age first cigarette (recoded to midpoints). Intermediate input to age_first_cigarette (cchsflow v3).","Intermediate: PUMF grouped age first cigarette recoded to midpoints. Required by cchsflow v3 to compute age_first_cigarette.","0.1.0","2026-06-09","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMKG040_cont","Age started daily smoking (PUMF grouped)","","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::[SMKG203_pre2005, SMKG207_pre2005], cchs2003_p::[SMKG203_pre2005, SMKG207_pre2005], cchs2005_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2007_2008_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2009_2010_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2010_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2011_2012_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2012_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2013_2014_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2014_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2001_m::[SMK_203, SMK_207], cchs2003_m::[SMK_203, SMK_207], cchs2005_m::[SMK_203, SMK_207], cchs2007_2008_m::[SMK_203, SMK_207], cchs2009_2010_m::[SMK_203, SMK_207], cchs2009_m::[SMK_203, SMK_207], cchs2010_m::[SMK_203, SMK_207], cchs2011_2012_m::[SMK_203, SMK_207], cchs2012_m::[SMK_203, SMK_207], cchs2013_2014_m::[SMK_203, SMK_207], cchs2014_m::[SMK_203, SMK_207], cchs2015_2016_m::SMK_040, cchs2017_2018_m::SMK_040, cchs2019_2020_m::SMK_040, cchs2021_m::SMK_040, cchs2022_m::SPU_15, cchs2023_m::SPU_15, [SMKG040]","smoking","Health behaviour","Years",NA,"PUMF grouped age started daily smoking (recoded to midpoints). Intermediate input to age_start_smoking (cchsflow v3).","Intermediate: PUMF grouped age started daily smoking recoded to midpoints. Required by cchsflow v3 to compute age_start_smoking.","0.1.0","2026-06-10","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" -"SMKDVSTP","Time since quit (master)","","Continuous","cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_m::SMKCDSTP, cchs2005_m::SMKEDSTP, cchs2007_2008_m::SMKDSTP, cchs2009_2010_m::SMKDSTP, cchs2009_m::SMKDSTP, cchs2010_m::SMKDSTP, cchs2011_2012_m::SMKDSTP, cchs2012_m::SMKDSTP, cchs2013_2014_m::SMKDSTP, cchs2014_m::SMKDSTP, [SMKDVSTP]","smoking","Health behaviour","Years","Master-only StatCan derived time since quit (all former smokers, 0-88 years). No longer a cchsflow feeder: v3 final derives time_quit_smoking from SMK_09A_cont/SMK_06A_cont. Retained for Master (RDC) cross-validation.","Master file derived time since quit smoking (all former smokers). Intermediate input to time_quit_smoking (cchsflow v3). Not available in PUMF.","Master-only cross-validation of PUMF midpoint-imputed years since quit. Not a feeder for any unified variable in cchsflow v3 final.","0.1.0","2026-06-09","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","master" -"SMK_05D","Ever daily (occ)","Ever smoked cigarettes daily (asked of occasional smokers)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05D, cchs2003_p::SMKC_05D, cchs2005_p::SMKE_05D, cchs2015_2016_p::SMK_030, cchs2017_2018_p::SMK_030, cchs2019_2020_p::SMK_030, cchs2001_m::SMKA_05D, cchs2003_m::SMKC_05D, cchs2005_m::SMKE_05D, cchs2015_2016_m::SMK_030, cchs2017_2018_m::SMK_030, cchs2019_2020_m::SMK_030, cchs2021_m::SMK_030, cchs2022_m::SPU_05, cchs2023_m::SPU_05, [SMK_05D]","smoking","Health behaviour","N/A",NA,"Ever smoked daily (asked of occasional smokers).","Intermediate: feeder for SMKDSTY_original 2015-2021 (with SMK_202, SMK_01A).","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMKDSTY_cat5","Smoking (5-cat)","Smoking status (5-category): daily, occasional, former daily, former occasional, never","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2015_2016_p::SMKDVSTY, cchs2017_2018_p::SMKDVSTY, cchs2019_2020_p::SMKDVSTY, cchs2022_p::SMKDVSTY, cchs2023_p::SMKDVSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2015_2016_m::SMKDVSTY, cchs2017_2018_m::SMKDVSTY, cchs2019_2020_m::SMKDVSTY, cchs2021_m::SMKDVSTY, cchs2022_m::SMKDVSTY, cchs2023_m::SMKDVSTY, [SMKDSTY]","smoking","Health behaviour","N/A",NA,"Smoking status, 5 categories avoiding the 2015 semantic break.","Intermediate: feeder for time_quit_smoking_daily (cchsflow v3).","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMK_09C","Yrs quit daily","Years since stopped smoking daily - former daily (Master continuous)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09C, cchs2005_m::SMKE_09C, cchs2015_2016_m::SMK_090, cchs2017_2018_m::SMK_090, cchs2019_2020_m::SMK_090, cchs2021_m::SMK_090, [SMK_09C]","smoking","Health behaviour","years",NA,"Master continuous years since stopped smoking daily (former daily smokers).","Intermediate: Master exact-years feeder for time_quit_smoking_daily.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"SMK_01C","Age 1st cig","Age smoked first whole cigarette","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::SMKA_01C, cchs2003_m::SMKC_01C, cchs2005_m::SMKE_01C, cchs2015_2016_m::SMK_035, cchs2017_2018_m::SMK_035, cchs2019_2020_m::SMK_035, cchs2021_m::SMK_035, cchs2022_m::CSS_10, cchs2023_m::CSS_10, [SMK_01C]","smoking","Health behaviour","years",NA,"Master continuous age smoked first whole cigarette.","Intermediate: Master exact-age feeder for age_first_cigarette.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"SMK_040","Age daily (ever)","Age started smoking cigarettes daily (all ever-daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::[SMK_203, SMK_207], cchs2003_m::[SMK_203, SMK_207], cchs2005_m::[SMK_203, SMK_207], cchs2007_2008_m::[SMK_203, SMK_207], cchs2009_2010_m::[SMK_203, SMK_207], cchs2009_m::[SMK_203, SMK_207], cchs2010_m::[SMK_203, SMK_207], cchs2011_2012_m::[SMK_203, SMK_207], cchs2012_m::[SMK_203, SMK_207], cchs2013_2014_m::[SMK_203, SMK_207], cchs2014_m::[SMK_203, SMK_207], cchs2022_m::SPU_15, cchs2023_m::SPU_15, [SMK_040]","smoking","Health behaviour","years",NA,"Master continuous age started smoking daily (all ever-daily smokers).","Intermediate: Master exact-age feeder for age_start_smoking.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"time_quit_smoking","Yrs since quit smoking","Years since quit smoking (combined former daily and occasional)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMK_09A_cont, SMK_06A_cont]","smoking","Health behaviour","years",NA,"Unified years since quit, all former smokers (SMK_09A_cont priority, SMK_06A_cont fallback).","Intermediate: feeder for pack_years_der. cchsflow v3 recommended primary cessation measure (all former smokers); study uses time_quit_smoking_daily for the cessation APC numerator.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"smoked_100_lifetime","Smoked 100+ (ever)*","Ever smoked 100 or more cigarettes in lifetime (unified)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMK_01A]","smoking","Health behaviour","N/A",NA,"Unified ever smoked 100+ cigarettes (pass-through of SMK_01A).","Intermediate: feeder for pack_years_der.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMKG203_cont","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG203, cchs2003_p::SMKCG203, cchs2005_p::SMKEG203, cchs2015_2016_p::[SMK_005, SMKG040], cchs2017_2018_p::[SMK_005, SMKG040], cchs2019_2020_p::[SMK_005, SMKG040], cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2007_2008_m::SMK_203, cchs2009_2010_m::SMK_203, cchs2011_2012_m::SMK_203, cchs2013_2014_m::SMK_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMKG203]","smoking","Health behaviour","years",NA,"Age started smoking cigarettes daily (current daily smokers)","Intermediate: age started daily, current daily smokers (midpoint). With SMKG207_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMKG207_cont","Age daily (fmr)","Age started smoking cigarettes daily (former daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG207, cchs2003_p::SMKCG207, cchs2005_p::SMKEG207, cchs2015_2016_p::[SMK_005, SMK_030, SMKG040], cchs2017_2018_p::[SMK_005, SMK_030, SMKG040], cchs2019_2020_p::[SMK_005, SMK_030, SMKG040], cchs2001_m::SMKA_207, cchs2003_m::SMKC_207, cchs2005_m::SMKE_207, cchs2007_2008_m::SMK_207, cchs2009_2010_m::SMK_207, cchs2011_2012_m::SMK_207, cchs2013_2014_m::SMK_207, cchs2015_2016_m::[SMK_005, SMK_030, SMK_040], cchs2017_2018_m::[SMK_005, SMK_030, SMK_040], cchs2019_2020_m::[SMK_005, SMK_030, SMK_040], cchs2021_m::[SMK_005, SMK_030, SMK_040], cchs2022_m::[SMK_005, SMK_030, SMK_040], cchs2023_m::[SMK_005, SMK_030, SMK_040], [SMKG207]","smoking","Health behaviour","years",NA,"Age started smoking cigarettes daily (former daily smokers)","Intermediate: age started daily, former daily smokers (midpoint). With SMKG203_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMK_203","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMK_203]","smoking","Health behaviour","years",NA,"Age started smoking cigarettes daily (current daily smokers)","Intermediate: transitive feeder for SMKG040_cont / SMK_040 (Master) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"SMK_207","Age daily (fmr)","Age started smoking cigarettes daily (former daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_207, cchs2003_m::SMKC_207, cchs2005_m::SMKE_207, cchs2015_2016_m::[SMK_005, SMK_030, SMK_040], cchs2017_2018_m::[SMK_005, SMK_030, SMK_040], cchs2019_2020_m::[SMK_005, SMK_030, SMK_040], cchs2021_m::[SMK_005, SMK_030, SMK_040], cchs2022_m::[SMK_005, SMK_030, SMK_040], cchs2023_m::[SMK_005, SMK_030, SMK_040], [SMK_207]","smoking","Health behaviour","years",NA,"Age started smoking cigarettes daily (former daily smokers)","Intermediate: transitive feeder for SMKG040_cont / SMK_040 (Master) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"SMK_005","Smoking freq (2015+)","Type of smoker presently (2015+ era-specific name for SMK_202)","Categorical","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","[SMK_005]","smoking","Health behaviour","N/A",NA,"Type of smoker presently (2015+ era-specific name for SMK_202)","Intermediate: transitive feeder for SMK_203, SMK_207 in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMK_030","Ever daily (2015+)","Smoked daily - lifetime (2015+ era-specific name for SMK_05D)","Categorical","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2022_m::SPU_05, cchs2023_m::SPU_05, [SMK_030]","smoking","Health behaviour","N/A",NA,"Smoked daily - lifetime (2015+ era-specific name for SMK_05D)","Intermediate: transitive feeder for SMK_207 in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMK_05B","Cigs/day (occ)","Number of cigarettes smoked daily - occasional smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05B, cchs2003_p::SMKC_05B, cchs2005_p::SMKE_05B, cchs2015_2016_p::SMK_050, cchs2017_2018_p::SMK_050, cchs2019_2020_p::SMK_050, cchs2022_p::CSS_30, cchs2023_p::CSS_30, cchs2001_m::SMKA_05B, cchs2003_m::SMKC_05B, cchs2005_m::SMKE_05B, cchs2015_2016_m::SMK_050, cchs2017_2018_m::SMK_050, cchs2019_2020_m::SMK_050, cchs2021_m::SMK_050, cchs2022_m::CSS_30, cchs2023_m::CSS_30, [SMK_05B]","smoking","Health behaviour","cigarettes",NA,"Number of cigarettes smoked daily - occasional smokers","Intermediate: cigarettes per day on days smoked (occasional smokers); occasional-period feeder for pack_years_der.","0.1.0","2026-06-10","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"SMK_05C","Days smoked/month","Days smoked at least 1 cigarette in past month","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05C, cchs2003_p::SMKC_05C, cchs2005_p::SMKE_05C, cchs2015_2016_p::SMK_055, cchs2017_2018_p::SMK_055, cchs2019_2020_p::SMK_055, cchs2022_p::CSS_35, cchs2023_p::CSS_35, cchs2001_m::SMKA_05C, cchs2003_m::SMKC_05C, cchs2005_m::SMKE_05C, cchs2015_2016_m::SMK_055, cchs2017_2018_m::SMK_055, cchs2019_2020_m::SMK_055, cchs2021_m::SMK_055, cchs2022_m::CSS_35, cchs2023_m::CSS_35, [SMK_05C]","smoking","Health behaviour","days",NA,"Days smoked at least 1 cigarette in past month","Intermediate: days smoked per month (occasional smokers); occasional-period feeder for pack_years_der.","0.1.0","2026-06-10","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","both" -"DHH_AGE","Age","Age","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::DHHA_AGE, cchs2003_m::DHHC_AGE, cchs2005_m::DHHE_AGE, cchs2022_m::AWCAGE, cchs2023_m::AWCAGE, [DHH_AGE]","demographics","Sociodemographics","Years",NA,"Age","Intermediate: transitive feeder for pack_years_der (Master exact age) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active",NA,"intermediate","master" -"cigs_per_day","Cigs/day (unified)*","Cigarettes per day - unified daily + former daily (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","DerivedVar::[SMK_204, SMK_208, SMKDSTY_original]","smoking","Health behaviour","Cigarettes/day","Unified cigs/day (cchsflow v3). DerivedVar::[SMK_204, SMK_208, SMKDSTY_original]. Universe: ever-daily smokers. Not supported PUMF 2022/2023 (SMK_208 is Master-only via SPU in those cycles).","Unified cigs/day (cchsflow v3). Combines SMK_204 (current daily) and SMK_208 (former daily) automatically.","Smoking intensity for descriptive tables and future dose-response models.","0.1.0","2026-06-11","Replaces separate SMK_204/SMK_208","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac) - removed cchs2022_p (not supported in v3 final)","predictor, table1, imputation-predictor","both" -"pack_years_der","Pack-years (unified)*","Cumulative pack-years - derived (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","DerivedVar::[SMKDSTY_original, DHHGAGE_cont, DHH_AGE, age_start_smoking, cigs_per_day, time_quit_smoking, SMK_204, SMK_208, age_first_cigarette, smoked_100_lifetime]","smoking","Health behaviour","Pack-years","Cumulative pack-years (cchsflow v3). PUMF feeders: SMKDSTY_original, DHHGAGE_cont, age_start_smoking, cigs_per_day, time_quit_smoking, SMK_204, SMK_208, age_first_cigarette, smoked_100_lifetime. Not supported PUMF 2022/2023.","Cumulative pack-years (cchsflow v3). Derived from cigs_per_day and years smoked.","Cumulative smoking exposure measure for descriptive tables.","0.1.0","2026-06-09","PUMF gap: not available 2022","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1","both" -"DHHGMS","Marital status","Marital status - (G)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::DHHAGMS, cchs2003_p::DHHCGMS, cchs2005_p::DHHEGMS, [DHHGMS]","Marital Status","Sociodemographics","N/A",NA,"Marital status - (G)","Marital status: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","pumf" -"ALCDTTM","Drinker type (last 12 months)","Type of drinker (12 months)","Categorical","cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2015_2016_p::ALCDVTTM, cchs2017_2018_p::ALCDVTTM, cchs2019_2020_p::ALCDVTTM, cchs2015_2016_m::ALCDVTTM, cchs2017_2018_m::ALCDVTTM, cchs2019_2020_m::ALCDVTTM, [ALCDTTM]","Alcohol","Health behaviour","N/A",NA,"Type of drinker (12 months)","Drinker type (12 months): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"ALWDWKY","Drinks last week","Weekly consumption of alcohol","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::ALCADWKY, cchs2003_p::ALCCDWKY, cchs2005_p::ALCEDWKY, cchs2015_2016_p::ALWDVWKY, cchs2017_2018_p::ALWDVWKY, cchs2019_2020_p::ALWDVWKY, cchs2001_m::ALCADWKY, cchs2003_m::ALCCDWKY, cchs2005_m::ALCEDWKY, cchs2015_2016_m::ALWDVWKY, cchs2017_2018_m::ALWDVWKY, cchs2019_2020_m::ALWDVWKY, [ALWDWKY]","Alcohol","Health behaviour","drinks/week",NA,"Weekly consumption of alcohol","Drinks last week: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"HWTGBMI_der","Derived BMI","Derived Body Mass Index","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","DerivedVar::[HWTGHTM, HWTGWTK]","BMI","Health status","kg/m2",NA,"Derived Body Mass Index","BMI (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","pumf" -"GEN_01","Self-perceived health","Self-perceived health","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_01, cchs2003_p::GENC_01, cchs2005_p::GENE_01, cchs2015_2016_p::GEN_005, cchs2017_2018_p::GEN_005, cchs2019_2020_p::GEN_005, cchs2001_m::GENA_01, cchs2003_m::GENC_01, cchs2005_m::GENE_01, cchs2015_2016_m::GEN_005, cchs2017_2018_m::GEN_005, cchs2019_2020_m::GEN_005, [GEN_01]","Self-perceived health","Health status","N/A",NA,"Self-perceived health","Self-rated general health: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"GEN_02B","Self-perceived mental health","Self-perceived mental health","Categorical","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2003_p::GENC_02B, cchs2005_p::GENE_02B, cchs2015_2016_p::GEN_015, cchs2017_2018_p::GEN_015, cchs2019_2020_p::GEN_015, cchs2003_m::GENC_02B, cchs2005_m::GENE_02B, cchs2015_2016_m::GEN_015, cchs2017_2018_m::GEN_015, cchs2019_2020_m::GEN_015, [GEN_02B]","Mental health","Health status","N/A",NA,"Self-perceived mental health","Self-rated mental health: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"GEN_07","Self-perceived life stress","Self-perceived life stress","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_07, cchs2003_p::GENC_07, cchs2005_p::GENE_07, cchs2015_2016_p::GEN_020, cchs2017_2018_p::GEN_020, cchs2019_2020_p::GEN_020, cchs2001_m::GENA_07, cchs2003_m::GENC_07, cchs2005_m::GENE_07, cchs2015_2016_m::GEN_020, cchs2017_2018_m::GEN_020, cchs2019_2020_m::GEN_020, [GEN_07]","Mental health","Health status","N/A",NA,"Self-perceived life stress","Self-perceived life stress: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"GEN_10","Sense of belonging","Sense of belonging in the community","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_10, cchs2003_p::GENC_10, cchs2005_p::GENE_10, cchs2015_2016_p::GEN_030, cchs2017_2018_p::GEN_030, cchs2019_2020_p::GEN_030, cchs2001_m::GENA_10, cchs2003_m::GENC_10, cchs2005_m::GENE_10, cchs2015_2016_m::GEN_030, cchs2017_2018_m::GEN_030, cchs2019_2020_m::GEN_030, [GEN_10]","Mental health","Health status","N/A",NA,"Sense of belonging in the community","Sense of community belonging: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_071","Hypertension","Do you have high blood pressure?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_071, cchs2003_p::CCCC_071, cchs2005_p::CCCE_071, cchs2015_2016_p::CCC_065, cchs2017_2018_p::CCC_065, cchs2019_2020_p::CCC_065, cchs2001_m::CCCA_071, cchs2003_m::CCCC_071, cchs2005_m::CCCE_071, cchs2015_2016_m::CCC_065, cchs2017_2018_m::CCC_065, cchs2019_2020_m::CCC_065, [CCC_071]","Chronic condition","Health status","N/A",NA,"Do you have high blood pressure?","Hypertension: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_091","COPD/Emphysema/Bronchitis","Do you have COPD (eg bronchitis, emphysema)?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::[CCC_91A, CCC_91B], cchs2003_p::[CCC_91A, CCC_91B], cchs2005_p::[CCC_91A, CCC_91E, CCC_91F], cchs2007_2008_p::[CCC_91A, CCC_91E, CCC_91F], cchs2015_2016_p::CCC_030, cchs2017_2018_p::CCC_030, cchs2019_2020_p::CCC_030, cchs2001_m::[CCC_91A, CCC_91B], cchs2003_m::[CCC_91A, CCC_91B], cchs2005_m::[CCC_91A, CCC_91E, CCC_91F], cchs2007_2008_m::[CCC_91A, CCC_91E, CCC_91F], cchs2015_2016_m::CCC_030, cchs2017_2018_m::CCC_030, cchs2019_2020_m::CCC_030, [CCC_091]","Chronic condition","Health status","N/A",NA,"Do you have COPD (eg bronchitis, emphysema)?","COPD/emphysema (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_101","Diabetes","Do you have diabetes?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_101, cchs2003_p::CCCC_101, cchs2005_p::CCCE_101, cchs2015_2016_p::CCC_095, cchs2017_2018_p::CCC_095, cchs2019_2020_p::CCC_095, cchs2001_m::CCCA_101, cchs2003_m::CCCC_101, cchs2005_m::CCCE_101, cchs2015_2016_m::CCC_095, cchs2017_2018_m::CCC_095, cchs2019_2020_m::CCC_095, [CCC_101]","Chronic condition","Health status","N/A",NA,"Do you have diabetes?","Diabetes: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_121","Heart Disease","Do you have heart disease?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_121, cchs2003_p::CCCC_121, cchs2005_p::CCCE_121, cchs2015_2016_p::CCC_085, cchs2017_2018_p::CCC_085, cchs2019_2020_p::CCC_085, cchs2001_m::CCCA_121, cchs2003_m::CCCC_121, cchs2005_m::CCCE_121, cchs2015_2016_m::CCC_085, cchs2017_2018_m::CCC_085, cchs2019_2020_m::CCC_085,[CCC_121]","Chronic condition","Health status","N/A",NA,"Do you have heart disease?","Heart disease: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_151","Stroke","Do you suffer from effects of stroke?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_151, cchs2003_p::CCCC_151, cchs2005_p::CCCE_151, cchs2015_2016_p::CCC_090, cchs2017_2018_p::CCC_090, cchs2019_2020_p::CCC_090, cchs2001_m::CCCA_151, cchs2003_m::CCCC_151, cchs2005_m::CCCE_151, cchs2015_2016_m::CCC_090, cchs2017_2018_m::CCC_090, cchs2019_2020_m::CCC_090,[CCC_151]","Chronic condition","Health status","N/A",NA,"Do you suffer from effects of stroke?","Stroke: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"CCC_280","Mood disorder","Do you have a mood disorder?","Categorical","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2003_p::CCCC_280, cchs2005_p::CCCE_280, cchs2015_2016_p::CCC_195, cchs2017_2018_p::CCC_195, cchs2019_2020_p::CCC_195, cchs2003_m::CCCC_280, cchs2005_m::CCCE_280, cchs2015_2016_m::CCC_195, cchs2017_2018_m::CCC_195, cchs2019_2020_m::CCC_195, [CCC_280]","Chronic condition","Health status","N/A",NA,"Do you have a mood disorder?","Mood disorder: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"energy_exp","Daily energy expenditure","Daily energy expenditure","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::PACADEE, cchs2003_p::PACCDEE, cchs2005_p::PACEDEE, cchs2015_2016_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2017_2018_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2019_2020_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2001_m::PACADEE, cchs2003_m::PACCDEE, cchs2005_m::PACEDEE, cchs2015_2016_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2017_2018_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2019_2020_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], [PACDEE]","Exercise","Health behaviour","METS",NA,"Daily energy expenditure","Daily energy expenditure (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"table1, imputation-predictor","both" -"HWTGHTM","Height","Height (metres)/self-reported - (D,G)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::HWTAGHT, cchs2003_p::HWTCGHT, cchs2005_p::HWTEGHTM, cchs2015_2016_p::HWTDGHTM, cchs2017_2018_p::HWTDGHTM, cchs2019_2020_p::HWTDGHTM, [HWTGHTM]","Height","Health status","meters",NA,"Height (metres)/self-reported - (D,G)","Intermediate: feeder for HWTGBMI_der (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","pumf" -"HWTGWTK","Weight","Weight - kilograms (D,G)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::HWTAGWTK, cchs2003_p::HWTCGWTK, cchs2005_p::HWTEGWTK, cchs2015_2016_p::HWTDGWTK, cchs2017_2018_p::HWTDGWTK, cchs2019_2020_p::HWTDGWTK, [HWTGWTK]","Weight","Health status","kg",NA,"Weight - kilograms (D,G)","Intermediate: feeder for HWTGBMI_der (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","pumf" -"CCC_91A","Bronchitis","Do you have chronic bronchitis?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m","cchs2001_p::CCCA_91A, cchs2003_p::CCCC_91A, cchs2005_p::CCCE_91A, cchs2001_m::CCCA_91A, cchs2003_m::CCCC_91A, cchs2005_m::CCCE_91A, [CCC_91A]","Chronic condition","Health status","N/A",NA,"Do you have chronic bronchitis?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"CCC_91B","Emphysema/COPD","Do you have emphysema or COPD?","Categorical","cchs2001_p, cchs2003_p, cchs2001_m, cchs2003_m","cchs2001_p::CCCA_91B, cchs2003_p::CCCC_91B, cchs2001_m::CCCA_91B, cchs2003_m::CCCC_91B","Chronic condition","Health status","N/A",NA,"Do you have emphysema or COPD?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"CCC_91E","Emphysema","Do you have emphysema?","Categorical","cchs2005_p, cchs2007_2008_p, cchs2005_m, cchs2007_2008_m","cchs2005_p::CCCE_91E, cchs2007_2008_p::CCC_91E, cchs2005_m::CCCE_91E, cchs2007_2008_m::CCC_91E","Chronic condition","Health status","N/A",NA,"Do you have emphysema?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"CCC_91F","COPD","Do you have COPD?","Categorical","cchs2005_p, cchs2007_2008_p, cchs2005_m, cchs2007_2008_m","cchs2005_p::CCCE_91F, cchs2007_2008_p::CCC_91F, cchs2005_m::CCCE_91F, cchs2007_2008_m::CCC_91F","Chronic condition","Health status","N/A",NA,"Do you have COPD?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAA_045","Sweat/breathe harder exercises (18+ years old)","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_045]","Exercise","Health behaviour","hours/week",NA,"Time spent - sweat/breathe hard exercises in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAA_050","Sweat/breathe harder exercises (18+ years old)","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_050]","Exercise","Health behaviour","minutes/week",NA,"Time spent - sweat/breathe hard exercises in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAA_075","Sweat/breathe harder activities (18+ years old)","Time spent - sweat/breathe hard activities in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_075]","Exercise","Health behaviour","hours/week",NA,"Time spent - sweat/breathe hard activities in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAA_080","Sweat/breathe harder activities (18+ years old)","Time spent - sweat/breathe hard activities in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_080]","Exercise","Health behaviour","minutes/week",NA,"Time spent - sweat/breathe hard activities in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAADVDYS","Active days (18+ years old)","Number of active days (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAADVDYS]","Exercise","Health behaviour","days",NA,"Number of active days (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAADVVIG","Vigorous activity (18+ years old)","Time spent - vigorous activity (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAADVVIG]","Exercise","Health behaviour","minutes/week",NA,"Time spent - vigorous activity (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAYDVTOA","Sweat/breathe hard activities (12-17 years old)","Time spent - sweat/breathe hard activities in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVTOA]","Exercise","Health behaviour","minutes/week",NA,"Time spent - sweat/breathe hard activities in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAYDVADL","Leisure activities (12-17 years old)","Time spent - leisure activity in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVADL]","Exercise","Health behaviour","minutes/week",NA,"Time spent - leisure activity in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAYDVVIG","Vigorous activities (12-17 years old)","Time spent - vigorous activity in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVVIG]","Exercise","Health behaviour","minutes/week",NA,"Time spent - vigorous activity in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" -"PAYDVDYS","Active days (12-17 years old)","Number of active days (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVDYS]","Exercise","Health behaviour","minutes/week",NA,"Number of active days (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active",NA,"intermediate","both" +"variable","label","labelLong","variableType","databaseStart","variableStart","subject","section","units","notes","description","purpose","version","lastUpdated","reviewNotes","status","versionNotes","role","source" +"SurveyCycle","Survey cycle","CCHS survey cycle","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","DerivedVar::[data_name]","demographics","Sociodemographics","N/A","NA","Derived from dataset name; identifies CCHS cycle for each respondent","Identifies the CCHS cycle for each respondent. Used to derive survey year for the period component of the APC model and to label cycle-specific results.","0.1.0","2026-06-11","","","NA","design, table1-stratifier, imputation-predictor","both" +"DHHGAGE_cont","Age (grouped continuous, years)","Age - continuous","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","[DHH_AGE]","demographics","Sociodemographics","Years","NA","Continuous age; preferred over DHHGAGE_A for APC model where available","Continuous age in years (midpoint-estimated in PUMF). The primary age input to the APC model. Cohort is derived as survey_year - age.","0.1.0","2026-06-11","","","NA","predictor, table1, apc-denominator, imputation-predictor","pumf" +"DHH_SEX","Sex","Sex","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","cchs2001_p::DHHA_SEX, cchs2003_p::DHHA_SEX, cchs2005_p::DHHA_SEX, [DHH_SEX]","demographics","Sociodemographics","N/A","NA","Sex (1=Male 2=Female). APC models run separately by sex","Sex. APC initiation and cessation models are fit separately for men and women because smoking trends differ substantially by sex. Also used to stratify Table 1.","0.1.0","2026-06-11","","","NA","model-stratifier, table1, apc-denominator, table1-stratifier, imputation-predictor","pumf" +"GEOGPRV","Province of residence","Province of residence","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","[GEOGPRV]","demographics","Sociodemographics","N/A","NA","Province/territory of residence. Used for provincial APC estimates","Province of residence. Used for provincial APC stratification (Stage 8 subgroup). Territories are pooled due to small sample sizes.","0.1.0","2026-06-11","","","NA","predictor, table1, imputation-predictor","pumf" +"WTS_M","Master survey weight","Survey sampling weight","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p","cchs2001_p::WTSAM, cchs2003_p::WTSC_M, cchs2005_p::WTSE_M, [WTS_M]","demographics","Sociodemographics","N/A","NA","CCHS sampling weight. Required for all prevalence estimates and weighted APC models","Survey sampling weight. Applied in APC logistic regression as a case weight to produce nationally representative estimates.","0.1.0","2026-06-11","","","NA","design, apc-denominator, imputation-predictor","pumf" +"SDCFIMM","Immigrant status (2007-2014 only)","Immigrant status (D)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","[SDCFIMM]","demographics","Sociodemographics","N/A","NA","Immigrant status derived (1=non-immigrant 2=immigrant 3=non-permanent resident). Used in MPoRT mortality adjustment","Immigrant status. Used in the MPoRT mortality correction to adjust for survival bias. Only available 2007-2014.","0.1.0","2026-06-11","","","NA","predictor, table1","pumf" +"SDCGCGT","Cultural/racial origin (2007-2014 only)","Cultural or racial origin (D/G)","Categorical","cchs2001_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","cchs2001_p::SDCA_COB, [SDCGCGT]","demographics","Sociodemographics","N/A","NA","Cultural/racial origin. Not in all cycles (NA(c) where absent). Used for subgroup analysis; not a primary APC model variable","Cultural/racial origin. Retained for potential subgroup analyses. Only available in select cycles; will be NA(c) elsewhere.","0.1.0","2026-06-11","Not available all cycles","","NA","predictor, table1","pumf" +"EDUDR03","Education (3-cat)","Highest education level - 3 categories","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2011_2012_p, cchs2013_2014_p, cchs2015_2016_p, cchs2017_2018_p","[EDUDR03]","demographics","Sociodemographics","N/A","NA","Highest education (3-category: less than high school / high school graduate / post-secondary). Used for subgroup analysis. cchsflow harmonized from EDUADR04/EDUCDR04/EDUEDR04/EHG2DVR3.","Education (3-category). Included for descriptive purposes and potential subgroup analysis. Not available in 2019-20 or 2022 PUMF.","0.1.0","2026-06-11","Not available 2019-20 or 2022","","NA","predictor, table1, imputation-predictor","pumf" +"SMK_01A","Smoked 100+ cigs","Ever smoked 100 or more cigarettes in lifetime","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_01A, cchs2003_p::SMKC_01A, cchs2005_p::SMKE_01A, cchs2015_2016_p::SMK_020, cchs2017_2018_p::SMK_020, cchs2019_2020_p::SMK_020, cchs2022_p::CSS_15, cchs2023_p::CSS_15, cchs2001_m::SMKA_01A, cchs2003_m::SMKC_01A, cchs2005_m::SMKE_01A, cchs2015_2016_m::SMK_020, cchs2017_2018_m::SMK_020, cchs2019_2020_m::SMK_020, cchs2021_m::SMK_020, cchs2022_m::CSS_15, cchs2023_m::CSS_15, [SMK_01A]","smoking","Health behaviour","N/A","NA","Gate question: ever smoked >=100 cigarettes lifetime. Combined with SMK_01B and SMK_202 for never/current/former classification","Intermediate: input to SMKDSTY_A derivation. Gate variable: ever smoked 100+ cigarettes. Combined with SMK_01B and SMK_202 to classify respondents as never/current/former smoker for the APC numerator.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_01B","Smoked 1 whole cig","Ever smoked a whole cigarette","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_01B, cchs2003_p::SMKC_01B, cchs2005_p::SMKE_01B, cchs2015_2016_p::SMK_025, cchs2017_2018_p::SMK_025, cchs2019_2020_p::SMK_025, cchs2022_p::CSS_05, cchs2023_p::CSS_05, cchs2001_m::SMKA_01B, cchs2003_m::SMKC_01B, cchs2005_m::SMKE_01B, cchs2015_2016_m::SMK_025, cchs2017_2018_m::SMK_025, cchs2019_2020_m::SMK_025, cchs2021_m::SMK_025, cchs2022_m::CSS_05, cchs2023_m::CSS_05, [SMK_01B]","smoking","Health behaviour","N/A","NA","Second gate for never-smoker definition: never smoked a whole cigarette AND <100 lifetime cigarettes = never smoker","Intermediate: input to SMKDSTY_A derivation. Gate variable: ever smoked a whole cigarette. Used with SMK_01A to define never-smoker status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_202","Smoking type","Type of smoker presently (daily/occasional/not at all)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKA_202, cchs2003_p::SMKC_202, cchs2005_p::SMKE_202, cchs2015_2016_p::SMK_005, cchs2017_2018_p::SMK_005, cchs2019_2020_p::SMK_005, cchs2001_m::SMKA_202, cchs2003_m::SMKC_202, cchs2005_m::SMKE_202, cchs2015_2016_m::SMK_005, cchs2017_2018_m::SMK_005, cchs2019_2020_m::SMK_005, cchs2021_m::SMK_005, [SMK_202]","smoking","Health behaviour","N/A","NA","Current smoking frequency. Combined with SMK_01A/SMK_01B to derive 3-category status: never / current / former","Intermediate: input to SMKDSTY_A derivation. Current smoking frequency (daily/occasional/not at all). Combined with SMK_01A/SMK_01B to derive 3-category smoking status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMKDSTY_original","Smoking status (6-cat)","Type of smoker derived - 6-category (cchsflow v3, original StatCan scheme)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2007_2008_p::SMKDSTY, cchs2009_2010_p::SMKDSTY, cchs2010_p::SMKDSTY, cchs2011_2012_p::SMKDSTY, cchs2012_p::SMKDSTY, cchs2013_2014_p::SMKDSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2007_2008_m::SMKDSTY, cchs2009_2010_m::SMKDSTY, cchs2009_m::SMKDSTY, cchs2010_m::SMKDSTY, cchs2011_2012_m::SMKDSTY, cchs2012_m::SMKDSTY, cchs2013_2014_m::SMKDSTY, cchs2014_m::SMKDSTY, DerivedVar::[SMK_202, SMK_05D, SMK_01A]","smoking","Health behaviour","N/A","NA","cchsflow v3 harmonized 6-cat smoking status: 1=daily, 2=occ(fmr daily), 3=always occasional, 4=former daily, 5=former occasional, 6=never. Consistent categories across all cycles.","Primary smoking classification for APC numerator construction and Table 1. cchsflow v3: 2001-2014 pass-through from SMKDSTY; 2015-2021 derived from SMK_202, SMK_05D, SMK_01A. Not supported PUMF 2022/2023.","0.1.0","2026-06-11","Renamed from SMKDSTY_A per CEP-002 year-based naming","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" +"age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically. Also the start of each established smoker's cessation risk clock (task 1.3).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" +"age_start_smoking","Age daily (unified)*","Age started smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG040_cont, SMK_040]","smoking","Health behaviour","Years","Raw SMKG040 absent from 2019-20 PUMF (DDI-confirmed): age_start_smoking unavailable for cchs2019_2020_p and cchs2022_p; Master covers 2001-2023. See cchsflow#185.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_040); PUMF: midpoint-estimated (SMKG040_cont). Primary daily initiation age","Age started smoking daily (unified cchsflow v3 variable). Used in the initiation APC model as an alternative or supplementary age measure.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" +"time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023.","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Years since stopped daily smoking. Retained for the intensity model and the daily-smoking sensitivity analysis; no longer the cessation exit (see time_quit_smoking_complete).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, imputation-predictor","both" +"time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003: not available in 2001 (NA(c); imputed per Appendix D).","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, imputation-predictor","both" +"SMK_09A_cont","Yrs quit daily (PUMF)","Years since stopped smoking daily - former daily (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_09A, cchs2003_p::SMKC_09A, cchs2005_p::SMKE_09A, cchs2015_2016_p::SMK_080, cchs2017_2018_p::SMK_080, cchs2019_2020_p::SMK_080, cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09A, cchs2005_m::SMKE_09A, cchs2015_2016_m::SMK_080, cchs2017_2018_m::SMK_080, cchs2019_2020_m::SMK_080, cchs2021_m::SMK_080, cchs2023_m::SPU_25, [SMK_09A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former daily smokers. Feeder for time_quit_smoking and time_quit_smoking_daily (cchsflow v3). Not available 2022 or PUMF 2023 (SPU_25 is Master-only).","PUMF-derived continuous years since quit. Superseded by time_quit_smoking once cchsflow v3 merges. Keep for pre-v3 fallback","Intermediate: PUMF/Master midpoint feeder for the unified cessation variables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_06A_cont","Yrs quit occ (PUMF)","Years since stopped smoking - former occasional smokers (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_06A, cchs2003_p::SMKC_06A, cchs2005_p::SMKE_06A, cchs2015_2016_p::SMK_060, cchs2017_2018_p::SMK_060, cchs2019_2020_p::SMK_060, cchs2001_m::SMKA_06A, cchs2003_m::SMKC_06A, cchs2005_m::SMKE_06A, cchs2015_2016_m::SMK_060, cchs2017_2018_m::SMK_060, cchs2019_2020_m::SMK_060, cchs2021_m::SMK_060, cchs2023_m::SPU_10, [SMK_06A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former occasional smokers. Feeder for time_quit_smoking (cchsflow v3 falls back to it when SMK_09A_cont is not applicable). Not available 2022 or PUMF 2023.","PUMF-derived years since quit for former occasional smokers. Not covered by time_quit_smoking (daily only)","Intermediate: occasional-smoker feeder for time_quit_smoking (all former smokers).","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMKDGSTP_cont","Yrs since quit (all)","Years since quit smoking completely - all former smokers (continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_p::SMKCGSTP, cchs2005_p::SMKEGSTP, cchs2007_2008_p::SMKGSTP, cchs2009_2010_p::SMKGSTP, cchs2010_p::SMKGSTP, cchs2011_2012_p::SMKGSTP, cchs2012_p::SMKGSTP, cchs2013_2014_p::SMKGSTP, cchs2003_m::SMKCDSTP, cchs2005_m::SMKEDSTP, cchs2007_2008_m::SMKDSTP, cchs2009_2010_m::SMKDSTP, cchs2009_m::SMKDSTP, cchs2010_m::SMKDSTP, cchs2011_2012_m::SMKDSTP, cchs2012_m::SMKDSTP, cchs2013_2014_m::SMKDSTP, cchs2014_m::SMKDSTP, cchs2015_2016_m::SMKDVSTP, cchs2017_2018_m::SMKDVSTP, cchs2019_2020_m::SMKDVSTP, cchs2021_m::SMKDVSTP, cchs2022_m::SMKDVSTP, cchs2023_m::SMKDVSTP, [SMKDGSTP]","smoking","Health behaviour","Years","NA","StatCan derived continuous years since quit (all former smokers). Available 2009+. Preferred where available; cross-validate with SMK_09A_cont","StatsCan derived years since quit (all former smokers). Available from 2007 onward. Cross-validates SMK_09A_cont estimates.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_204","Cigs/day (current)","Number of cigarettes smoked daily - current daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_204, cchs2003_p::SMKC_204, cchs2005_p::SMKE_204, cchs2015_2016_p::SMK_045, cchs2017_2018_p::SMK_045, cchs2019_2020_p::SMK_045, cchs2022_p::CSS_25, cchs2023_p::CSS_25, cchs2001_m::SMKA_204, cchs2003_m::SMKC_204, cchs2005_m::SMKE_204, cchs2015_2016_m::SMK_045, cchs2017_2018_m::SMK_045, cchs2019_2020_m::SMK_045, cchs2021_m::SMK_045, cchs2022_m::CSS_25, cchs2023_m::CSS_25, [SMK_204]","smoking","Health behaviour","Cigarettes/day","NA","Smoking intensity - current daily smokers. Input to intensity (CPD) model","Cigarettes per day for current daily smokers. Input to smoking intensity descriptive tables and future intensity model.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_208","Cigs/day (former)","Number of cigarettes smoked daily - former daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_208, cchs2003_p::SMKC_208, cchs2005_p::SMKE_208, cchs2015_2016_p::SMK_075, cchs2017_2018_p::SMK_075, cchs2019_2020_p::SMK_075, cchs2001_m::SMKA_208, cchs2003_m::SMKC_208, cchs2005_m::SMKE_208, cchs2015_2016_m::SMK_075, cchs2017_2018_m::SMK_075, cchs2019_2020_m::SMK_075, cchs2021_m::SMK_075, cchs2022_m::SPU_20, cchs2023_m::SPU_20, [SMK_208]","smoking","Health behaviour","Cigarettes/day","NA","Smoking intensity - former daily smokers (peak CPD while smoking). Input to intensity model","Cigarettes per day for former daily smokers (peak while smoking). Input to smoking intensity descriptive tables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMKG01C_cont","Age first cigarette (PUMF grouped)","","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKAG01C, cchs2003_p::SMKCG01C, cchs2005_p::SMKEG01C, cchs2015_2016_p::SMKG035, cchs2017_2018_p::SMKG035, cchs2019_2020_p::SMKG035, cchs2022_p::CSS_10, cchs2023_p::CSS_10, cchs2001_m::SMKA_01C, cchs2003_m::SMKC_01C, cchs2005_m::SMKE_01C, cchs2007_2008_m::SMK_01C, cchs2009_2010_m::SMK_01C, cchs2009_m::SMK_01C, cchs2010_m::SMK_01C, cchs2011_2012_m::SMK_01C, cchs2012_m::SMK_01C, cchs2013_2014_m::SMK_01C, cchs2014_m::SMK_01C, cchs2015_2016_m::SMK_035, cchs2017_2018_m::SMK_035, cchs2019_2020_m::SMK_035, cchs2021_m::SMK_035, cchs2022_m::CSS_10, cchs2023_m::CSS_10, [SMKG01C]","smoking","Health behaviour","Years","NA","PUMF grouped age first cigarette (recoded to midpoints). Intermediate input to age_first_cigarette (cchsflow v3).","Intermediate: PUMF grouped age first cigarette recoded to midpoints. Required by cchsflow v3 to compute age_first_cigarette.","0.1.0","2026-06-09","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMKG040_cont","Age started daily smoking (PUMF grouped)","","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::[SMKG203_pre2005, SMKG207_pre2005], cchs2003_p::[SMKG203_pre2005, SMKG207_pre2005], cchs2005_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2007_2008_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2009_2010_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2010_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2011_2012_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2012_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2013_2014_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2014_p::[SMKG203_2005plus, SMKG207_2005plus], cchs2001_m::[SMK_203, SMK_207], cchs2003_m::[SMK_203, SMK_207], cchs2005_m::[SMK_203, SMK_207], cchs2007_2008_m::[SMK_203, SMK_207], cchs2009_2010_m::[SMK_203, SMK_207], cchs2009_m::[SMK_203, SMK_207], cchs2010_m::[SMK_203, SMK_207], cchs2011_2012_m::[SMK_203, SMK_207], cchs2012_m::[SMK_203, SMK_207], cchs2013_2014_m::[SMK_203, SMK_207], cchs2014_m::[SMK_203, SMK_207], cchs2015_2016_m::SMK_040, cchs2017_2018_m::SMK_040, cchs2019_2020_m::SMK_040, cchs2021_m::SMK_040, cchs2022_m::SPU_15, cchs2023_m::SPU_15, [SMKG040]","smoking","Health behaviour","Years","NA","PUMF grouped age started daily smoking (recoded to midpoints). Intermediate input to age_start_smoking (cchsflow v3).","Intermediate: PUMF grouped age started daily smoking recoded to midpoints. Required by cchsflow v3 to compute age_start_smoking.","0.1.0","2026-06-10","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMKDVSTP","Time since quit (master)","","Continuous","cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_m::SMKCDSTP, cchs2005_m::SMKEDSTP, cchs2007_2008_m::SMKDSTP, cchs2009_2010_m::SMKDSTP, cchs2009_m::SMKDSTP, cchs2010_m::SMKDSTP, cchs2011_2012_m::SMKDSTP, cchs2012_m::SMKDSTP, cchs2013_2014_m::SMKDSTP, cchs2014_m::SMKDSTP, [SMKDVSTP]","smoking","Health behaviour","Years","Master-only StatCan derived time since quit (all former smokers, 0-88 years). No longer a cchsflow feeder: v3 final derives time_quit_smoking from SMK_09A_cont/SMK_06A_cont. Retained for Master (RDC) cross-validation.","Master file derived time since quit smoking (all former smokers). Intermediate input to time_quit_smoking (cchsflow v3). Not available in PUMF.","Master-only cross-validation of PUMF midpoint-imputed years since quit. Not a feeder for any unified variable in cchsflow v3 final.","0.1.0","2026-06-09","","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","master" +"SMK_05D","Ever daily (occ)","Ever smoked cigarettes daily (asked of occasional smokers)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05D, cchs2003_p::SMKC_05D, cchs2005_p::SMKE_05D, cchs2015_2016_p::SMK_030, cchs2017_2018_p::SMK_030, cchs2019_2020_p::SMK_030, cchs2001_m::SMKA_05D, cchs2003_m::SMKC_05D, cchs2005_m::SMKE_05D, cchs2015_2016_m::SMK_030, cchs2017_2018_m::SMK_030, cchs2019_2020_m::SMK_030, cchs2021_m::SMK_030, cchs2022_m::SPU_05, cchs2023_m::SPU_05, [SMK_05D]","smoking","Health behaviour","N/A","NA","Ever smoked daily (asked of occasional smokers).","Intermediate: feeder for SMKDSTY_original 2015-2021 (with SMK_202, SMK_01A).","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMKDSTY_cat5","Smoking (5-cat)","Smoking status (5-category): daily, occasional, former daily, former occasional, never","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2015_2016_p::SMKDVSTY, cchs2017_2018_p::SMKDVSTY, cchs2019_2020_p::SMKDVSTY, cchs2022_p::SMKDVSTY, cchs2023_p::SMKDVSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2015_2016_m::SMKDVSTY, cchs2017_2018_m::SMKDVSTY, cchs2019_2020_m::SMKDVSTY, cchs2021_m::SMKDVSTY, cchs2022_m::SMKDVSTY, cchs2023_m::SMKDVSTY, [SMKDSTY]","smoking","Health behaviour","N/A","NA","Smoking status, 5 categories avoiding the 2015 semantic break.","Intermediate: feeder for time_quit_smoking_daily (cchsflow v3).","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMK_09C","Yrs quit daily","Years since stopped smoking daily - former daily (Master continuous)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09C, cchs2005_m::SMKE_09C, cchs2015_2016_m::SMK_090, cchs2017_2018_m::SMK_090, cchs2019_2020_m::SMK_090, cchs2021_m::SMK_090, [SMK_09C]","smoking","Health behaviour","years","NA","Master continuous years since stopped smoking daily (former daily smokers).","Intermediate: Master exact-years feeder for time_quit_smoking_daily.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"SMK_01C","Age 1st cig","Age smoked first whole cigarette","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::SMKA_01C, cchs2003_m::SMKC_01C, cchs2005_m::SMKE_01C, cchs2015_2016_m::SMK_035, cchs2017_2018_m::SMK_035, cchs2019_2020_m::SMK_035, cchs2021_m::SMK_035, cchs2022_m::CSS_10, cchs2023_m::CSS_10, [SMK_01C]","smoking","Health behaviour","years","NA","Master continuous age smoked first whole cigarette.","Intermediate: Master exact-age feeder for age_first_cigarette.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"SMK_040","Age daily (ever)","Age started smoking cigarettes daily (all ever-daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::[SMK_203, SMK_207], cchs2003_m::[SMK_203, SMK_207], cchs2005_m::[SMK_203, SMK_207], cchs2007_2008_m::[SMK_203, SMK_207], cchs2009_2010_m::[SMK_203, SMK_207], cchs2009_m::[SMK_203, SMK_207], cchs2010_m::[SMK_203, SMK_207], cchs2011_2012_m::[SMK_203, SMK_207], cchs2012_m::[SMK_203, SMK_207], cchs2013_2014_m::[SMK_203, SMK_207], cchs2014_m::[SMK_203, SMK_207], cchs2022_m::SPU_15, cchs2023_m::SPU_15, [SMK_040]","smoking","Health behaviour","years","NA","Master continuous age started smoking daily (all ever-daily smokers).","Intermediate: Master exact-age feeder for age_start_smoking.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"time_quit_smoking","Yrs since quit smoking","Years since quit smoking (combined former daily and occasional)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMK_09A_cont, SMK_06A_cont]","smoking","Health behaviour","years","NA","Unified years since quit, all former smokers (SMK_09A_cont priority, SMK_06A_cont fallback).","Intermediate: feeder for pack_years_der. cchsflow v3 recommended primary cessation measure (all former smokers); study uses time_quit_smoking_daily for the cessation APC numerator.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"smoked_100_lifetime","Smoked 100+ (ever)*","Ever smoked 100 or more cigarettes in lifetime (unified)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMK_01A]","smoking","Health behaviour","N/A","NA","Unified ever smoked 100+ cigarettes (pass-through of SMK_01A).","Established-smoker gate: 100 or more cigarettes in lifetime defines the smoking universe for both transitions; experimental smokers are Never (estimand specification, section 2).","0.1.0","2026-08-27","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","apc-numerator, apc-denominator","both" +"SMKG203_cont","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG203, cchs2003_p::SMKCG203, cchs2005_p::SMKEG203, cchs2015_2016_p::[SMK_005, SMKG040], cchs2017_2018_p::[SMK_005, SMKG040], cchs2019_2020_p::[SMK_005, SMKG040], cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2007_2008_m::SMK_203, cchs2009_2010_m::SMK_203, cchs2011_2012_m::SMK_203, cchs2013_2014_m::SMK_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMKG203]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (current daily smokers)","Intermediate: age started daily, current daily smokers (midpoint). With SMKG207_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMKG207_cont","Age daily (fmr)","Age started smoking cigarettes daily (former daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG207, cchs2003_p::SMKCG207, cchs2005_p::SMKEG207, cchs2015_2016_p::[SMK_005, SMK_030, SMKG040], cchs2017_2018_p::[SMK_005, SMK_030, SMKG040], cchs2019_2020_p::[SMK_005, SMK_030, SMKG040], cchs2001_m::SMKA_207, cchs2003_m::SMKC_207, cchs2005_m::SMKE_207, cchs2007_2008_m::SMK_207, cchs2009_2010_m::SMK_207, cchs2011_2012_m::SMK_207, cchs2013_2014_m::SMK_207, cchs2015_2016_m::[SMK_005, SMK_030, SMK_040], cchs2017_2018_m::[SMK_005, SMK_030, SMK_040], cchs2019_2020_m::[SMK_005, SMK_030, SMK_040], cchs2021_m::[SMK_005, SMK_030, SMK_040], cchs2022_m::[SMK_005, SMK_030, SMK_040], cchs2023_m::[SMK_005, SMK_030, SMK_040], [SMKG207]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (former daily smokers)","Intermediate: age started daily, former daily smokers (midpoint). With SMKG203_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMK_203","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMK_203]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (current daily smokers)","Intermediate: transitive feeder for SMKG040_cont / SMK_040 (Master) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"SMK_207","Age daily (fmr)","Age started smoking cigarettes daily (former daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_207, cchs2003_m::SMKC_207, cchs2005_m::SMKE_207, cchs2015_2016_m::[SMK_005, SMK_030, SMK_040], cchs2017_2018_m::[SMK_005, SMK_030, SMK_040], cchs2019_2020_m::[SMK_005, SMK_030, SMK_040], cchs2021_m::[SMK_005, SMK_030, SMK_040], cchs2022_m::[SMK_005, SMK_030, SMK_040], cchs2023_m::[SMK_005, SMK_030, SMK_040], [SMK_207]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (former daily smokers)","Intermediate: transitive feeder for SMKG040_cont / SMK_040 (Master) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"SMK_005","Smoking freq (2015+)","Type of smoker presently (2015+ era-specific name for SMK_202)","Categorical","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","[SMK_005]","smoking","Health behaviour","N/A","NA","Type of smoker presently (2015+ era-specific name for SMK_202)","Intermediate: transitive feeder for SMK_203, SMK_207 in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMK_030","Ever daily (2015+)","Smoked daily - lifetime (2015+ era-specific name for SMK_05D)","Categorical","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2022_m::SPU_05, cchs2023_m::SPU_05, [SMK_030]","smoking","Health behaviour","N/A","NA","Smoked daily - lifetime (2015+ era-specific name for SMK_05D)","Intermediate: transitive feeder for SMK_207 in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMK_05B","Cigs/day (occ)","Number of cigarettes smoked daily - occasional smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05B, cchs2003_p::SMKC_05B, cchs2005_p::SMKE_05B, cchs2015_2016_p::SMK_050, cchs2017_2018_p::SMK_050, cchs2019_2020_p::SMK_050, cchs2022_p::CSS_30, cchs2023_p::CSS_30, cchs2001_m::SMKA_05B, cchs2003_m::SMKC_05B, cchs2005_m::SMKE_05B, cchs2015_2016_m::SMK_050, cchs2017_2018_m::SMK_050, cchs2019_2020_m::SMK_050, cchs2021_m::SMK_050, cchs2022_m::CSS_30, cchs2023_m::CSS_30, [SMK_05B]","smoking","Health behaviour","cigarettes","NA","Number of cigarettes smoked daily - occasional smokers","Intermediate: cigarettes per day on days smoked (occasional smokers); occasional-period feeder for pack_years_der.","0.1.0","2026-06-10","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"SMK_05C","Days smoked/month","Days smoked at least 1 cigarette in past month","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_05C, cchs2003_p::SMKC_05C, cchs2005_p::SMKE_05C, cchs2015_2016_p::SMK_055, cchs2017_2018_p::SMK_055, cchs2019_2020_p::SMK_055, cchs2022_p::CSS_35, cchs2023_p::CSS_35, cchs2001_m::SMKA_05C, cchs2003_m::SMKC_05C, cchs2005_m::SMKE_05C, cchs2015_2016_m::SMK_055, cchs2017_2018_m::SMK_055, cchs2019_2020_m::SMK_055, cchs2021_m::SMK_055, cchs2022_m::CSS_35, cchs2023_m::CSS_35, [SMK_05C]","smoking","Health behaviour","days","NA","Days smoked at least 1 cigarette in past month","Intermediate: days smoked per month (occasional smokers); occasional-period feeder for pack_years_der.","0.1.0","2026-06-10","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" +"DHH_AGE","Age","Age","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::DHHA_AGE, cchs2003_m::DHHC_AGE, cchs2005_m::DHHE_AGE, cchs2022_m::AWCAGE, cchs2023_m::AWCAGE, [DHH_AGE]","demographics","Sociodemographics","Years","NA","Age","Intermediate: transitive feeder for pack_years_der (Master exact age) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" +"cigs_per_day","Cigs/day (unified)*","Cigarettes per day - unified daily + former daily (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","DerivedVar::[SMK_204, SMK_208, SMKDSTY_original]","smoking","Health behaviour","Cigarettes/day","Unified cigs/day (cchsflow v3). DerivedVar::[SMK_204, SMK_208, SMKDSTY_original]. Universe: ever-daily smokers. Not supported PUMF 2022/2023 (SMK_208 is Master-only via SPU in those cycles).","Unified cigs/day (cchsflow v3). Combines SMK_204 (current daily) and SMK_208 (former daily) automatically.","Smoking intensity for descriptive tables and future dose-response models.","0.1.0","2026-06-11","Replaces separate SMK_204/SMK_208","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac) - removed cchs2022_p (not supported in v3 final)","predictor, table1, imputation-predictor","both" +"pack_years_der","Pack-years (unified)*","Cumulative pack-years - derived (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","DerivedVar::[SMKDSTY_original, DHHGAGE_cont, DHH_AGE, age_start_smoking, cigs_per_day, time_quit_smoking, SMK_204, SMK_208, age_first_cigarette, smoked_100_lifetime]","smoking","Health behaviour","Pack-years","Cumulative pack-years (cchsflow v3). PUMF feeders: SMKDSTY_original, DHHGAGE_cont, age_start_smoking, cigs_per_day, time_quit_smoking, SMK_204, SMK_208, age_first_cigarette, smoked_100_lifetime. Not supported PUMF 2022/2023.","Cumulative pack-years (cchsflow v3). Derived from cigs_per_day and years smoked.","Cumulative smoking exposure measure for descriptive tables.","0.1.0","2026-06-09","PUMF gap: not available 2022","active","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1","both" +"DHHGMS","Marital status","Marital status - (G)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::DHHAGMS, cchs2003_p::DHHCGMS, cchs2005_p::DHHEGMS, [DHHGMS]","Marital Status","Sociodemographics","N/A","NA","Marital status - (G)","Marital status: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","pumf" +"ALCDTTM","Drinker type (last 12 months)","Type of drinker (12 months)","Categorical","cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2015_2016_p::ALCDVTTM, cchs2017_2018_p::ALCDVTTM, cchs2019_2020_p::ALCDVTTM, cchs2015_2016_m::ALCDVTTM, cchs2017_2018_m::ALCDVTTM, cchs2019_2020_m::ALCDVTTM, [ALCDTTM]","Alcohol","Health behaviour","N/A","NA","Type of drinker (12 months)","Drinker type (12 months): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"ALWDWKY","Drinks last week","Weekly consumption of alcohol","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::ALCADWKY, cchs2003_p::ALCCDWKY, cchs2005_p::ALCEDWKY, cchs2015_2016_p::ALWDVWKY, cchs2017_2018_p::ALWDVWKY, cchs2019_2020_p::ALWDVWKY, cchs2001_m::ALCADWKY, cchs2003_m::ALCCDWKY, cchs2005_m::ALCEDWKY, cchs2015_2016_m::ALWDVWKY, cchs2017_2018_m::ALWDVWKY, cchs2019_2020_m::ALWDVWKY, [ALWDWKY]","Alcohol","Health behaviour","drinks/week","NA","Weekly consumption of alcohol","Drinks last week: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"HWTGBMI_der","Derived BMI","Derived Body Mass Index","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","DerivedVar::[HWTGHTM, HWTGWTK]","BMI","Health status","kg/m2","NA","Derived Body Mass Index","BMI (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","pumf" +"GEN_01","Self-perceived health","Self-perceived health","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_01, cchs2003_p::GENC_01, cchs2005_p::GENE_01, cchs2015_2016_p::GEN_005, cchs2017_2018_p::GEN_005, cchs2019_2020_p::GEN_005, cchs2001_m::GENA_01, cchs2003_m::GENC_01, cchs2005_m::GENE_01, cchs2015_2016_m::GEN_005, cchs2017_2018_m::GEN_005, cchs2019_2020_m::GEN_005, [GEN_01]","Self-perceived health","Health status","N/A","NA","Self-perceived health","Self-rated general health: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"GEN_02B","Self-perceived mental health","Self-perceived mental health","Categorical","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2003_p::GENC_02B, cchs2005_p::GENE_02B, cchs2015_2016_p::GEN_015, cchs2017_2018_p::GEN_015, cchs2019_2020_p::GEN_015, cchs2003_m::GENC_02B, cchs2005_m::GENE_02B, cchs2015_2016_m::GEN_015, cchs2017_2018_m::GEN_015, cchs2019_2020_m::GEN_015, [GEN_02B]","Mental health","Health status","N/A","NA","Self-perceived mental health","Self-rated mental health: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"GEN_07","Self-perceived life stress","Self-perceived life stress","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_07, cchs2003_p::GENC_07, cchs2005_p::GENE_07, cchs2015_2016_p::GEN_020, cchs2017_2018_p::GEN_020, cchs2019_2020_p::GEN_020, cchs2001_m::GENA_07, cchs2003_m::GENC_07, cchs2005_m::GENE_07, cchs2015_2016_m::GEN_020, cchs2017_2018_m::GEN_020, cchs2019_2020_m::GEN_020, [GEN_07]","Mental health","Health status","N/A","NA","Self-perceived life stress","Self-perceived life stress: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"GEN_10","Sense of belonging","Sense of belonging in the community","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::GENA_10, cchs2003_p::GENC_10, cchs2005_p::GENE_10, cchs2015_2016_p::GEN_030, cchs2017_2018_p::GEN_030, cchs2019_2020_p::GEN_030, cchs2001_m::GENA_10, cchs2003_m::GENC_10, cchs2005_m::GENE_10, cchs2015_2016_m::GEN_030, cchs2017_2018_m::GEN_030, cchs2019_2020_m::GEN_030, [GEN_10]","Mental health","Health status","N/A","NA","Sense of belonging in the community","Sense of community belonging: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_071","Hypertension","Do you have high blood pressure?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_071, cchs2003_p::CCCC_071, cchs2005_p::CCCE_071, cchs2015_2016_p::CCC_065, cchs2017_2018_p::CCC_065, cchs2019_2020_p::CCC_065, cchs2001_m::CCCA_071, cchs2003_m::CCCC_071, cchs2005_m::CCCE_071, cchs2015_2016_m::CCC_065, cchs2017_2018_m::CCC_065, cchs2019_2020_m::CCC_065, [CCC_071]","Chronic condition","Health status","N/A","NA","Do you have high blood pressure?","Hypertension: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_091","COPD/Emphysema/Bronchitis","Do you have COPD (eg bronchitis, emphysema)?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::[CCC_91A, CCC_91B], cchs2003_p::[CCC_91A, CCC_91B], cchs2005_p::[CCC_91A, CCC_91E, CCC_91F], cchs2007_2008_p::[CCC_91A, CCC_91E, CCC_91F], cchs2015_2016_p::CCC_030, cchs2017_2018_p::CCC_030, cchs2019_2020_p::CCC_030, cchs2001_m::[CCC_91A, CCC_91B], cchs2003_m::[CCC_91A, CCC_91B], cchs2005_m::[CCC_91A, CCC_91E, CCC_91F], cchs2007_2008_m::[CCC_91A, CCC_91E, CCC_91F], cchs2015_2016_m::CCC_030, cchs2017_2018_m::CCC_030, cchs2019_2020_m::CCC_030, [CCC_091]","Chronic condition","Health status","N/A","NA","Do you have COPD (eg bronchitis, emphysema)?","COPD/emphysema (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_101","Diabetes","Do you have diabetes?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_101, cchs2003_p::CCCC_101, cchs2005_p::CCCE_101, cchs2015_2016_p::CCC_095, cchs2017_2018_p::CCC_095, cchs2019_2020_p::CCC_095, cchs2001_m::CCCA_101, cchs2003_m::CCCC_101, cchs2005_m::CCCE_101, cchs2015_2016_m::CCC_095, cchs2017_2018_m::CCC_095, cchs2019_2020_m::CCC_095, [CCC_101]","Chronic condition","Health status","N/A","NA","Do you have diabetes?","Diabetes: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_121","Heart Disease","Do you have heart disease?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_121, cchs2003_p::CCCC_121, cchs2005_p::CCCE_121, cchs2015_2016_p::CCC_085, cchs2017_2018_p::CCC_085, cchs2019_2020_p::CCC_085, cchs2001_m::CCCA_121, cchs2003_m::CCCC_121, cchs2005_m::CCCE_121, cchs2015_2016_m::CCC_085, cchs2017_2018_m::CCC_085, cchs2019_2020_m::CCC_085,[CCC_121]","Chronic condition","Health status","N/A","NA","Do you have heart disease?","Heart disease: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_151","Stroke","Do you suffer from effects of stroke?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::CCCA_151, cchs2003_p::CCCC_151, cchs2005_p::CCCE_151, cchs2015_2016_p::CCC_090, cchs2017_2018_p::CCC_090, cchs2019_2020_p::CCC_090, cchs2001_m::CCCA_151, cchs2003_m::CCCC_151, cchs2005_m::CCCE_151, cchs2015_2016_m::CCC_090, cchs2017_2018_m::CCC_090, cchs2019_2020_m::CCC_090,[CCC_151]","Chronic condition","Health status","N/A","NA","Do you suffer from effects of stroke?","Stroke: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"CCC_280","Mood disorder","Do you have a mood disorder?","Categorical","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2003_p::CCCC_280, cchs2005_p::CCCE_280, cchs2015_2016_p::CCC_195, cchs2017_2018_p::CCC_195, cchs2019_2020_p::CCC_195, cchs2003_m::CCCC_280, cchs2005_m::CCCE_280, cchs2015_2016_m::CCC_195, cchs2017_2018_m::CCC_195, cchs2019_2020_m::CCC_195, [CCC_280]","Chronic condition","Health status","N/A","NA","Do you have a mood disorder?","Mood disorder: auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"energy_exp","Daily energy expenditure","Daily energy expenditure","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","cchs2001_p::PACADEE, cchs2003_p::PACCDEE, cchs2005_p::PACEDEE, cchs2015_2016_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2017_2018_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2019_2020_p::[DHHGAGE_cont, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2001_m::PACADEE, cchs2003_m::PACCDEE, cchs2005_m::PACEDEE, cchs2015_2016_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2017_2018_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], cchs2019_2020_m::[DHH_AGE, PAA_045, PAA_050, PAA_075, PAA_080, PAADVDYS, PAADVVIG, PAYDVTOA, PAYDVADL, PAYDVVIG, PAYDVDYS], [PACDEE]","Exercise","Health behaviour","METS","NA","Daily energy expenditure","Daily energy expenditure (derived): auxiliary imputation predictor and study-base descriptive.","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","table1, imputation-predictor","both" +"HWTGHTM","Height","Height (metres)/self-reported - (D,G)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::HWTAGHT, cchs2003_p::HWTCGHT, cchs2005_p::HWTEGHTM, cchs2015_2016_p::HWTDGHTM, cchs2017_2018_p::HWTDGHTM, cchs2019_2020_p::HWTDGHTM, [HWTGHTM]","Height","Health status","meters","NA","Height (metres)/self-reported - (D,G)","Intermediate: feeder for HWTGBMI_der (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","pumf" +"HWTGWTK","Weight","Weight - kilograms (D,G)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p","cchs2001_p::HWTAGWTK, cchs2003_p::HWTCGWTK, cchs2005_p::HWTEGWTK, cchs2015_2016_p::HWTDGWTK, cchs2017_2018_p::HWTDGWTK, cchs2019_2020_p::HWTDGWTK, [HWTGWTK]","Weight","Health status","kg","NA","Weight - kilograms (D,G)","Intermediate: feeder for HWTGBMI_der (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","pumf" +"CCC_91A","Bronchitis","Do you have chronic bronchitis?","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m","cchs2001_p::CCCA_91A, cchs2003_p::CCCC_91A, cchs2005_p::CCCE_91A, cchs2001_m::CCCA_91A, cchs2003_m::CCCC_91A, cchs2005_m::CCCE_91A, [CCC_91A]","Chronic condition","Health status","N/A","NA","Do you have chronic bronchitis?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"CCC_91B","Emphysema/COPD","Do you have emphysema or COPD?","Categorical","cchs2001_p, cchs2003_p, cchs2001_m, cchs2003_m","cchs2001_p::CCCA_91B, cchs2003_p::CCCC_91B, cchs2001_m::CCCA_91B, cchs2003_m::CCCC_91B","Chronic condition","Health status","N/A","NA","Do you have emphysema or COPD?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"CCC_91E","Emphysema","Do you have emphysema?","Categorical","cchs2005_p, cchs2007_2008_p, cchs2005_m, cchs2007_2008_m","cchs2005_p::CCCE_91E, cchs2007_2008_p::CCC_91E, cchs2005_m::CCCE_91E, cchs2007_2008_m::CCC_91E","Chronic condition","Health status","N/A","NA","Do you have emphysema?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"CCC_91F","COPD","Do you have COPD?","Categorical","cchs2005_p, cchs2007_2008_p, cchs2005_m, cchs2007_2008_m","cchs2005_p::CCCE_91F, cchs2007_2008_p::CCC_91F, cchs2005_m::CCCE_91F, cchs2007_2008_m::CCC_91F","Chronic condition","Health status","N/A","NA","Do you have COPD?","Intermediate: feeder for CCC_091 (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAA_045","Sweat/breathe harder exercises (18+ years old)","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_045]","Exercise","Health behaviour","hours/week","NA","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAA_050","Sweat/breathe harder exercises (18+ years old)","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_050]","Exercise","Health behaviour","minutes/week","NA","Time spent - sweat/breathe hard exercises in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAA_075","Sweat/breathe harder activities (18+ years old)","Time spent - sweat/breathe hard activities in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_075]","Exercise","Health behaviour","hours/week","NA","Time spent - sweat/breathe hard activities in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAA_080","Sweat/breathe harder activities (18+ years old)","Time spent - sweat/breathe hard activities in a week (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAA_080]","Exercise","Health behaviour","minutes/week","NA","Time spent - sweat/breathe hard activities in a week (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAADVDYS","Active days (18+ years old)","Number of active days (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAADVDYS]","Exercise","Health behaviour","days","NA","Number of active days (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAADVVIG","Vigorous activity (18+ years old)","Time spent - vigorous activity (18+ years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAADVVIG]","Exercise","Health behaviour","minutes/week","NA","Time spent - vigorous activity (18+ years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAYDVTOA","Sweat/breathe hard activities (12-17 years old)","Time spent - sweat/breathe hard activities in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVTOA]","Exercise","Health behaviour","minutes/week","NA","Time spent - sweat/breathe hard activities in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAYDVADL","Leisure activities (12-17 years old)","Time spent - leisure activity in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVADL]","Exercise","Health behaviour","minutes/week","NA","Time spent - leisure activity in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAYDVVIG","Vigorous activities (12-17 years old)","Time spent - vigorous activity in a week (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVVIG]","Exercise","Health behaviour","minutes/week","NA","Time spent - vigorous activity in a week (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" +"PAYDVDYS","Active days (12-17 years old)","Number of active days (12-17 years old)","Continuous","cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m","[PAYDVDYS]","Exercise","Health behaviour","minutes/week","NA","Number of active days (12-17 years old)","Intermediate: feeder for energy_exp (cchsflow derivation chain).","0.1.0","2026-06-11","Added as auxiliary imputation predictor / Table 1 variable (protocol v0.3.1)","active","NA","intermediate","both" From 2128257336949ca3059363e7c574ffbb4c871b01 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:15:35 -0400 Subject: [PATCH 19/29] Task 1.3: move value codes into config (no hard-coded status or sex codes) SMKDSTY_original groupings (ever, current, former, never) and the sex codes now live under config.yml survey.* as *_codes / *_code entries, read through survey_code(); R/apc-model.R no longer contains literal status or sex codes (prepare_apc_data, build_initiation_data, build_cessation_data, get_period_constraint). A test changes the former-smoker codes in config and checks the universe classification follows. First step of remediation task 3.1 (value-code semantics layer), scoped to the APC stage. --- R/apc-model.R | 34 ++++++++++++++++++++-------------- config.yml | 19 ++++++++++++++++++- tests/testthat/test-apc-data.R | 15 +++++++++++++++ 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index 4226d07..24b90bd 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -34,10 +34,13 @@ prepare_apc_data <- function(analysis_data, cfg) { data <- derive_survey_year(analysis_data, cfg) - init_men <- build_initiation_data(data[data[[survey_var(cfg, "sex")]] == 1, ], cfg) - init_women <- build_initiation_data(data[data[[survey_var(cfg, "sex")]] == 2, ], cfg) - cess_men <- build_cessation_data(data[data[[survey_var(cfg, "sex")]] == 1, ], cfg) - cess_women <- build_cessation_data(data[data[[survey_var(cfg, "sex")]] == 2, ], cfg) + sex <- data[[survey_var(cfg, "sex")]] + men <- !is.na(sex) & sex == survey_code(cfg, "sex", "men_code") + women <- !is.na(sex) & sex == survey_code(cfg, "sex", "women_code") + init_men <- build_initiation_data(data[men, ], cfg) + init_women <- build_initiation_data(data[women, ], cfg) + cess_men <- build_cessation_data(data[men, ], cfg) + cess_women <- build_cessation_data(data[women, ], cfg) list( initiation_men = apply_survival_correction(init_men, cfg), @@ -103,7 +106,7 @@ build_initiation_data <- function(data, cfg) { # Restrict to valid cohorts data <- data[data$cohort >= cohort_min, ] - # Identify ever-smokers: SMKDSTY_original %in% 1:5, age_first_cigarette >= min_age + # Identify ever-smokers (status codes from config), age_first_cigarette >= min_age # Never-smokers (SMKDSTY_original = 6) carry NA(a) for age_first_cigarette; # 55 is the legitimate midpoint of the "50+ years" category among ever-smokers. # SMKDSTY_original categories: 1=daily, 2=occ(fmr daily), 3=always occ, 4=fmr daily, 5=fmr occ, 6=never @@ -113,7 +116,8 @@ build_initiation_data <- function(data, cfg) { # smokers (a whole cigarette, fewer than 100) are Never: at risk, no event. gate <- data[[survey_var(cfg, "established_smoker")]] gate_yes <- survey_code(cfg, "established_smoker", "yes_code") - ever_smoker <- !is.na(smkdsty) & smkdsty %in% 1:5 & !is.na(gate) & gate == gate_yes + ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") + ever_smoker <- !is.na(smkdsty) & smkdsty %in% ever_codes & !is.na(gate) & gate == gate_yes age_init_raw <- data[[age_col]] @@ -269,12 +273,14 @@ build_cessation_data <- function(data, cfg) { data <- data[!is.na(data$cohort) & data$cohort >= cohort_min, ] - # Universe: established smokers. SMKDSTY_original 1 = daily, 2 = occasional - # (formerly daily), 3 = occasional (never daily), 4 = former daily, - # 5 = former occasional, 6 = never smoked. + # Universe: established smokers. Status codes and their state groupings come + # from config (survey.smoking_status.*_codes), not from literals here. + ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") + current_codes <- survey_code(cfg, "smoking_status", "current_codes") + former_codes <- survey_code(cfg, "smoking_status", "former_codes") smk <- data[[status_col]] gate <- data[[gate_col]] - established <- !is.na(smk) & smk %in% 1:5 & !is.na(gate) & gate == gate_yes + established <- !is.na(smk) & smk %in% ever_codes & !is.na(gate) & gate == gate_yes d <- data[established, ] smk <- d[[status_col]] @@ -285,8 +291,8 @@ build_cessation_data <- function(data, cfg) { weight <- d[[weight_col]] cycle <- as.character(d[[cycle_col]]) # observed cycles only; avoids NA sums for empty levels - current <- smk %in% c(1, 2, 3) - former <- smk %in% c(4, 5) + current <- smk %in% current_codes + former <- smk %in% former_codes # Classification: each established smoker falls in exactly one group missing_entry <- is.na(age_init) @@ -601,10 +607,10 @@ get_period_constraint <- function(model_type, sex, cfg) { pc <- cfg$apc$period_constraints if (model_type == "initiation") { - if (sex == 2) { + if (sex == survey_code(cfg, "sex", "women_code")) { return(pc$initiation_women_from) } - if (sex == 1) { + if (sex == survey_code(cfg, "sex", "men_code")) { return(pc$initiation_men_from) } stop("sex must be 1 or 2, got: ", sex) diff --git a/config.yml b/config.yml index ea03711..3c3e42d 100644 --- a/config.yml +++ b/config.yml @@ -104,11 +104,16 @@ default: cycle: SurveyCycle # Survey cycle identifier (derived, not recoded by cchsflow) # Core demographics + # Value codes live here, not in R code: survey_code(cfg, "sex", "men_code") -> 1. sex: pumf: var: DHH_SEX # Sex (1=male, 2=female) + men_code: 1 + women_code: 2 master: var: DHH_SEX + men_code: 1 + women_code: 2 age: pumf: var: DHHGAGE_cont # Continuous age (years); midpoint-estimated from grouped categories @@ -148,11 +153,23 @@ default: var: EDUDR04 # 4-category education # Smoking history (unified cchsflow v3 variables) + # SMKDSTY_original: 1 = daily, 2 = occasional (formerly daily), 3 = occasional + # (never daily), 4 = former daily, 5 = former occasional, 6 = never smoked. + # The state groupings below implement the estimand specification (section 2): + # current = still smoking (daily or occasional); former = stopped completely. smoking_status: pumf: - var: SMKDSTY_original # 6-cat: daily, occ(fmr daily), always occ, fmr daily, fmr occ, never + var: SMKDSTY_original + ever_codes: [1, 2, 3, 4, 5] + current_codes: [1, 2, 3] + former_codes: [4, 5] + never_code: 6 master: var: SMKDSTY_original + ever_codes: [1, 2, 3, 4, 5] + current_codes: [1, 2, 3] + former_codes: [4, 5] + never_code: 6 age_first_cigarette: pumf: var: age_first_cigarette # Age smoked first whole cigarette (midpoint-estimated) diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index 02619d0..419ecd7 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -286,3 +286,18 @@ test_that("apply_survival_correction: mport raises not-implemented error", { df <- data.frame(age = 1, cohort = 1970, period = 1985, event = 0, weight = 100) expect_error(apply_survival_correction(df, cfg), "not yet implemented") }) + +test_that("value codes are read from config, not hard-coded", { + cfg <- cess_cfg() + expect_equal(survey_code(cfg, "sex", "men_code"), 1) + expect_equal(survey_code(cfg, "smoking_status", "former_codes"), c(4, 5)) + # Relabel the former-smoker codes in config and the universe classification follows + cfg2 <- cfg + cfg2$survey$smoking_status$pumf$former_codes <- c(4) + cfg2$survey$smoking_status$pumf$current_codes <- c(1, 2, 3, 5) + q <- one_person(cfg2, status = 5, age_first = 20, yrs_quit_complete = 10, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(q, cfg2)) + # status 5 is now "current": at risk to survey, no event + expect_equal(sum(result$event), 0L) + expect_equal(max(result$age), 50L) +}) From 625bf6006c86ca2c3f93baadb44d00bb68225638 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:16:41 -0400 Subject: [PATCH 20/29] Docs: value codes and thresholds are configuration; list the config keys --- CLAUDE.md | 4 +++- docs/workflow/7-apc-data-preparation.qmd | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9051611..818252c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,9 @@ The `variableStart` worksheet column uses cchsflow notation: `cchs2001_p::SMKA_0 **Deprecated aliases:** `SMK_005` → `SMK_202`; `SMK_030` → `SMK_05D` -**APC model variables (internal):** `age`, `cohort`, `period`, `init`, `weighting`, `ont_id` +**APC model variables (internal):** `age`, `cohort`, `period`, `event`, `weight` + +**Value codes are configuration, not code.** Status groupings (`survey.smoking_status..ever_codes/current_codes/former_codes/never_code`), sex codes (`survey.sex..men_code/women_code`), the established-smoker gate (`survey.established_smoker..yes_code`), and analytic thresholds (`apc.cessation_durability_years`, bounds via `survey_bound()`) live in `config.yml` and are read with `survey_code()` / `survey_bound()`. Do not write literal codes or thresholds into R. Remaining exception, scheduled as plan task 2.6: literal variable names in `R/imputation.R`. ### cchsflow dependency diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index e77ca56..9baa3e5 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -27,6 +27,8 @@ Configuration used: | `survey$age_first_cigarette` | `config.yml` | Age at first whole cigarette | | `survey$years_since_quit_complete` | `config.yml` | Years since stopping smoking completely (the cessation exit) | | `survey$established_smoker` | `config.yml` | 100-cigarette gate; `yes_code` defines the smoking universe | +| `survey$smoking_status` `*_codes` | `config.yml` | Which `SMKDSTY_original` codes count as ever, current, former, never | +| `survey$sex` `*_code` | `config.yml` | Sex codes used to stratify the models | | `survey_bound(cfg, "age_first_cigarette", "min")` | `config.yml` | APC floor for initiation age (PUMF: 13, Master: 8) | | `apc$cessation_durability_years` | `config.yml` | A quit counts as cessation only after this many years (2) | | `apc$age_knots` | `config.yml` | `[10, 15, 20, 50, 60]` | From 2dd192a960427e49b80b4cad234629005afa296e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:35:47 -0400 Subject: [PATCH 21/29] APC fit guards: refuse empty or zero-event numerators; check convergence Public issue #3, items 4 and 7. fit_binomial_apc() now stops when there are no person-year rows, when the numerator has no events or zero weighted events (a model that would encode 'nobody ever made this transition'), or when a cell has non-positive person-years or more events than person-years; after glm() it stops on non-convergence and warns on a boundary fit. Tests cover the empty and zero-event cases and a converging fit. --- R/apc-model.R | 25 ++++++++++++++++++++++++- tests/testthat/test-apc-data.R | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/R/apc-model.R b/R/apc-model.R index 24b90bd..9aa4f9b 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -640,6 +640,9 @@ get_period_constraint <- function(model_type, sex, cfg) { #' @param weight Numeric vector of survey weights #' @return Fitted glm object (family = binomial) fit_binomial_apc <- function(basis_matrix, event, weight) { + if (length(event) == 0) { + stop("fit_binomial_apc: no person-year rows to fit.") + } # Aggregate to unique basis rows (= unique age-period-cohort cells after clamping) df <- as.data.frame(basis_matrix) df$.event <- event @@ -661,9 +664,29 @@ fit_binomial_apc <- function(basis_matrix, event, weight) { cell_df$.d <- d cell_df$.pop <- pop - glm(cbind(.d, .pop - .d) ~ . - .d - .pop, + # Guards (public issue #3, items 4 and 7): a model fitted to no events, or that + # did not converge, must fail loudly rather than return a plausible-looking fit. + if (sum(event == 1) == 0 || sum(d) <= 0) { + stop( + "fit_binomial_apc: the numerator is empty (", sum(event == 1), " event rows, ", + "weighted events = ", sum(d), "). A model with no events would encode ", + "'nobody ever made this transition'." + ) + } + if (any(pop <= 0) || any(d > pop)) { + stop("fit_binomial_apc: a cell has non-positive person-years or more events than person-years.") + } + + fit <- glm(cbind(.d, .pop - .d) ~ . - .d - .pop, data = cell_df, family = binomial(), control = glm.control(maxit = 100, epsilon = 1e-8) ) + if (!isTRUE(fit$converged)) { + stop("fit_binomial_apc: glm did not converge (", fit$iter, " iterations).") + } + if (isTRUE(fit$boundary)) { + warning("fit_binomial_apc: glm stopped at the boundary of the parameter space; inspect the fit.") + } + fit } diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index 419ecd7..fedf2de 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -301,3 +301,25 @@ test_that("value codes are read from config, not hard-coded", { expect_equal(sum(result$event), 0L) expect_equal(max(result$age), 50L) }) + +test_that("fit_binomial_apc: refuses to fit a model with no events", { + basis <- matrix(c(1, 2, 3, 4, 5, 6), ncol = 1, dimnames = list(NULL, "x")) + expect_error( + fit_binomial_apc(basis, event = rep(0L, 6), weight = rep(100, 6)), + "numerator is empty" + ) + expect_error( + fit_binomial_apc(basis[0, , drop = FALSE], event = integer(0), weight = numeric(0)), + "no person-year rows" + ) +}) + +test_that("fit_binomial_apc: fits and reports convergence when events exist", { + set.seed(1) + x <- rep(seq(-1, 1, length.out = 20), each = 5) + basis <- matrix(x, ncol = 1, dimnames = list(NULL, "x")) + event <- as.integer(runif(length(x)) < plogis(-1 + x)) + fit <- fit_binomial_apc(basis, event = event, weight = rep(10, length(x))) + expect_true(isTRUE(fit$converged)) + expect_s3_class(fit, "glm") +}) From 511fcd464e94dc07fcf6ce0e41eb1369f75a4014 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:37:37 -0400 Subject: [PATCH 22/29] Plain language: replace 'gate', 'universe', 'clock', 'spell' and similar PI style review. 'Established-smoker criterion' replaces 'gate'; 'the model includes' replaces 'universe'; 'time at risk begins' replaces 'clock starts'; 'a person who started and stopped at the same age' replaces 'one-year spell'; the diagnostics group is same_age_quits. Error and guard comments say what happens instead of 'fail loudly' or 'plausible-looking'. Applied to the workflow page, the estimand specification, CLAUDE.md, config comments, the worksheet purpose text, and R comments and messages. --- CLAUDE.md | 4 +-- R/apc-model.R | 27 +++++++++---------- config.yml | 2 +- docs/development/estimand-specification.md | 30 +++++++++++----------- docs/workflow/7-apc-data-preparation.qmd | 4 +-- tests/testthat/test-apc-data.R | 4 +-- worksheets/cshm-variables.csv | 4 +-- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 818252c..66e04f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ python3 ~/github/cchsflow-docs/mcp-server/cli.py compare cchs2013_2014_p cchs201 The `variableStart` worksheet column uses cchsflow notation: `cchs2001_p::SMKA_01A, cchs2007_2008_p::SMK_01A, [SMK_01A]` — `_p` = PUMF, `_m` = Master, `[VAR]` = fallback name. -**Unified variables (preferred):** `age_first_cigarette` (entry), `smoked_100_lifetime` (established-smoker gate), `time_quit_smoking_complete` (cessation exit, 2003+), `age_start_smoking` and `time_quit_smoking_daily` (daily-smoking attributes) +**Unified variables (preferred):** `age_first_cigarette` (entry), `smoked_100_lifetime` (established-smoker criterion), `time_quit_smoking_complete` (cessation exit, 2003+), `age_start_smoking` and `time_quit_smoking_daily` (daily-smoking attributes) **Master-only continuous:** `SMK_01C`, `SMK_040`, `SMK_09C` / `SMK_06C` / `SMK_10C` @@ -120,7 +120,7 @@ The `variableStart` worksheet column uses cchsflow notation: `cchs2001_p::SMKA_0 **APC model variables (internal):** `age`, `cohort`, `period`, `event`, `weight` -**Value codes are configuration, not code.** Status groupings (`survey.smoking_status..ever_codes/current_codes/former_codes/never_code`), sex codes (`survey.sex..men_code/women_code`), the established-smoker gate (`survey.established_smoker..yes_code`), and analytic thresholds (`apc.cessation_durability_years`, bounds via `survey_bound()`) live in `config.yml` and are read with `survey_code()` / `survey_bound()`. Do not write literal codes or thresholds into R. Remaining exception, scheduled as plan task 2.6: literal variable names in `R/imputation.R`. +**Value codes are configuration, not code.** Status groupings (`survey.smoking_status..ever_codes/current_codes/former_codes/never_code`), sex codes (`survey.sex..men_code/women_code`), the established-smoker criterion (`survey.established_smoker..yes_code`), and analytic thresholds (`apc.cessation_durability_years`, bounds via `survey_bound()`) live in `config.yml` and are read with `survey_code()` / `survey_bound()`. Do not write literal codes or thresholds into R. Remaining exception, scheduled as plan task 2.6: literal variable names in `R/imputation.R`. ### cchsflow dependency diff --git a/R/apc-model.R b/R/apc-model.R index 9aa4f9b..a8a0b0c 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -111,7 +111,7 @@ build_initiation_data <- function(data, cfg) { # 55 is the legitimate midpoint of the "50+ years" category among ever-smokers. # SMKDSTY_original categories: 1=daily, 2=occ(fmr daily), 3=always occ, 4=fmr daily, 5=fmr occ, 6=never smkdsty <- data[[status_col]] - # Established-smoker gate (estimand specification, section 2): only people who + # Established-smoker criterion (estimand specification, section 2): only people who # have smoked 100 or more cigarettes enter the smoking states. Experimental # smokers (a whole cigarette, fewer than 100) are Never: at risk, no event. gate <- data[[survey_var(cfg, "established_smoker")]] @@ -193,8 +193,8 @@ build_initiation_data <- function(data, cfg) { #' `age_denom_max`, restricted to the calendar window `period_range`. #' #' @param denom_source Data frame with: person_id, cohort, age_denom_max, weight, -#' and optionally `age_denom_min` (per-person start age; the cessation clock -#' starts at each person's own entry age). Rows without it use `min_age`. +#' and optionally `age_denom_min` (per-person start age; time at risk of +#' cessation begins at each person's own entry age). Rows without it use `min_age`. #' @param period_range Integer vector of calendar years #' @param min_age Default minimum age for being at risk (used when #' `age_denom_min` is absent or NA) @@ -239,12 +239,12 @@ expand_denominator <- function(denom_source, period_range, min_age) { #' Build the cessation numerator and denominator dataset #' #' Implements the estimand specification (docs/development/estimand-specification.md). -#' The universe is established smokers (100 or more cigarettes; SMKDSTY 1 to 5). +#' The model includes established smokers (100 or more cigarettes; SMKDSTY 1 to 5). #' The event is stopping smoking completely, dated by `years_since_quit_complete`. -#' Each person's risk clock starts at their own age at first whole cigarette. +#' Each person's time at risk begins at their own age at first whole cigarette. #' A quit counts only if it has lasted `cfg$apc$cessation_durability_years` at the #' survey; otherwise the person is current at survey and censored at the quit age. -#' A quit at the entry age is a one-year spell: one trial, with the event. +#' A person who started and stopped at the same age contributes one trial, with the event. #' #' People whose entry age is missing or later than the survey age, whose quit #' precedes their entry, or whose quit timing is missing (including the 2001 @@ -273,7 +273,7 @@ build_cessation_data <- function(data, cfg) { data <- data[!is.na(data$cohort) & data$cohort >= cohort_min, ] - # Universe: established smokers. Status codes and their state groupings come + # Included population: established smokers. Status codes and their state groupings come # from config (survey.smoking_status.*_codes), not from literals here. ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") current_codes <- survey_code(cfg, "smoking_status", "current_codes") @@ -309,7 +309,7 @@ build_cessation_data <- function(data, cfg) { current_at_survey = !excluded & current, durable_quitters = durable, recent_quitters_censored = recent, - same_age_spells = same_age, + same_age_quits = same_age, excluded_missing_entry = missing_entry, excluded_entry_after_survey = entry_after_survey, excluded_timing_missing = timing_missing, @@ -338,7 +338,7 @@ build_cessation_data <- function(data, cfg) { "Cessation risk set: ", totals[["established"]], " established smokers; ", totals[["durable_quitters"]], " durable quitters (events); ", totals[["recent_quitters_censored"]], " recent quitters censored; ", - totals[["same_age_spells"]], " same-age spells. Excluded pending imputation: ", + totals[["same_age_quits"]], " started and stopped at the same age. Excluded pending imputation: ", totals[["excluded_missing_entry"]], " missing entry age, ", totals[["excluded_entry_after_survey"]], " entry after survey, ", totals[["excluded_timing_missing"]], " missing quit timing, ", @@ -358,7 +358,8 @@ build_cessation_data <- function(data, cfg) { # risk from entry to the survey year (included as a full year). Durable and # recent quitters are at risk from entry to the year before the quit year: the # quit year is the event row for durable quitters and unobservable for recent - # quitters. A same-age spell has no denominator row; its one trial is the event. + # quitters. Someone who started and stopped at the same age has no denominator row; + # their one trial is the event. in_denom <- !excluded & (current | durable | recent) age_denom_max <- ifelse(current[in_denom], age_survey[in_denom], age_quit[in_denom] - 1L) denom_source <- data.frame( @@ -665,12 +666,12 @@ fit_binomial_apc <- function(basis_matrix, event, weight) { cell_df$.pop <- pop # Guards (public issue #3, items 4 and 7): a model fitted to no events, or that - # did not converge, must fail loudly rather than return a plausible-looking fit. + # did not converge, stops with an error rather than returning a fit that looks valid. if (sum(event == 1) == 0 || sum(d) <= 0) { stop( "fit_binomial_apc: the numerator is empty (", sum(event == 1), " event rows, ", - "weighted events = ", sum(d), "). A model with no events would encode ", - "'nobody ever made this transition'." + "weighted events = ", sum(d), "). A model with no events would estimate ", + "that this transition never happens." ) } if (any(pop <= 0) || any(d > pop)) { diff --git a/config.yml b/config.yml index 3c3e42d..77495a8 100644 --- a/config.yml +++ b/config.yml @@ -190,7 +190,7 @@ default: var: age_start_smoking # Exact age from SMK_040 min: 8 max: 100 - # Established-smoker gate (estimand specification, section 2): at least 100 + # Established-smoker criterion (estimand specification, section 2): at least 100 # cigarettes in lifetime. Experimental smokers (fewer than 100) are Never. # `yes_code` is the value meaning "smoked 100 or more" (CCHS SMK_01A: 1 = yes, 2 = no). established_smoker: diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index fd008ef..552aa7f 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -1,7 +1,7 @@ # Smoking states and transitions: analysis specification **Task 1.0 of the remediation plan.** Implements protocol v0.4.0, section 3.4.1, and adjudication item A1 (2026-08-07). -**Status:** ratified in full by the PI, 2026-08-27, after external review: the established-smoker gate (100 cigarettes; experimental smokers are Never) and the other state gates, the per-transition treatment of immigrants (censored before arrival in Canada), the same-age rule, and the 2001 decision. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. +**Status:** ratified in full by the PI, 2026-08-27, after external review: the established-smoker criterion (100 cigarettes; experimental smokers are Never) and the other state definitions, the per-transition treatment of immigrants (censored before arrival in Canada), the same-age rule, and the 2001 decision. **Depends on** private PR #4 (task 1.7a), which introduces the `"none"` mortality-correction value used in section 6; merge #4 first. ## 1. Why this document exists @@ -11,13 +11,13 @@ The pipeline built its initiation model on one definition of a smoker (anyone wh Each person is in exactly one state at each age. -**The established-smoker gate (ratified 2026-08-27).** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The gate is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who pass the gate; it does not by itself make anyone a smoker. +**The established-smoker criterion (ratified 2026-08-27).** A person enters the model's smoking states only if they have smoked at least 100 cigarettes in their lifetime. This is the Manuel et al. (2020) rule, and it follows the CCHS convention: respondents who have smoked a whole cigarette but fewer than 100 in total (experimental smokers) are treated as non-smokers. The criterion is observed with the unified variable `smoked_100_lifetime` (cchsflow; PUMF 2001 to 2019--20) and its source question `SMK_01A` (all cycles, including 2022). Age at first whole cigarette supplies the timing of entry only for people who meet the criterion; it does not by itself make anyone a smoker. | State | Definition | How it is observed at survey | |---|---|---| | Never | Has not smoked 100 cigarettes: never smoked a whole cigarette, or smoked fewer than 100 (experimental smoker) | `smoked_100_lifetime` = no; includes `SMKDSTY_original` never smoked and, from 2015, the SMKDVSTY experimental-smoker category | -| Current | Passed the gate and has not stopped smoking completely (daily or occasional) | `smoked_100_lifetime` = yes and `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | -| Former | Passed the gate and has stopped smoking completely | `smoked_100_lifetime` = yes and `SMKDSTY_original` = former daily or former occasional | +| Current | Meets the criterion and has not stopped smoking completely (daily or occasional) | `smoked_100_lifetime` = yes and `SMKDSTY_original` = daily, occasional (formerly daily), or occasional (never daily) | +| Former | Meets the criterion and has stopped smoking completely | `smoked_100_lifetime` = yes and `SMKDSTY_original` = former daily or former occasional | The table gives the observed status at survey. The modelled state can differ from it: the durability rule in section 4 moves people who quit less than two years before the survey from observed Former to modelled Current. @@ -27,25 +27,25 @@ Two points follow from the definitions. Stopping daily smoking while still smoki | Transition | From | To | Event age | CCHS variable (cchsflow v3) | |---|---|---|---|---| -| Initiation | Never | Current | Age at first whole cigarette, for people who pass the gate | `age_first_cigarette` | +| Initiation | Never | Current | Age at first whole cigarette, for people who meet the criterion | `age_first_cigarette` | | Cessation | Current | Former | Age at which the person stopped smoking completely | survey age minus `time_quit_smoking_complete` | Not modelled as transitions: relapse (Former back to Current) and daily onset (Current to a daily sub-state). The CCHS records one quit per person, so relapse cannot be estimated from it; its influence is a sensitivity analysis (protocol section 3.5). Daily onset (`age_start_smoking`) is retained as a characteristic of current smokers and for the intensity model. -The CCHS does not ask the age at which the 100th cigarette was smoked, so the age at first whole cigarette is the entry age for everyone who passes the gate. This follows Manuel et al. (2020) and is a known approximation: entry is dated to the start of smoking, not to the point at which it became established. +The CCHS does not ask the age at which the 100th cigarette was smoked, so the age at first whole cigarette is the entry age for everyone who meets the criterion. This follows Manuel et al. (2020) and is a known approximation: entry is dated to the start of smoking, not to the point at which it became established. -**Consequence for the code.** The cessation model currently uses the ever-daily universe and `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the universe is all established smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. +**Consequence for the code.** The cessation model currently includes only ever-daily smokers and uses `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the model includes all established smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. **The 2001 cycle (decided 2026-08-27).** `time_quit_smoking_complete` is derived from questions first asked in 2003. For 2001 the timing of complete cessation is treated as not asked in that cycle (NA(c)) and handled by the imputation procedure for cycle-level absence (Appendix D); the 2001 `time_quit_smoking` variable, which lacks the "stopped completely" question, is not used as a substitute. ## 4. Event-time conventions - **Time step and interval.** One year. The row for age *a* covers the year from the person's *a*-th birthday to the day before the next one. An event at age *a* happened during that year. The event row is part of the risk set: it carries one trial, with the event. Within a year, initiation is applied before cessation, so a person who starts and stops at the same age has a one-year spell (below). The year of the survey is the last observed row for everyone; it is treated as a full year of exposure, a simplification shared with the Manuel and Holford implementations. -- **One spell per person.** A person enters Current once and leaves it at most once. +- **One period of smoking per person.** A person enters Current once and leaves it at most once. - **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. - **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. - **Durable cessation and recent quitters.** The primary definition of cessation is a quit that has lasted at least two years at the survey. Three things are distinguished for a person who quit less than two years before the survey. *Observed status:* Former (`SMKDSTY_original`). *Modelled state:* Current at every age up to the survey, because the quit is not yet known to be durable; this is the state used for prevalence and passed to the generator. *Cessation risk set:* person-years from entry to the reported quit age, then censored with no event; the years between the quit age and the survey are not in the risk set because whether the quit will hold cannot yet be observed. The person therefore has exactly one modelled state at each age (Current) while contributing to the risk set only up to the quit age. Risk-set membership describes what can be observed about the outcome; it is not the state. -- **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* treat it as a one-year spell. The person enters Current at that age, contributes one person-year at risk of cessation at that age, and has the cessation event in it. *Prespecified sensitivity:* remove the person from both transition models -- no initiation event and no cessation spell -- treating them as never having established smoking, so that reconstructed prevalence does not acquire an initiation without its cessation. Few respondents, possibly none, are expected to meet this condition; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. +- **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* the person smoked for one year. They enter Current at that age, contribute one person-year at risk of cessation at that age, and have the cessation event in it. *Prespecified sensitivity:* remove the person from both transition models -- no initiation event and no cessation record -- treating them as never having established smoking, so that reconstructed prevalence does not acquire an initiation without its cessation. Few respondents, possibly none, are expected to have started and stopped at the same age; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. - **Reported ages that cannot be right** (initiation after survey age, cessation before initiation) are treated as missing and enter the imputation procedure (task 1.8c). No person is silently reclassified. ## 5. Target population and risk-set entry @@ -72,25 +72,25 @@ The generator (shg-rcpp) consumes the rate tables and produces, for each simulat | Item | Decision | Source | |---|---|---| | State model | Established-smoking model: Never, Current, Former | Adjudication A1, 2026-08-07 | -| Established-smoker gate | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020; PI decision, 2026-08-27 | +| Established-smoker criterion | At least 100 cigarettes in lifetime; experimental smokers are Never | Manuel et al. 2020; PI decision, 2026-08-27 | | 2001 cycle | Complete-cessation timing is NA(c); imputed | PI decision, 2026-08-27 | | Interval convention | Age row = year from the *a*-th birthday; event row in risk set; initiation before cessation within a year; survey year included as a full year | Specified here, 2026-08-27 | | Entry event | First whole cigarette (`age_first_cigarette`) | A1; Manuel et al. 2020 | | Exit event | Stopped smoking completely (`time_quit_smoking_complete`) | A1; protocol 3.4.1 | | Durability | Two years; more recent quitters are current at survey | Protocol 3.4.1 | -| Same-age rule | One-year spell (primary); exclusion (sensitivity) | PI decision, 2026-08-27 | +| Same-age rule | One year of smoking with the event in it (primary); exclusion (sensitivity) | PI decision, 2026-08-27 | | Relapse | Not modelled; sensitivity analysis | Protocol 3.4.1 | | Immigration entry | Censored before arrival in Canada; per-transition delayed entry (section 5); PUMF approximation in task 1.9 | Protocol 3.3; adjudication B1; PI decision, 2026-08-27 | ## 8. What changes in the pipeline because of this note -- [ ] 1.3: cessation universe = ever-smokers; exit variable = `time_quit_smoking_complete`; clock from `age_first_cigarette`; recent-quitter censoring; same-age rule. +- [ ] 1.3: cessation model includes all established smokers; exit variable = `time_quit_smoking_complete`; time at risk begins at `age_first_cigarette`; recent-quitter censoring; same-age rule. - [ ] Config: replace the `years_since_quit` mapping (`time_quit_smoking_daily`) with the complete-cessation variable; keep `age_start_daily` for the intensity model only. -- [ ] Gate: add `smoked_100_lifetime` (with `SMK_01A` for 2022) to the variables sheet as the universe variable for both transitions; experimental smokers map to Never. +- [ ] Criterion: add `smoked_100_lifetime` (with `SMK_01A` for 2022) to the variables sheet as the inclusion variable for both transitions; experimental smokers map to Never. - [ ] 2001: complete-cessation timing tagged NA(c) and routed to the cycle-level imputation path. - [ ] Worksheets: roles for `time_quit_smoking_complete` (apc-numerator, apc-denominator); `age_start_smoking` loses its cessation role. -- [ ] 1.3 diagnostic: report the unweighted and weighted number of same-age spells per cycle. +- [ ] 1.3 diagnostic: report the unweighted and weighted number of people who started and stopped at the same age, per cycle. - [ ] 1.2: initiation window aligned to the entry event above. - [ ] 1.9: per-transition delayed entry (section 5), including exclusion of pre-immigration initiation events. -- [ ] 1.8c: imputation universes follow section 2 (state gates first). +- [ ] 1.8c: imputation follows the state definitions in section 2 (state membership imputed first). - [ ] Generator contract (section 6) added to the rate-table schema description. diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index 9baa3e5..7f2e19d 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -26,7 +26,7 @@ Configuration used: | `survey$smoking_status` | `config.yml` | 6-category smoking status | | `survey$age_first_cigarette` | `config.yml` | Age at first whole cigarette | | `survey$years_since_quit_complete` | `config.yml` | Years since stopping smoking completely (the cessation exit) | -| `survey$established_smoker` | `config.yml` | 100-cigarette gate; `yes_code` defines the smoking universe | +| `survey$established_smoker` | `config.yml` | 100-cigarette criterion; `yes_code` is the value that includes a person in the smoking models | | `survey$smoking_status` `*_codes` | `config.yml` | Which `SMKDSTY_original` codes count as ever, current, former, never | | `survey$sex` `*_code` | `config.yml` | Sex codes used to stratify the models | | `survey_bound(cfg, "age_first_cigarette", "min")` | `config.yml` | APC floor for initiation age (PUMF: 13, Master: 8) | @@ -90,7 +90,7 @@ The spline basis columns are built in Stage 8 (`build_spline_basis()`), not stor **Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weighting` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). -**Who is in the cessation model, and when the clock starts.** The universe is established smokers (100 or more cigarettes in their lifetime, `smoked_100_lifetime`), whatever their current pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person is at risk from their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count: the person is current at survey and censored at the quit age. A quit at the entry age is a one-year spell. People with missing entry age or quit timing (including the whole 2001 cycle, where the stopped-completely questions were not asked) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. +**Cessation model: who is included and when time at risk begins.** The cessation model includes established smokers: people who have smoked 100 or more cigarettes in their lifetime (`smoked_100_lifetime`), whatever their current smoking pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person's time at risk begins at their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count as cessation: the person is a current smoker at the survey and their time at risk ends at the quit age without an event. A person who started and stopped at the same age contributes one year at risk with the event in it. People with a missing entry age or missing quit timing (including the whole 2001 cycle, where the stopped-completely questions were not asked) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. **Four separate data frames, not one.** Men and women are modelled separately (consistent with Manuel et al. 2020). Initiation and cessation are separate models. Keeping four data frames lets Stage 8 fit all four models in parallel using `{targets}` branching. diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index fedf2de..e99d584 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -157,7 +157,7 @@ test_that("build_cessation_data: recent quitter is censored at the quit age with expect_equal(sum(diag$n[diag$group == "recent_quitters_censored"]), 1L) }) -test_that("build_cessation_data: same-age initiation and cessation is one trial with the event", { +test_that("build_cessation_data: starting and stopping at the same age is one trial with the event", { cfg <- cess_cfg() s <- one_person(cfg, status = 5, age_first = 30, yrs_quit_complete = 20, age = 50, survey_year = 2010) result <- suppressMessages(build_cessation_data(s, cfg)) @@ -165,7 +165,7 @@ test_that("build_cessation_data: same-age initiation and cessation is one trial expect_equal(result$event, 1L) expect_equal(result$age, 30L) diag <- attr(result, "cessation_diagnostics") - expect_equal(sum(diag$n[diag$group == "same_age_spells"]), 1L) + expect_equal(sum(diag$n[diag$group == "same_age_quits"]), 1L) }) test_that("build_cessation_data: missing quit timing (e.g. 2001, NA(c)) is excluded and counted, not reclassified", { diff --git a/worksheets/cshm-variables.csv b/worksheets/cshm-variables.csv index 8a59102..f2152ee 100644 --- a/worksheets/cshm-variables.csv +++ b/worksheets/cshm-variables.csv @@ -11,7 +11,7 @@ "SMK_01B","Smoked 1 whole cig","Ever smoked a whole cigarette","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_01B, cchs2003_p::SMKC_01B, cchs2005_p::SMKE_01B, cchs2015_2016_p::SMK_025, cchs2017_2018_p::SMK_025, cchs2019_2020_p::SMK_025, cchs2022_p::CSS_05, cchs2023_p::CSS_05, cchs2001_m::SMKA_01B, cchs2003_m::SMKC_01B, cchs2005_m::SMKE_01B, cchs2015_2016_m::SMK_025, cchs2017_2018_m::SMK_025, cchs2019_2020_m::SMK_025, cchs2021_m::SMK_025, cchs2022_m::CSS_05, cchs2023_m::CSS_05, [SMK_01B]","smoking","Health behaviour","N/A","NA","Second gate for never-smoker definition: never smoked a whole cigarette AND <100 lifetime cigarettes = never smoker","Intermediate: input to SMKDSTY_A derivation. Gate variable: ever smoked a whole cigarette. Used with SMK_01A to define never-smoker status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_202","Smoking type","Type of smoker presently (daily/occasional/not at all)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKA_202, cchs2003_p::SMKC_202, cchs2005_p::SMKE_202, cchs2015_2016_p::SMK_005, cchs2017_2018_p::SMK_005, cchs2019_2020_p::SMK_005, cchs2001_m::SMKA_202, cchs2003_m::SMKC_202, cchs2005_m::SMKE_202, cchs2015_2016_m::SMK_005, cchs2017_2018_m::SMK_005, cchs2019_2020_m::SMK_005, cchs2021_m::SMK_005, [SMK_202]","smoking","Health behaviour","N/A","NA","Current smoking frequency. Combined with SMK_01A/SMK_01B to derive 3-category status: never / current / former","Intermediate: input to SMKDSTY_A derivation. Current smoking frequency (daily/occasional/not at all). Combined with SMK_01A/SMK_01B to derive 3-category smoking status.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMKDSTY_original","Smoking status (6-cat)","Type of smoker derived - 6-category (cchsflow v3, original StatCan scheme)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2007_2008_p::SMKDSTY, cchs2009_2010_p::SMKDSTY, cchs2010_p::SMKDSTY, cchs2011_2012_p::SMKDSTY, cchs2012_p::SMKDSTY, cchs2013_2014_p::SMKDSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2007_2008_m::SMKDSTY, cchs2009_2010_m::SMKDSTY, cchs2009_m::SMKDSTY, cchs2010_m::SMKDSTY, cchs2011_2012_m::SMKDSTY, cchs2012_m::SMKDSTY, cchs2013_2014_m::SMKDSTY, cchs2014_m::SMKDSTY, DerivedVar::[SMK_202, SMK_05D, SMK_01A]","smoking","Health behaviour","N/A","NA","cchsflow v3 harmonized 6-cat smoking status: 1=daily, 2=occ(fmr daily), 3=always occasional, 4=former daily, 5=former occasional, 6=never. Consistent categories across all cycles.","Primary smoking classification for APC numerator construction and Table 1. cchsflow v3: 2001-2014 pass-through from SMKDSTY; 2015-2021 derived from SMK_202, SMK_05D, SMK_01A. Not supported PUMF 2022/2023.","0.1.0","2026-06-11","Renamed from SMKDSTY_A per CEP-002 year-based naming","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically. Also the start of each established smoker's cessation risk clock (task 1.3).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" +"age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically. Also the age at which each established smoker's time at risk of cessation begins (task 1.3).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" "age_start_smoking","Age daily (unified)*","Age started smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG040_cont, SMK_040]","smoking","Health behaviour","Years","Raw SMKG040 absent from 2019-20 PUMF (DDI-confirmed): age_start_smoking unavailable for cchs2019_2020_p and cchs2022_p; Master covers 2001-2023. See cchsflow#185.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_040); PUMF: midpoint-estimated (SMKG040_cont). Primary daily initiation age","Age started smoking daily (unified cchsflow v3 variable). Used in the initiation APC model as an alternative or supplementary age measure.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" "time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023.","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Years since stopped daily smoking. Retained for the intensity model and the daily-smoking sensitivity analysis; no longer the cessation exit (see time_quit_smoking_complete).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, imputation-predictor","both" "time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003: not available in 2001 (NA(c); imputed per Appendix D).","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, imputation-predictor","both" @@ -29,7 +29,7 @@ "SMK_01C","Age 1st cig","Age smoked first whole cigarette","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::SMKA_01C, cchs2003_m::SMKC_01C, cchs2005_m::SMKE_01C, cchs2015_2016_m::SMK_035, cchs2017_2018_m::SMK_035, cchs2019_2020_m::SMK_035, cchs2021_m::SMK_035, cchs2022_m::CSS_10, cchs2023_m::CSS_10, [SMK_01C]","smoking","Health behaviour","years","NA","Master continuous age smoked first whole cigarette.","Intermediate: Master exact-age feeder for age_first_cigarette.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" "SMK_040","Age daily (ever)","Age started smoking cigarettes daily (all ever-daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_m::[SMK_203, SMK_207], cchs2003_m::[SMK_203, SMK_207], cchs2005_m::[SMK_203, SMK_207], cchs2007_2008_m::[SMK_203, SMK_207], cchs2009_2010_m::[SMK_203, SMK_207], cchs2009_m::[SMK_203, SMK_207], cchs2010_m::[SMK_203, SMK_207], cchs2011_2012_m::[SMK_203, SMK_207], cchs2012_m::[SMK_203, SMK_207], cchs2013_2014_m::[SMK_203, SMK_207], cchs2014_m::[SMK_203, SMK_207], cchs2022_m::SPU_15, cchs2023_m::SPU_15, [SMK_040]","smoking","Health behaviour","years","NA","Master continuous age started smoking daily (all ever-daily smokers).","Intermediate: Master exact-age feeder for age_start_smoking.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" "time_quit_smoking","Yrs since quit smoking","Years since quit smoking (combined former daily and occasional)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMK_09A_cont, SMK_06A_cont]","smoking","Health behaviour","years","NA","Unified years since quit, all former smokers (SMK_09A_cont priority, SMK_06A_cont fallback).","Intermediate: feeder for pack_years_der. cchsflow v3 recommended primary cessation measure (all former smokers); study uses time_quit_smoking_daily for the cessation APC numerator.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" -"smoked_100_lifetime","Smoked 100+ (ever)*","Ever smoked 100 or more cigarettes in lifetime (unified)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMK_01A]","smoking","Health behaviour","N/A","NA","Unified ever smoked 100+ cigarettes (pass-through of SMK_01A).","Established-smoker gate: 100 or more cigarettes in lifetime defines the smoking universe for both transitions; experimental smokers are Never (estimand specification, section 2).","0.1.0","2026-08-27","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","apc-numerator, apc-denominator","both" +"smoked_100_lifetime","Smoked 100+ (ever)*","Ever smoked 100 or more cigarettes in lifetime (unified)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMK_01A]","smoking","Health behaviour","N/A","NA","Unified ever smoked 100+ cigarettes (pass-through of SMK_01A).","Established-smoker criterion: 100 or more cigarettes in lifetime defines who is included in both transition models; experimental smokers are Never (estimand specification, section 2).","0.1.0","2026-08-27","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","apc-numerator, apc-denominator","both" "SMKG203_cont","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG203, cchs2003_p::SMKCG203, cchs2005_p::SMKEG203, cchs2015_2016_p::[SMK_005, SMKG040], cchs2017_2018_p::[SMK_005, SMKG040], cchs2019_2020_p::[SMK_005, SMKG040], cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2007_2008_m::SMK_203, cchs2009_2010_m::SMK_203, cchs2011_2012_m::SMK_203, cchs2013_2014_m::SMK_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMKG203]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (current daily smokers)","Intermediate: age started daily, current daily smokers (midpoint). With SMKG207_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" "SMKG207_cont","Age daily (fmr)","Age started smoking cigarettes daily (former daily smokers)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKAG207, cchs2003_p::SMKCG207, cchs2005_p::SMKEG207, cchs2015_2016_p::[SMK_005, SMK_030, SMKG040], cchs2017_2018_p::[SMK_005, SMK_030, SMKG040], cchs2019_2020_p::[SMK_005, SMK_030, SMKG040], cchs2001_m::SMKA_207, cchs2003_m::SMKC_207, cchs2005_m::SMKE_207, cchs2007_2008_m::SMK_207, cchs2009_2010_m::SMK_207, cchs2011_2012_m::SMK_207, cchs2013_2014_m::SMK_207, cchs2015_2016_m::[SMK_005, SMK_030, SMK_040], cchs2017_2018_m::[SMK_005, SMK_030, SMK_040], cchs2019_2020_m::[SMK_005, SMK_030, SMK_040], cchs2021_m::[SMK_005, SMK_030, SMK_040], cchs2022_m::[SMK_005, SMK_030, SMK_040], cchs2023_m::[SMK_005, SMK_030, SMK_040], [SMKG207]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (former daily smokers)","Intermediate: age started daily, former daily smokers (midpoint). With SMKG203_cont, feeds SMKG040_cont for 2001-2014.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","both" "SMK_203","Age daily (curr)","Age started smoking cigarettes daily (current daily smokers)","Continuous","cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_m::SMKA_203, cchs2003_m::SMKC_203, cchs2005_m::SMKE_203, cchs2015_2016_m::[SMK_005, SMK_040], cchs2017_2018_m::[SMK_005, SMK_040], cchs2019_2020_m::[SMK_005, SMK_040], cchs2021_m::[SMK_005, SMK_040], cchs2022_m::[SMK_005, SMK_040], cchs2023_m::[SMK_005, SMK_040], [SMK_203]","smoking","Health behaviour","years","NA","Age started smoking cigarettes daily (current daily smokers)","Intermediate: transitive feeder for SMKG040_cont / SMK_040 (Master) in the cchsflow v3 derivation chain.","0.1.0","2026-06-09","Added for cchsflow v3 derivation chain; Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","active","NA","intermediate","master" From 787cf330c28646d1cd2c64c394d1be6d1fb90482 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 14:39:07 -0400 Subject: [PATCH 23/29] Plain language: rename 'gate' identifiers to smoked_100; plainer test names; last 'spell' --- R/apc-model.R | 14 +++++++------- docs/development/estimand-specification.md | 2 +- tests/testthat/test-apc-data.R | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index a8a0b0c..6d70d60 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -114,10 +114,10 @@ build_initiation_data <- function(data, cfg) { # Established-smoker criterion (estimand specification, section 2): only people who # have smoked 100 or more cigarettes enter the smoking states. Experimental # smokers (a whole cigarette, fewer than 100) are Never: at risk, no event. - gate <- data[[survey_var(cfg, "established_smoker")]] - gate_yes <- survey_code(cfg, "established_smoker", "yes_code") + smoked_100 <- data[[survey_var(cfg, "established_smoker")]] + smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") - ever_smoker <- !is.na(smkdsty) & smkdsty %in% ever_codes & !is.na(gate) & gate == gate_yes + ever_smoker <- !is.na(smkdsty) & smkdsty %in% ever_codes & !is.na(smoked_100) & smoked_100 == smoked_100_yes age_init_raw <- data[[age_col]] @@ -258,8 +258,8 @@ expand_denominator <- function(denom_source, period_range, min_age) { #' `cessation_diagnostics` (per-cycle counts, unweighted and weighted) build_cessation_data <- function(data, cfg) { status_col <- survey_var(cfg, "smoking_status") - gate_col <- survey_var(cfg, "established_smoker") - gate_yes <- survey_code(cfg, "established_smoker", "yes_code") + smoked_100_col <- survey_var(cfg, "established_smoker") + smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") quit_col <- survey_var(cfg, "years_since_quit_complete") init_col <- survey_var(cfg, "age_first_cigarette") age_col <- survey_var(cfg, "age") @@ -279,8 +279,8 @@ build_cessation_data <- function(data, cfg) { current_codes <- survey_code(cfg, "smoking_status", "current_codes") former_codes <- survey_code(cfg, "smoking_status", "former_codes") smk <- data[[status_col]] - gate <- data[[gate_col]] - established <- !is.na(smk) & smk %in% ever_codes & !is.na(gate) & gate == gate_yes + smoked_100 <- data[[smoked_100_col]] + established <- !is.na(smk) & smk %in% ever_codes & !is.na(smoked_100) & smoked_100 == smoked_100_yes d <- data[established, ] smk <- d[[status_col]] diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 552aa7f..9dadf73 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -40,7 +40,7 @@ The CCHS does not ask the age at which the 100th cigarette was smoked, so the ag ## 4. Event-time conventions -- **Time step and interval.** One year. The row for age *a* covers the year from the person's *a*-th birthday to the day before the next one. An event at age *a* happened during that year. The event row is part of the risk set: it carries one trial, with the event. Within a year, initiation is applied before cessation, so a person who starts and stops at the same age has a one-year spell (below). The year of the survey is the last observed row for everyone; it is treated as a full year of exposure, a simplification shared with the Manuel and Holford implementations. +- **Time step and interval.** One year. The row for age *a* covers the year from the person's *a*-th birthday to the day before the next one. An event at age *a* happened during that year. The event row is part of the risk set: it carries one trial, with the event. Within a year, initiation is applied before cessation, so a person who starts and stops at the same age is counted as having smoked for one year (below). The year of the survey is the last observed row for everyone; it is treated as a full year of exposure, a simplification shared with the Manuel and Holford implementations. - **One period of smoking per person.** A person enters Current once and leaves it at most once. - **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. - **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index e99d584..d4f0463 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -108,20 +108,20 @@ one_person <- function(cfg, status, smoked_100 = 1, age_first = 16, yrs_quit_com df } -test_that("build_cessation_data: universe is established smokers, all ever-smoker statuses", { +test_that("build_cessation_data: includes established smokers of every ever-smoker status", { cfg <- cess_cfg() data <- make_apc_test_data(cfg) result <- suppressMessages(build_cessation_data(data, cfg)) diag <- attr(result, "cessation_diagnostics") expect_true(is.data.frame(diag)) established <- sum(diag$n[diag$group == "established"]) - gate <- data[[survey_var(cfg, "established_smoker")]] + smoked_100 <- data[[survey_var(cfg, "established_smoker")]] smk <- data[[survey_var(cfg, "smoking_status")]] - expect_equal(established, sum(!is.na(gate) & gate == 1 & smk %in% 1:5 & data$cohort >= cfg$apc$cohort_min)) + expect_equal(established, sum(!is.na(smoked_100) & smoked_100 == 1 & smk %in% 1:5 & data$cohort >= cfg$apc$cohort_min)) expect_true(all(result$event %in% c(0L, 1L))) }) -test_that("build_cessation_data: experimental smokers (under 100 cigarettes) are outside the universe", { +test_that("build_cessation_data: experimental smokers (under 100 cigarettes) are not included", { cfg <- cess_cfg() exp_smoker <- one_person(cfg, status = 4, smoked_100 = 2, age_first = 15, yrs_quit_complete = 10) result <- suppressMessages(build_cessation_data(exp_smoker, cfg)) @@ -291,7 +291,7 @@ test_that("value codes are read from config, not hard-coded", { cfg <- cess_cfg() expect_equal(survey_code(cfg, "sex", "men_code"), 1) expect_equal(survey_code(cfg, "smoking_status", "former_codes"), c(4, 5)) - # Relabel the former-smoker codes in config and the universe classification follows + # Relabel the former-smoker codes in config and the classification follows cfg2 <- cfg cfg2$survey$smoking_status$pumf$former_codes <- c(4) cfg2$survey$smoking_status$pumf$current_codes <- c(1, 2, 3, 5) From 8a099a52fe2354218a576b461ac38963b99df1a7 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 15:58:00 -0400 Subject: [PATCH 24/29] Task 1.3: add feeders for time_quit_smoking_complete; check feeder closure at Stage 1 The first CI pipeline run failed after 56 minutes at the imputation stage: time_quit_smoking_complete was never derived because two of its cchsflow inputs, SMK_10_gate and SMK_10A_cont, were not in the study variables sheet, and rec_with_table() skips a derivation with missing inputs without an error. Both are added as intermediate rows. A new check_feeder_closure() reads every DerivedVar rule for a study variable from the variable-details sheet and stops in the coverage_check target if any input is absent from the variables sheet, so this class of gap fails in seconds at Stage 1. Tests: the project worksheets pass the check; a synthetic sheet with a missing feeder fails with a named message. Verified locally: harmonizing the 2013-14 and 2001 fixtures yields time_quit_smoking_complete (present for 2013-14, absent for 2001 as expected). --- R/validate-coverage.R | 44 +++++++++++++++++++++++++ _targets.R | 5 +-- tests/testthat/test-validate-coverage.R | 20 +++++++++++ worksheets/cshm-variables.csv | 2 ++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/testthat/test-validate-coverage.R diff --git a/R/validate-coverage.R b/R/validate-coverage.R index ae7ebc9..1a76e6a 100644 --- a/R/validate-coverage.R +++ b/R/validate-coverage.R @@ -154,3 +154,47 @@ validate_cycle_coverage <- function(variables_sheet, invisible(list(declared = declared_gaps, critical = critical_gaps)) } + + +#' Check that every derived study variable has all of its feeders in the sheet +#' +#' cchsflow derives a variable only when every input listed in its +#' `DerivedVar::[...]` rule is present. A missing feeder makes rec_with_table() +#' skip the derived variable without an error, and the gap is noticed only when a +#' later stage looks for the column (in the first CI run of task 1.3, after 56 +#' minutes). This check reads the derivation rules from the variable-details +#' sheet and stops at Stage 1 if a feeder is absent from the variables sheet. +#' +#' @param variables_sheet Study variables worksheet (data frame) +#' @param variable_details_sheet Combined variable-details worksheet (data frame) +#' @return Invisibly, a data frame of (variable, missing_feeder) pairs; stops if +#' any row exists +check_feeder_closure <- function(variables_sheet, variable_details_sheet) { + study <- unique(variables_sheet$variable) + det <- variable_details_sheet[variable_details_sheet$variable %in% study, c("variable", "variableStart")] + rules <- det[grepl("DerivedVar::\\[", det$variableStart), ] + gaps <- list() + for (i in seq_len(nrow(rules))) { + inner <- sub(".*DerivedVar::\\[([^]]*)\\].*", "\\1", rules$variableStart[i]) + feeders <- trimws(strsplit(inner, ",")[[1]]) + missing <- setdiff(feeders, study) + if (length(missing)) { + gaps[[length(gaps) + 1]] <- data.frame( + variable = rules$variable[i], missing_feeder = missing, stringsAsFactors = FALSE + ) + } + } + gaps <- if (length(gaps)) { + unique(do.call(rbind, gaps)) + } else { + data.frame(variable = character(0), missing_feeder = character(0)) + } + if (nrow(gaps) > 0) { + stop( + "Derived study variables with feeders missing from worksheets/cshm-variables.csv: ", + paste(unique(paste0(gaps$variable, " needs ", gaps$missing_feeder)), collapse = "; "), + ". Add the feeders as intermediate rows; cchsflow skips the derivation silently otherwise." + ) + } + invisible(gaps) +} diff --git a/_targets.R b/_targets.R index 6ce582b..173d84c 100644 --- a/_targets.R +++ b/_targets.R @@ -30,10 +30,11 @@ list( # Stage 0: Pre-flight validation — verify variable coverage before loading data # Returns gap report (declared + critical); warns or errors per cfg$strict_validation - tar_target(coverage_check, + tar_target(coverage_check, { + check_feeder_closure(variables_sheet, variable_details_sheet) validate_cycle_coverage(variables_sheet, variable_details_sheet, cfg, strict = cfg$strict_validation %||% FALSE) - ), + }), # Stage 2: Load and harmonize CCHS cycles # Combined harmonized cycles, study variables only (stored in the _targets/ store) diff --git a/tests/testthat/test-validate-coverage.R b/tests/testthat/test-validate-coverage.R new file mode 100644 index 0000000..8c21f22 --- /dev/null +++ b/tests/testthat/test-validate-coverage.R @@ -0,0 +1,20 @@ +test_that("check_feeder_closure passes on the project worksheets", { + # testthat runs from tests/testthat; setup.R defines project_root + ws <- function(f) file.path(project_root, "worksheets", f) + vars <- read.csv(ws("cshm-variables.csv")) + det <- as.data.frame(dplyr::bind_rows( + read.csv(ws("cchsflow-variable-details.csv")), + read.csv(ws("cshm-variable-details.csv")) + )) + expect_silent(check_feeder_closure(vars, det)) +}) + +test_that("check_feeder_closure stops when a derived variable's feeder is missing", { + vars <- data.frame(variable = c("derived_x", "feeder_a"), stringsAsFactors = FALSE) + det <- data.frame( + variable = c("derived_x", "derived_x", "feeder_a"), + variableStart = c("DerivedVar::[feeder_a, feeder_b]", "DerivedVar::[feeder_a, feeder_b]", "cchs2001_p::A"), + stringsAsFactors = FALSE + ) + expect_error(check_feeder_closure(vars, det), "derived_x needs feeder_b") +}) diff --git a/worksheets/cshm-variables.csv b/worksheets/cshm-variables.csv index f2152ee..bf0be11 100644 --- a/worksheets/cshm-variables.csv +++ b/worksheets/cshm-variables.csv @@ -17,6 +17,8 @@ "time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003: not available in 2001 (NA(c); imputed per Appendix D).","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, imputation-predictor","both" "SMK_09A_cont","Yrs quit daily (PUMF)","Years since stopped smoking daily - former daily (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_09A, cchs2003_p::SMKC_09A, cchs2005_p::SMKE_09A, cchs2015_2016_p::SMK_080, cchs2017_2018_p::SMK_080, cchs2019_2020_p::SMK_080, cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09A, cchs2005_m::SMKE_09A, cchs2015_2016_m::SMK_080, cchs2017_2018_m::SMK_080, cchs2019_2020_m::SMK_080, cchs2021_m::SMK_080, cchs2023_m::SPU_25, [SMK_09A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former daily smokers. Feeder for time_quit_smoking and time_quit_smoking_daily (cchsflow v3). Not available 2022 or PUMF 2023 (SPU_25 is Master-only).","PUMF-derived continuous years since quit. Superseded by time_quit_smoking once cchsflow v3 merges. Keep for pre-v3 fallback","Intermediate: PUMF/Master midpoint feeder for the unified cessation variables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_06A_cont","Yrs quit occ (PUMF)","Years since stopped smoking - former occasional smokers (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_06A, cchs2003_p::SMKC_06A, cchs2005_p::SMKE_06A, cchs2015_2016_p::SMK_060, cchs2017_2018_p::SMK_060, cchs2019_2020_p::SMK_060, cchs2001_m::SMKA_06A, cchs2003_m::SMKC_06A, cchs2005_m::SMKE_06A, cchs2015_2016_m::SMK_060, cchs2017_2018_m::SMK_060, cchs2019_2020_m::SMK_060, cchs2021_m::SMK_060, cchs2023_m::SPU_10, [SMK_06A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former occasional smokers. Feeder for time_quit_smoking (cchsflow v3 falls back to it when SMK_09A_cont is not applicable). Not available 2022 or PUMF 2023.","PUMF-derived years since quit for former occasional smokers. Not covered by time_quit_smoking (daily only)","Intermediate: occasional-smoker feeder for time_quit_smoking (all former smokers).","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" +"SMK_10A_cont","Yrs quit (reducer)","Years since quit completely (former daily who continued occasional, continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2003_p::SMKC_10A, cchs2005_p::SMKE_10A, cchs2015_2016_p::SMK_100, cchs2017_2018_p::SMK_100, cchs2019_2020_p::SMK_100, cchs2003_m::SMKC_10A, cchs2005_m::SMKE_10A, cchs2015_2016_m::SMK_100, cchs2017_2018_m::SMK_100, cchs2019_2020_m::SMK_100, cchs2021_m::SMK_100, cchs2023_m::SPU_35, [SMK_10A]","Smoking","Health behaviour","years","cchsflow v3 building block; not used directly by pipeline code.","Years since quit completely (former daily who continued occasional, continuous)","Intermediate: years since stopped smoking completely (midpoint, gradual quitters); feeder for time_quit_smoking_complete.","0.2.0","2026-08-27","Added under task 1.3: feeder closure for time_quit_smoking_complete (found by the CI pipeline run).","","","intermediate","both" +"SMK_10_gate","Quit gate","Quit completely when stopped daily (gate variable)","Categorical","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_p::SMKC_10, cchs2005_p::SMKE_10, cchs2015_2016_p::SMK_095, cchs2017_2018_p::SMK_095, cchs2019_2020_p::SMK_095, cchs2003_m::SMKC_10, cchs2005_m::SMKE_10, cchs2015_2016_m::SMK_095, cchs2017_2018_m::SMK_095, cchs2019_2020_m::SMK_095, cchs2021_m::SMK_095, cchs2022_m::SPU_30, cchs2023_m::SPU_30, [SMK_10]","Smoking","Health behaviour","N/A","cchsflow v3 building block; not used directly by pipeline code.","Quit completely when stopped daily (gate variable)","Intermediate: routes former smokers to the stopped-completely questions; feeder for time_quit_smoking_complete.","0.2.0","2026-08-27","Added under task 1.3: feeder closure for time_quit_smoking_complete (found by the CI pipeline run).","","","intermediate","both" "SMKDGSTP_cont","Yrs since quit (all)","Years since quit smoking completely - all former smokers (continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2003_p::SMKCGSTP, cchs2005_p::SMKEGSTP, cchs2007_2008_p::SMKGSTP, cchs2009_2010_p::SMKGSTP, cchs2010_p::SMKGSTP, cchs2011_2012_p::SMKGSTP, cchs2012_p::SMKGSTP, cchs2013_2014_p::SMKGSTP, cchs2003_m::SMKCDSTP, cchs2005_m::SMKEDSTP, cchs2007_2008_m::SMKDSTP, cchs2009_2010_m::SMKDSTP, cchs2009_m::SMKDSTP, cchs2010_m::SMKDSTP, cchs2011_2012_m::SMKDSTP, cchs2012_m::SMKDSTP, cchs2013_2014_m::SMKDSTP, cchs2014_m::SMKDSTP, cchs2015_2016_m::SMKDVSTP, cchs2017_2018_m::SMKDVSTP, cchs2019_2020_m::SMKDVSTP, cchs2021_m::SMKDVSTP, cchs2022_m::SMKDVSTP, cchs2023_m::SMKDVSTP, [SMKDGSTP]","smoking","Health behaviour","Years","NA","StatCan derived continuous years since quit (all former smokers). Available 2009+. Preferred where available; cross-validate with SMK_09A_cont","StatsCan derived years since quit (all former smokers). Available from 2007 onward. Cross-validates SMK_09A_cont estimates.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_204","Cigs/day (current)","Number of cigarettes smoked daily - current daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2022_p, cchs2023_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_204, cchs2003_p::SMKC_204, cchs2005_p::SMKE_204, cchs2015_2016_p::SMK_045, cchs2017_2018_p::SMK_045, cchs2019_2020_p::SMK_045, cchs2022_p::CSS_25, cchs2023_p::CSS_25, cchs2001_m::SMKA_204, cchs2003_m::SMKC_204, cchs2005_m::SMKE_204, cchs2015_2016_m::SMK_045, cchs2017_2018_m::SMK_045, cchs2019_2020_m::SMK_045, cchs2021_m::SMK_045, cchs2022_m::CSS_25, cchs2023_m::CSS_25, [SMK_204]","smoking","Health behaviour","Cigarettes/day","NA","Smoking intensity - current daily smokers. Input to intensity (CPD) model","Cigarettes per day for current daily smokers. Input to smoking intensity descriptive tables and future intensity model.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_208","Cigs/day (former)","Number of cigarettes smoked daily - former daily smokers","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","cchs2001_p::SMKA_208, cchs2003_p::SMKC_208, cchs2005_p::SMKE_208, cchs2015_2016_p::SMK_075, cchs2017_2018_p::SMK_075, cchs2019_2020_p::SMK_075, cchs2001_m::SMKA_208, cchs2003_m::SMKC_208, cchs2005_m::SMKE_208, cchs2015_2016_m::SMK_075, cchs2017_2018_m::SMK_075, cchs2019_2020_m::SMK_075, cchs2021_m::SMK_075, cchs2022_m::SPU_20, cchs2023_m::SPU_20, [SMK_208]","smoking","Health behaviour","Cigarettes/day","NA","Smoking intensity - former daily smokers (peak CPD while smoking). Input to intensity model","Cigarettes per day for former daily smokers (peak while smoking). Input to smoking intensity descriptive tables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" From 22753f65834bce4c60416cacad057128b506848e Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 16:32:58 -0400 Subject: [PATCH 25/29] Task 1.3: fix review findings -- entry age, exclusions, quit-timing bounds External review of PR #7 (three blocking, four further findings). Cessation risk now begins at each person's own age at first cigarette; the reporting floor is no longer applied to the start age. Quit timing is validated: it must be finite, within the configured bounds for the data source (survey.years_since_quit_complete min/max), and place the quit no later than the survey; otherwise the person is excluded and counted (excluded_quit_timing_invalid). The durability setting must be present in config; the hard-coded fallback is gone. The initiation builder is rewritten with the same classification discipline: never smokers and experimental smokers are at risk from the floor age to the survey with no event; established smokers have one event at their entry age; established smokers who started below the floor were already smoking when observation begins and contribute nothing here (they stay in the cessation model); missing status, missing 100-cigarette answer, missing entry age, and entry after the survey or outside the configured bounds are excluded and counted, never reclassified as Never. Per-cycle counts are the initiation_diagnostics attribute (shared summarise_groups helper). expand_denominator() coerces cohort and ages to integers again. The worksheet row for time_quit_smoking_complete gains apc-denominator and documents that the variable is absent from the 2022 PUMF as well as 2001 (both NA(c), imputed); the specification, config comment, and workflow page say the same. Stage 7 docs use event and weight. The test that hard-coded status and criterion codes reads them from config. New respondent-level invariant tests: risk from an entry age below the floor; negative and out-of-bounds quit durations excluded; no person-year after the survey age or before entry; missing or invalid initiation records excluded rather than treated as Never; never smokers at risk from the floor to the survey; early initiators contribute no initiation rows. --- R/apc-model.R | 232 ++++++++++++--------- config.yml | 4 +- docs/development/estimand-specification.md | 2 +- docs/workflow/7-apc-data-preparation.qmd | 8 +- tests/testthat/test-apc-data.R | 83 +++++++- worksheets/cshm-variables.csv | 2 +- 6 files changed, 219 insertions(+), 112 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index 6d70d60..b3377cb 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -89,101 +89,139 @@ derive_survey_year <- function(data, cfg) { } -#' Build combined initiation numerator + denominator dataset +#' Build the initiation numerator and denominator dataset #' -#' @param data Data frame for one sex, with survey_year and cohort columns +#' Implements the estimand specification. Never smokers (status = never code) and +#' experimental smokers (smoked a whole cigarette but fewer than 100) are at risk +#' from the study floor age to the survey year, with no event. Established +#' smokers (100 or more cigarettes) have one event at their age at first whole +#' cigarette and are at risk from the floor age to the year before it. An +#' established smoker whose entry age is below the floor was already smoking +#' when observation begins and contributes nothing to this model (they remain in +#' the cessation model). Records with a missing status, a missing 100-cigarette +#' answer among ever-smokers, a missing entry age, or an entry age after the +#' survey age or outside the configured bounds are excluded and counted; task +#' 1.8c routes them through imputation. Nobody is reclassified as Never because +#' their information is missing. +#' +#' @param data Analysis data (one row per respondent) with a `cohort` column #' @param cfg Config object -#' @return Long-format data frame: age, cohort, period, event, weight +#' @return Data frame with age, cohort, period, event, weight, plus the attribute +#' `initiation_diagnostics` (per-cycle counts, unweighted and weighted) build_initiation_data <- function(data, cfg) { status_col <- survey_var(cfg, "smoking_status") - age_col <- survey_var(cfg, "age_first_cigarette") + init_col <- survey_var(cfg, "age_first_cigarette") + age_col <- survey_var(cfg, "age") weight_col <- survey_var(cfg, "weight") - min_age <- survey_bound(cfg, "age_first_cigarette", "min") + cycle_col <- survey_var(cfg, "cycle") + smoked_100_col <- survey_var(cfg, "established_smoker") + smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") + ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") + never_code <- survey_code(cfg, "smoking_status", "never_code") + floor_age <- survey_bound(cfg, "age_first_cigarette", "min") + init_max <- survey_bound(cfg, "age_first_cigarette", "max") cohort_min <- cfg$apc$cohort_min period_min <- cfg$apc$period_min period_max <- cfg$apc$period_max - # Restrict to valid cohorts - data <- data[data$cohort >= cohort_min, ] - - # Identify ever-smokers (status codes from config), age_first_cigarette >= min_age - # Never-smokers (SMKDSTY_original = 6) carry NA(a) for age_first_cigarette; - # 55 is the legitimate midpoint of the "50+ years" category among ever-smokers. - # SMKDSTY_original categories: 1=daily, 2=occ(fmr daily), 3=always occ, 4=fmr daily, 5=fmr occ, 6=never - smkdsty <- data[[status_col]] - # Established-smoker criterion (estimand specification, section 2): only people who - # have smoked 100 or more cigarettes enter the smoking states. Experimental - # smokers (a whole cigarette, fewer than 100) are Never: at risk, no event. - smoked_100 <- data[[survey_var(cfg, "established_smoker")]] - smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") - ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") - ever_smoker <- !is.na(smkdsty) & smkdsty %in% ever_codes & !is.na(smoked_100) & smoked_100 == smoked_100_yes - - age_init_raw <- data[[age_col]] - - # The analytic floor is survey_bound(cfg, "age_first_cigarette", "min"): - # 13 for PUMF, 8 for Master per config.yml. Note SMKG01C_cont has a 5-11 - # category (midpoint 8) in all PUMF cycles, so a floor of 13 excludes that - # group — whether to lower the PUMF floor to 8 is an open study decision. - # Source of truth for category midpoints: cchsflow variable_details.csv (recEnd). - ages_among_smokers <- age_init_raw[ever_smoker & !is.na(age_init_raw)] - if (length(ages_among_smokers) > 0 && min(ages_among_smokers) > 10) { - warning( - "min(age_first_cigarette) = ", min(ages_among_smokers), - " among ever-smokers — early-initiation categories appear absent or ", - "excluded by the configured floor (", min_age, "). RDC Master run will ", - "use exact ages." - ) - } + data <- data[!is.na(data$cohort) & data$cohort >= cohort_min, ] - # Issue 2: flag implausible initiation ages (age_first > current age) - age_survey <- data[[survey_var(cfg, "age")]] - implausible <- ever_smoker & !is.na(age_init_raw) & age_init_raw > age_survey - n_implausible <- sum(implausible, na.rm = TRUE) - if (n_implausible > 0) { - message("Excluding ", n_implausible, " rows with age_first_cigarette > current age.") - } + smk <- data[[status_col]] + smoked_100 <- data[[smoked_100_col]] + age_init <- as.integer(round(data[[init_col]])) + age_survey <- as.integer(round(data[[age_col]])) + weight <- data[[weight_col]] + cycle <- as.character(data[[cycle_col]]) + + status_missing <- is.na(smk) + never <- !status_missing & smk == never_code + ever_status <- !status_missing & smk %in% ever_codes + criterion_missing <- ever_status & is.na(smoked_100) + experimental <- ever_status & !is.na(smoked_100) & smoked_100 != smoked_100_yes + established <- ever_status & !is.na(smoked_100) & smoked_100 == smoked_100_yes + missing_entry <- established & is.na(age_init) + entry_invalid <- established & !is.na(age_init) & + (age_init > age_survey | age_init > init_max | age_init < 0) + entry_before_floor <- established & !is.na(age_init) & !entry_invalid & age_init < floor_age + initiator <- established & !is.na(age_init) & !entry_invalid & age_init >= floor_age + at_risk_to_survey <- never | experimental + excluded <- status_missing | criterion_missing | missing_entry | entry_invalid - # Valid initiators: ever-smoker, plausible age, age >= min_age - valid_init <- ever_smoker & - !is.na(age_init_raw) & - age_init_raw >= min_age & - !implausible + groups <- list( + respondents = rep(TRUE, nrow(data)), + never_smokers = never, + experimental_smokers = experimental, + initiators = initiator, + entered_before_floor = entry_before_floor, + excluded_status_missing = status_missing, + excluded_criterion_missing = criterion_missing, + excluded_missing_entry = missing_entry, + excluded_entry_invalid = entry_invalid + ) + diag <- summarise_groups(groups, cycle, weight) + totals <- vapply(groups, sum, numeric(1)) + message( + "Initiation risk set: ", totals[["respondents"]], " respondents; ", + totals[["initiators"]], " initiators (events); ", totals[["never_smokers"]], + " never and ", totals[["experimental_smokers"]], " experimental smokers at risk; ", + totals[["entered_before_floor"]], " entered before the floor age (no rows). ", + "Excluded pending imputation: ", totals[["excluded_status_missing"]], " status missing, ", + totals[["excluded_criterion_missing"]], " 100-cigarette answer missing, ", + totals[["excluded_missing_entry"]], " entry age missing, ", + totals[["excluded_entry_invalid"]], " entry age invalid." + ) - # Numerator: one row per initiator - num <- data[valid_init, ] - age_num <- as.integer(round(num[[age_col]])) numerator <- data.frame( - age = age_num, - cohort = num$cohort, - period = num$cohort + age_num, - event = rep(1L, nrow(num)), - weight = num[[weight_col]] + age = age_init[initiator], + cohort = data$cohort[initiator], + period = data$cohort[initiator] + age_init[initiator], + event = rep(1L, sum(initiator)), + weight = weight[initiator] ) - # Denominator: person-years at risk before initiation - # Person attributes needed for expand + in_denom <- at_risk_to_survey | initiator + age_denom_max <- ifelse(initiator[in_denom], age_init[in_denom] - 1L, age_survey[in_denom]) denom_source <- data.frame( - person_id = seq_len(nrow(data)), - cohort = data$cohort, - age_init = ifelse(valid_init, as.integer(round(age_init_raw)), NA_integer_), - # Never-smokers and invalid: treat as still at risk through end of period range - age_survey = as.integer(round(data[[survey_var(cfg, "age")]])), - weight = data[[weight_col]] - ) - # For never-smokers (no initiation), denominator runs to survey age (proxy for period_max) - # For initiators, denominator runs up to (but not including) age_init - denom_source$age_denom_max <- ifelse( - is.na(denom_source$age_init), - denom_source$age_survey, # never initiated — at risk through observed age - denom_source$age_init - 1L # initiated — at risk until year before initiation + person_id = seq_len(sum(in_denom)), + cohort = data$cohort[in_denom], + age_denom_min = rep(floor_age, sum(in_denom)), + age_denom_max = age_denom_max, + weight = weight[in_denom] ) - period_range <- seq(period_min, period_max) + denominator <- expand_denominator(denom_source, period_range, floor_age) - denominator <- expand_denominator(denom_source, period_range, min_age) + out <- rbind(numerator, denominator) + attr(out, "initiation_diagnostics") <- diag + out +} - rbind(numerator, denominator) + +#' Per-cycle counts (unweighted and weighted) for a list of logical groups +#' +#' @param groups Named list of logical vectors of equal length +#' @param cycle Character vector of cycle codes, same length +#' @param weight Numeric weights, same length +#' @return Data frame: group, cycle, n, weighted +summarise_groups <- function(groups, cycle, weight) { + empty <- data.frame( + group = character(0), cycle = character(0), + n = integer(0), weighted = numeric(0), stringsAsFactors = FALSE + ) + if (length(cycle) == 0) { + return(empty) + } + do.call(rbind, lapply(names(groups), function(g) { + sel <- groups[[g]] + agg_n <- tapply(as.integer(sel), cycle, sum) + agg_w <- tapply(weight * sel, cycle, sum) + agg_n[is.na(agg_n)] <- 0L + agg_w[is.na(agg_w)] <- 0 + data.frame( + group = g, cycle = names(agg_n), n = as.integer(agg_n), + weighted = as.numeric(agg_w), stringsAsFactors = FALSE + ) + })) } @@ -214,9 +252,10 @@ expand_denominator <- function(denom_source, period_range, min_age) { rep(min_age, n) } rows <- vector("list", n) + age_min <- as.integer(round(age_min)) for (i in seq_len(n)) { - co <- denom_source$cohort[i] - am <- denom_source$age_denom_max[i] + co <- as.integer(round(denom_source$cohort[i])) + am <- as.integer(round(denom_source$age_denom_max[i])) w <- denom_source$weight[i] if (is.na(am) || is.na(co)) next # At risk from their own start age to age_denom_max, within the calendar window @@ -265,8 +304,11 @@ build_cessation_data <- function(data, cfg) { age_col <- survey_var(cfg, "age") weight_col <- survey_var(cfg, "weight") cycle_col <- survey_var(cfg, "cycle") - floor_age <- survey_bound(cfg, "age_first_cigarette", "min") - durability <- cfg$apc$cessation_durability_years %||% 2 + floor_age <- survey_bound(cfg, "age_first_cigarette", "min") # default for expand_denominator only + durability <- cfg$apc$cessation_durability_years + if (is.null(durability)) stop("cfg$apc$cessation_durability_years is not set.") + quit_min <- survey_bound(cfg, "years_since_quit_complete", "min") + quit_max <- survey_bound(cfg, "years_since_quit_complete", "max") cohort_min <- cfg$apc$cohort_min period_min <- cfg$apc$period_min period_max <- cfg$apc$period_max @@ -298,8 +340,13 @@ build_cessation_data <- function(data, cfg) { missing_entry <- is.na(age_init) entry_after_survey <- !missing_entry & age_init > age_survey timing_missing <- former & is.na(yrs_quit) - quit_before_entry <- former & !is.na(age_quit) & !missing_entry & age_quit < age_init - excluded <- missing_entry | entry_after_survey | timing_missing | quit_before_entry + # Quit timing must be finite, within the configured bounds for the source + # (PUMF top-code, Master ceiling), and place the quit no later than the survey. + timing_invalid <- former & !is.na(yrs_quit) & + (!is.finite(yrs_quit) | yrs_quit < quit_min | yrs_quit > quit_max | + is.na(age_quit) | age_quit > age_survey | age_quit < 0) + quit_before_entry <- former & !is.na(age_quit) & !timing_invalid & !missing_entry & age_quit < age_init + excluded <- missing_entry | entry_after_survey | timing_missing | timing_invalid | quit_before_entry recent <- !excluded & former & yrs_quit < durability durable <- !excluded & former & yrs_quit >= durability same_age <- durable & age_quit == age_init @@ -313,26 +360,10 @@ build_cessation_data <- function(data, cfg) { excluded_missing_entry = missing_entry, excluded_entry_after_survey = entry_after_survey, excluded_timing_missing = timing_missing, + excluded_quit_timing_invalid = timing_invalid, excluded_quit_before_entry = quit_before_entry ) - diag <- do.call(rbind, lapply(names(groups), function(g) { - sel <- groups[[g]] - if (length(sel) == 0) { - return(data.frame( - group = character(0), cycle = character(0), - n = integer(0), weighted = numeric(0), stringsAsFactors = FALSE - )) - } - agg_n <- tapply(as.integer(sel), cycle, sum) - agg_w <- tapply(weight * sel, cycle, sum) - agg_n[is.na(agg_n)] <- 0L - agg_w[is.na(agg_w)] <- 0 - data.frame( - group = g, cycle = names(agg_n), - n = as.integer(agg_n), weighted = as.numeric(agg_w), - stringsAsFactors = FALSE - ) - })) + diag <- summarise_groups(groups, cycle, weight) totals <- vapply(groups, sum, numeric(1)) message( "Cessation risk set: ", totals[["established"]], " established smokers; ", @@ -342,6 +373,7 @@ build_cessation_data <- function(data, cfg) { totals[["excluded_missing_entry"]], " missing entry age, ", totals[["excluded_entry_after_survey"]], " entry after survey, ", totals[["excluded_timing_missing"]], " missing quit timing, ", + totals[["excluded_quit_timing_invalid"]], " quit timing out of bounds, ", totals[["excluded_quit_before_entry"]], " quit before entry." ) @@ -365,7 +397,7 @@ build_cessation_data <- function(data, cfg) { denom_source <- data.frame( person_id = seq_len(sum(in_denom)), cohort = d$cohort[in_denom], - age_denom_min = pmax(age_init[in_denom], floor_age), + age_denom_min = age_init[in_denom], # each person's own entry age; the floor is reporting-only age_denom_max = age_denom_max, weight = weight[in_denom] ) diff --git a/config.yml b/config.yml index 77495a8..9653992 100644 --- a/config.yml +++ b/config.yml @@ -201,8 +201,8 @@ default: var: smoked_100_lifetime yes_code: 1 # Cessation exit: years since stopped smoking COMPLETELY (estimand specification, - # section 3). Derived from questions first asked in 2003; in 2001 it is not asked - # (NA(c)) and is handled by the imputation path for cycle-level absence. + # section 3). Derived from questions first asked in 2003; not available in 2001 or + # the 2022 PUMF (NA(c)); handled by the imputation path for cycle-level absence. years_since_quit_complete: pumf: var: time_quit_smoking_complete # midpoint-estimated; top-coded at 15 diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 9dadf73..1794427 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -36,7 +36,7 @@ The CCHS does not ask the age at which the 100th cigarette was smoked, so the ag **Consequence for the code.** The cessation model currently includes only ever-daily smokers and uses `time_quit_smoking_daily` (config key `years_since_quit`). Under this specification the model includes all established smokers and the exit variable is `time_quit_smoking_complete`. Task 1.3 makes this change; the config key and the worksheet roles change with it. -**The 2001 cycle (decided 2026-08-27).** `time_quit_smoking_complete` is derived from questions first asked in 2003. For 2001 the timing of complete cessation is treated as not asked in that cycle (NA(c)) and handled by the imputation procedure for cycle-level absence (Appendix D); the 2001 `time_quit_smoking` variable, which lacks the "stopped completely" question, is not used as a substitute. +**Cycles without complete-cessation timing (decided 2026-08-27).** `time_quit_smoking_complete` is derived from questions first asked in 2003, and cchsflow does not derive it for the 2022 PUMF (the 2023 Master file has it). For 2001, and for the 2022 PUMF, the timing of complete cessation is treated as not asked in that cycle (NA(c)) and handled by the imputation procedure for cycle-level absence (Appendix D); the 2001 `time_quit_smoking` variable, which lacks the "stopped completely" question, is not used as a substitute. ## 4. Event-time conventions diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index 7f2e19d..d1e5b1f 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -60,8 +60,8 @@ nrow(apc_data$cessation_men) nrow(apc_data$cessation_women) # Event rates -mean(apc_data$initiation_men$init) -mean(apc_data$cessation_men$init) +mean(apc_data$initiation_men$event) +mean(apc_data$cessation_men$event) ``` ## Outputs @@ -88,9 +88,9 @@ The spline basis columns are built in Stage 8 (`build_spline_basis()`), not stor **Retrospective history construction.** A current smoker at age 45 in survey year 2014 is recorded as a person-year at risk of cessation at every age from their initiation age to 45. This reconstruction assumes respondents accurately recall their age of initiation and cessation (supported by CCHS validation studies). -**Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weighting` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). +**Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weight` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). -**Cessation model: who is included and when time at risk begins.** The cessation model includes established smokers: people who have smoked 100 or more cigarettes in their lifetime (`smoked_100_lifetime`), whatever their current smoking pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person's time at risk begins at their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count as cessation: the person is a current smoker at the survey and their time at risk ends at the quit age without an event. A person who started and stopped at the same age contributes one year at risk with the event in it. People with a missing entry age or missing quit timing (including the whole 2001 cycle, where the stopped-completely questions were not asked) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. +**Cessation model: who is included and when time at risk begins.** The cessation model includes established smokers: people who have smoked 100 or more cigarettes in their lifetime (`smoked_100_lifetime`), whatever their current smoking pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person's time at risk begins at their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count as cessation: the person is a current smoker at the survey and their time at risk ends at the quit age without an event. A person who started and stopped at the same age contributes one year at risk with the event in it. People with a missing entry age or missing quit timing (including the whole 2001 cycle and the 2022 PUMF cycle, where the stopped-completely timing is not available) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. **Four separate data frames, not one.** Men and women are modelled separately (consistent with Manuel et al. 2020). Initiation and cessation are separate models. Keeping four data frames lets Stage 8 fit all four models in parallel using `{targets}` branching. diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index d4f0463..e3fd91e 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -117,7 +117,9 @@ test_that("build_cessation_data: includes established smokers of every ever-smok established <- sum(diag$n[diag$group == "established"]) smoked_100 <- data[[survey_var(cfg, "established_smoker")]] smk <- data[[survey_var(cfg, "smoking_status")]] - expect_equal(established, sum(!is.na(smoked_100) & smoked_100 == 1 & smk %in% 1:5 & data$cohort >= cfg$apc$cohort_min)) + yes <- survey_code(cfg, "established_smoker", "yes_code") + ever <- survey_code(cfg, "smoking_status", "ever_codes") + expect_equal(established, sum(!is.na(smoked_100) & smoked_100 == yes & smk %in% ever & data$cohort >= cfg$apc$cohort_min)) expect_true(all(result$event %in% c(0L, 1L))) }) @@ -159,11 +161,11 @@ test_that("build_cessation_data: recent quitter is censored at the quit age with test_that("build_cessation_data: starting and stopping at the same age is one trial with the event", { cfg <- cess_cfg() - s <- one_person(cfg, status = 5, age_first = 30, yrs_quit_complete = 20, age = 50, survey_year = 2010) + s <- one_person(cfg, status = 5, age_first = 40, yrs_quit_complete = 10, age = 50, survey_year = 2010) result <- suppressMessages(build_cessation_data(s, cfg)) expect_equal(nrow(result), 1L) expect_equal(result$event, 1L) - expect_equal(result$age, 30L) + expect_equal(result$age, 40L) diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "same_age_quits"]), 1L) }) @@ -179,7 +181,7 @@ test_that("build_cessation_data: missing quit timing (e.g. 2001, NA(c)) is exclu test_that("build_cessation_data: a quit before entry is excluded and counted", { cfg <- cess_cfg() - bad <- one_person(cfg, status = 4, age_first = 30, yrs_quit_complete = 30, age = 50, survey_year = 2010) + bad <- one_person(cfg, status = 4, age_first = 45, yrs_quit_complete = 10, age = 50, survey_year = 2010) # quit at 40, before entry at 45 result <- suppressMessages(build_cessation_data(bad, cfg)) expect_equal(nrow(result), 0L) diag <- attr(result, "cessation_diagnostics") @@ -323,3 +325,76 @@ test_that("fit_binomial_apc: fits and reports convergence when events exist", { expect_true(isTRUE(fit$converged)) expect_s3_class(fit, "glm") }) + +# ---- respondent-level invariants (external review of PR #7) ---- + +test_that("cessation: risk begins at the person's own entry age, even below the reporting floor", { + cfg <- cess_cfg() + early <- one_person(cfg, status = 1, age_first = 8, age = 40, survey_year = 2005) + result <- suppressMessages(build_cessation_data(early, cfg)) + expect_equal(min(result$age), 8L) + expect_true(survey_bound(cfg, "age_first_cigarette", "min") > 8) +}) + +test_that("cessation: a negative or out-of-bounds quit duration is excluded and counted, never post-survey time", { + cfg <- cess_cfg() + bad <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = -5, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(bad, cfg)) + expect_equal(nrow(result), 0L) + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "excluded_quit_timing_invalid"]), 1L) + big <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 99, age = 50, survey_year = 2010) + expect_equal(nrow(suppressMessages(build_cessation_data(big, cfg))), 0L) +}) + +test_that("cessation and initiation: no person-year after the survey age, none before entry", { + cfg <- cess_cfg() + data <- make_apc_test_data(cfg, n = 300, seed = 7) + age_survey <- data[[survey_var(cfg, "age")]] + cess <- suppressMessages(build_cessation_data(data, cfg)) + init <- suppressMessages(build_initiation_data(data, cfg)) + expect_true(all(cess$age <= max(age_survey))) + expect_true(all(init$age <= max(age_survey))) + expect_true(all(cess$age >= min(data[[survey_var(cfg, "age_first_cigarette")]], na.rm = TRUE))) + expect_true(all(cess$event %in% c(0L, 1L))) + expect_true(all(init$event %in% c(0L, 1L))) +}) + +test_that("initiation: missing status, missing 100-cigarette answer, or invalid entry are excluded, not Never", { + cfg <- cess_cfg() + no_crit <- one_person(cfg, status = 1, smoked_100 = NA, age_first = 16, age = 40, survey_year = 2005) + r1 <- suppressMessages(build_initiation_data(no_crit, cfg)) + expect_equal(nrow(r1), 0L) + d1 <- attr(r1, "initiation_diagnostics") + expect_equal(sum(d1$n[d1$group == "excluded_criterion_missing"]), 1L) + late <- one_person(cfg, status = 1, age_first = 45, age = 40, survey_year = 2005) + expect_equal(nrow(suppressMessages(build_initiation_data(late, cfg))), 0L) + no_age <- one_person(cfg, status = 1, age_first = NA, age = 40, survey_year = 2005) + r3 <- suppressMessages(build_initiation_data(no_age, cfg)) + expect_equal(nrow(r3), 0L) + d3 <- attr(r3, "initiation_diagnostics") + expect_equal(sum(d3$n[d3$group == "excluded_missing_entry"]), 1L) +}) + +test_that("initiation: never smokers are at risk from the floor to the survey; initiators have one event", { + cfg <- cess_cfg() + floor_age <- survey_bound(cfg, "age_first_cigarette", "min") + nev <- one_person(cfg, status = 6, smoked_100 = NA, age_first = NA, age = 30, survey_year = 2010) + r <- suppressMessages(build_initiation_data(nev, cfg)) + expect_equal(sum(r$event), 0L) + expect_equal(sort(r$age), floor_age:30) + st <- one_person(cfg, status = 1, age_first = 20, age = 30, survey_year = 2010) + r2 <- suppressMessages(build_initiation_data(st, cfg)) + expect_equal(sum(r2$event), 1L) + expect_equal(r2$age[r2$event == 1L], 20L) + expect_equal(sort(r2$age[r2$event == 0L]), floor_age:19) +}) + +test_that("initiation: an established smoker who started below the floor contributes no initiation rows", { + cfg <- cess_cfg() + early <- one_person(cfg, status = 1, age_first = 8, age = 40, survey_year = 2005) + r <- suppressMessages(build_initiation_data(early, cfg)) + expect_equal(nrow(r), 0L) + d <- attr(r, "initiation_diagnostics") + expect_equal(sum(d$n[d$group == "entered_before_floor"]), 1L) +}) diff --git a/worksheets/cshm-variables.csv b/worksheets/cshm-variables.csv index bf0be11..9dfe63b 100644 --- a/worksheets/cshm-variables.csv +++ b/worksheets/cshm-variables.csv @@ -14,7 +14,7 @@ "age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically. Also the age at which each established smoker's time at risk of cessation begins (task 1.3).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" "age_start_smoking","Age daily (unified)*","Age started smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG040_cont, SMK_040]","smoking","Health behaviour","Years","Raw SMKG040 absent from 2019-20 PUMF (DDI-confirmed): age_start_smoking unavailable for cchs2019_2020_p and cchs2022_p; Master covers 2001-2023. See cchsflow#185.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_040); PUMF: midpoint-estimated (SMKG040_cont). Primary daily initiation age","Age started smoking daily (unified cchsflow v3 variable). Used in the initiation APC model as an alternative or supplementary age measure.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" "time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023.","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Years since stopped daily smoking. Retained for the intensity model and the daily-smoking sensitivity analysis; no longer the cessation exit (see time_quit_smoking_complete).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, imputation-predictor","both" -"time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003: not available in 2001 (NA(c); imputed per Appendix D).","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, imputation-predictor","both" +"time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003. Not available in 2001 or the 2022 PUMF (NA(c); imputed per Appendix D); available in the 2023 Master file.","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" "SMK_09A_cont","Yrs quit daily (PUMF)","Years since stopped smoking daily - former daily (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_09A, cchs2003_p::SMKC_09A, cchs2005_p::SMKE_09A, cchs2015_2016_p::SMK_080, cchs2017_2018_p::SMK_080, cchs2019_2020_p::SMK_080, cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09A, cchs2005_m::SMKE_09A, cchs2015_2016_m::SMK_080, cchs2017_2018_m::SMK_080, cchs2019_2020_m::SMK_080, cchs2021_m::SMK_080, cchs2023_m::SPU_25, [SMK_09A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former daily smokers. Feeder for time_quit_smoking and time_quit_smoking_daily (cchsflow v3). Not available 2022 or PUMF 2023 (SPU_25 is Master-only).","PUMF-derived continuous years since quit. Superseded by time_quit_smoking once cchsflow v3 merges. Keep for pre-v3 fallback","Intermediate: PUMF/Master midpoint feeder for the unified cessation variables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_06A_cont","Yrs quit occ (PUMF)","Years since stopped smoking - former occasional smokers (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_06A, cchs2003_p::SMKC_06A, cchs2005_p::SMKE_06A, cchs2015_2016_p::SMK_060, cchs2017_2018_p::SMK_060, cchs2019_2020_p::SMK_060, cchs2001_m::SMKA_06A, cchs2003_m::SMKC_06A, cchs2005_m::SMKE_06A, cchs2015_2016_m::SMK_060, cchs2017_2018_m::SMK_060, cchs2019_2020_m::SMK_060, cchs2021_m::SMK_060, cchs2023_m::SPU_10, [SMK_06A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former occasional smokers. Feeder for time_quit_smoking (cchsflow v3 falls back to it when SMK_09A_cont is not applicable). Not available 2022 or PUMF 2023.","PUMF-derived years since quit for former occasional smokers. Not covered by time_quit_smoking (daily only)","Intermediate: occasional-smoker feeder for time_quit_smoking (all former smokers).","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_10A_cont","Yrs quit (reducer)","Years since quit completely (former daily who continued occasional, continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2003_p::SMKC_10A, cchs2005_p::SMKE_10A, cchs2015_2016_p::SMK_100, cchs2017_2018_p::SMK_100, cchs2019_2020_p::SMK_100, cchs2003_m::SMKC_10A, cchs2005_m::SMKE_10A, cchs2015_2016_m::SMK_100, cchs2017_2018_m::SMK_100, cchs2019_2020_m::SMK_100, cchs2021_m::SMK_100, cchs2023_m::SPU_35, [SMK_10A]","Smoking","Health behaviour","years","cchsflow v3 building block; not used directly by pipeline code.","Years since quit completely (former daily who continued occasional, continuous)","Intermediate: years since stopped smoking completely (midpoint, gradual quitters); feeder for time_quit_smoking_complete.","0.2.0","2026-08-27","Added under task 1.3: feeder closure for time_quit_smoking_complete (found by the CI pipeline run).","","","intermediate","both" From 624d938e983bfa77518fde82f491b01eba58eea8 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 16:46:18 -0400 Subject: [PATCH 26/29] Variable ranges come from the variable-details worksheet, not config PI direction: the variable-details worksheet is the reference for minimum and maximum values; config.yml refers to it instead of holding copies. New details_range() reads a variable's valid range for one database from the worksheet rules -- [lo, hi] copy rules, the set of category-to-value midpoints, and, for derived variables, the union of their feeders' ranges -- so ranges follow each cycle's grouping and top-codes. config.yml survey entries for continuous variables now say `range: variable_details`; the 28 literal min/max lines are removed. survey_range() resolves the pointer; the APC builders receive the variable-details sheet from the pipeline and validate entry ages and quit durations per respondent against the range for that respondent's cycle. The initiation floor (PUMF 13, Master 8; public issue #5) is an analysis decision, not a range, and moves to apc.initiation_floor_age. Finding made by the change: the PUMF groups quit duration as under 1, 1-2, 2-3, and 3 or more years from 2003 onward, so the largest midpoint is 5, not the 15 that config comments, worksheet notes, and the specification claimed; only the 2001 cycle reaches 15. All those claims are corrected, and the coarsening is flagged for task 1.10. Tests: details_range on synthetic rules; survey_range on the real worksheets (0.5-5 for 2013-14, 15 for 2001 daily); out-of-range quit durations excluded; synthetic test histories now use in-range values. --- CLAUDE.md | 2 +- R/apc-model.R | 56 ++++++++--- R/config-utils.R | 39 ++++++++ R/variable-details-sheet-utils.R | 54 +++++++++++ _targets.R | 2 +- config.yml | 58 ++++++------ docs/workflow/7-apc-data-preparation.qmd | 3 +- tests/testthat/helper-apc.R | 3 +- tests/testthat/setup.R | 11 ++- tests/testthat/test-apc-data.R | 115 +++++++++++++++-------- worksheets/cshm-variables.csv | 4 +- 11 files changed, 257 insertions(+), 90 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 66e04f8..5f99a4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,7 +120,7 @@ The `variableStart` worksheet column uses cchsflow notation: `cchs2001_p::SMKA_0 **APC model variables (internal):** `age`, `cohort`, `period`, `event`, `weight` -**Value codes are configuration, not code.** Status groupings (`survey.smoking_status..ever_codes/current_codes/former_codes/never_code`), sex codes (`survey.sex..men_code/women_code`), the established-smoker criterion (`survey.established_smoker..yes_code`), and analytic thresholds (`apc.cessation_durability_years`, bounds via `survey_bound()`) live in `config.yml` and are read with `survey_code()` / `survey_bound()`. Do not write literal codes or thresholds into R. Remaining exception, scheduled as plan task 2.6: literal variable names in `R/imputation.R`. +**Value codes are configuration, not code.** Status groupings (`survey.smoking_status..ever_codes/current_codes/former_codes/never_code`), sex codes (`survey.sex..men_code/women_code`), the established-smoker criterion (`survey.established_smoker..yes_code`), and analytic thresholds (`apc.cessation_durability_years`, `apc.initiation_floor_age`) live in `config.yml` and are read with `survey_code()` / `initiation_floor()`. **Variable ranges (min/max) are not in config at all:** `survey...range: variable_details` points at the variable-details worksheet, and `details_range()` / `survey_range()` derive the range from the recoding rules for each cycle. Example of why: the PUMF groups quit duration to "3 or more years" from 2003 (largest midpoint 5), where the old config comments claimed a top-code of 15. Do not write literal codes or thresholds into R. Remaining exception, scheduled as plan task 2.6: literal variable names in `R/imputation.R`. ### cchsflow dependency diff --git a/R/apc-model.R b/R/apc-model.R index b3377cb..6942010 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -28,19 +28,21 @@ #' #' @param analysis_data Output of impute_data() #' @param cfg Config object from config::get() +#' @param variable_details_sheet Combined variable-details worksheet; the +#' reference for variable ranges (survey_range()) #' @return Named list: initiation_men, initiation_women, cessation_men, #' cessation_women. Each element is a data frame with columns: #' age, cohort, period, event, weight. -prepare_apc_data <- function(analysis_data, cfg) { +prepare_apc_data <- function(analysis_data, cfg, variable_details_sheet) { data <- derive_survey_year(analysis_data, cfg) sex <- data[[survey_var(cfg, "sex")]] men <- !is.na(sex) & sex == survey_code(cfg, "sex", "men_code") women <- !is.na(sex) & sex == survey_code(cfg, "sex", "women_code") - init_men <- build_initiation_data(data[men, ], cfg) - init_women <- build_initiation_data(data[women, ], cfg) - cess_men <- build_cessation_data(data[men, ], cfg) - cess_women <- build_cessation_data(data[women, ], cfg) + init_men <- build_initiation_data(data[men, ], cfg, variable_details_sheet) + init_women <- build_initiation_data(data[women, ], cfg, variable_details_sheet) + cess_men <- build_cessation_data(data[men, ], cfg, variable_details_sheet) + cess_women <- build_cessation_data(data[women, ], cfg, variable_details_sheet) list( initiation_men = apply_survival_correction(init_men, cfg), @@ -108,7 +110,7 @@ derive_survey_year <- function(data, cfg) { #' @param cfg Config object #' @return Data frame with age, cohort, period, event, weight, plus the attribute #' `initiation_diagnostics` (per-cycle counts, unweighted and weighted) -build_initiation_data <- function(data, cfg) { +build_initiation_data <- function(data, cfg, variable_details_sheet) { status_col <- survey_var(cfg, "smoking_status") init_col <- survey_var(cfg, "age_first_cigarette") age_col <- survey_var(cfg, "age") @@ -118,8 +120,7 @@ build_initiation_data <- function(data, cfg) { smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") ever_codes <- survey_code(cfg, "smoking_status", "ever_codes") never_code <- survey_code(cfg, "smoking_status", "never_code") - floor_age <- survey_bound(cfg, "age_first_cigarette", "min") - init_max <- survey_bound(cfg, "age_first_cigarette", "max") + floor_age <- initiation_floor(cfg) cohort_min <- cfg$apc$cohort_min period_min <- cfg$apc$period_min period_max <- cfg$apc$period_max @@ -132,6 +133,7 @@ build_initiation_data <- function(data, cfg) { age_survey <- as.integer(round(data[[age_col]])) weight <- data[[weight_col]] cycle <- as.character(data[[cycle_col]]) + init_range <- per_respondent_range(cfg, "age_first_cigarette", cycle, variable_details_sheet) status_missing <- is.na(smk) never <- !status_missing & smk == never_code @@ -141,7 +143,7 @@ build_initiation_data <- function(data, cfg) { established <- ever_status & !is.na(smoked_100) & smoked_100 == smoked_100_yes missing_entry <- established & is.na(age_init) entry_invalid <- established & !is.na(age_init) & - (age_init > age_survey | age_init > init_max | age_init < 0) + (age_init > age_survey | outside_range(age_init, init_range)) entry_before_floor <- established & !is.na(age_init) & !entry_invalid & age_init < floor_age initiator <- established & !is.na(age_init) & !entry_invalid & age_init >= floor_age at_risk_to_survey <- never | experimental @@ -197,6 +199,33 @@ build_initiation_data <- function(data, cfg) { } +#' Per-respondent valid range for a survey variable, by each respondent's cycle +#' +#' @param cfg Config object +#' @param key Survey key (e.g. "age_first_cigarette") +#' @param cycle Character vector of cycle codes, one per respondent +#' @param variable_details_sheet Combined variable-details worksheet +#' @return List with numeric vectors `min` and `max` (NA where unbounded) +per_respondent_range <- function(cfg, key, cycle, variable_details_sheet) { + dbs <- cycle_database(cfg, cycle) + known <- unique(dbs[!is.na(dbs)]) + ranges <- lapply(known, function(db) survey_range(cfg, key, db, variable_details_sheet)) + names(ranges) <- known + idx <- match(dbs, known) + list( + min = vapply(idx, function(i) if (is.na(i)) NA_real_ else ranges[[i]][["min"]], numeric(1)), + max = vapply(idx, function(i) if (is.na(i)) NA_real_ else ranges[[i]][["max"]], numeric(1)) + ) +} + +#' TRUE where a value lies outside its per-respondent range (NA bounds ignored) +outside_range <- function(x, range) { + below <- !is.na(range$min) & x < range$min + above <- !is.na(range$max) & x > range$max + !is.na(x) & (below | above) +} + + #' Per-cycle counts (unweighted and weighted) for a list of logical groups #' #' @param groups Named list of logical vectors of equal length @@ -295,7 +324,7 @@ expand_denominator <- function(denom_source, period_range, min_age) { #' @param cfg Config object #' @return Data frame with age, cohort, period, event, weight, plus the attribute #' `cessation_diagnostics` (per-cycle counts, unweighted and weighted) -build_cessation_data <- function(data, cfg) { +build_cessation_data <- function(data, cfg, variable_details_sheet) { status_col <- survey_var(cfg, "smoking_status") smoked_100_col <- survey_var(cfg, "established_smoker") smoked_100_yes <- survey_code(cfg, "established_smoker", "yes_code") @@ -304,11 +333,9 @@ build_cessation_data <- function(data, cfg) { age_col <- survey_var(cfg, "age") weight_col <- survey_var(cfg, "weight") cycle_col <- survey_var(cfg, "cycle") - floor_age <- survey_bound(cfg, "age_first_cigarette", "min") # default for expand_denominator only + floor_age <- initiation_floor(cfg) # default for expand_denominator only durability <- cfg$apc$cessation_durability_years if (is.null(durability)) stop("cfg$apc$cessation_durability_years is not set.") - quit_min <- survey_bound(cfg, "years_since_quit_complete", "min") - quit_max <- survey_bound(cfg, "years_since_quit_complete", "max") cohort_min <- cfg$apc$cohort_min period_min <- cfg$apc$period_min period_max <- cfg$apc$period_max @@ -332,6 +359,7 @@ build_cessation_data <- function(data, cfg) { age_quit <- as.integer(round(age_survey - yrs_quit)) weight <- d[[weight_col]] cycle <- as.character(d[[cycle_col]]) # observed cycles only; avoids NA sums for empty levels + quit_range <- per_respondent_range(cfg, "years_since_quit_complete", cycle, variable_details_sheet) current <- smk %in% current_codes former <- smk %in% former_codes @@ -343,7 +371,7 @@ build_cessation_data <- function(data, cfg) { # Quit timing must be finite, within the configured bounds for the source # (PUMF top-code, Master ceiling), and place the quit no later than the survey. timing_invalid <- former & !is.na(yrs_quit) & - (!is.finite(yrs_quit) | yrs_quit < quit_min | yrs_quit > quit_max | + (!is.finite(yrs_quit) | outside_range(yrs_quit, quit_range) | is.na(age_quit) | age_quit > age_survey | age_quit < 0) quit_before_entry <- former & !is.na(age_quit) & !timing_invalid & !missing_entry & age_quit < age_init excluded <- missing_entry | entry_after_survey | timing_missing | timing_invalid | quit_before_entry diff --git a/R/config-utils.R b/R/config-utils.R index 624c723..5f0be4e 100644 --- a/R/config-utils.R +++ b/R/config-utils.R @@ -36,6 +36,45 @@ survey_code <- function(cfg, key, code) { val } +# Database name for a survey-cycle code (1-based position in cfg$cchs_cycles). +cycle_database <- function(cfg, cycle_code) { + code <- suppressWarnings(as.integer(as.character(cycle_code))) + out <- rep(NA_character_, length(code)) + ok <- !is.na(code) & code >= 1 & code <= length(cfg$cchs_cycles) + out[ok] <- unlist(cfg$cchs_cycles)[code[ok]] + out +} + +# Valid range of a survey variable for one database. config.yml declares +# `range: variable_details` and the range is read from the worksheet rules +# (details_range()); a literal min/max in config is honoured only as a legacy +# fallback. Returns c(min, max), NA where the rules give no bound. +survey_range <- function(cfg, key, database, variable_details_sheet) { + entry <- cfg$survey[[key]] + if (is.null(entry)) stop("survey_range: unknown key '", key, "'") + src <- cfg$data_source %||% "pumf" + src_entry <- entry[[src]] + if (is.null(src_entry) || !is.list(src_entry)) stop("survey_range: no '", src, "' entry for key '", key, "'") + if (identical(src_entry$range, "variable_details")) { + return(details_range(src_entry$var, database, variable_details_sheet)) + } + if (!is.null(src_entry$min) || !is.null(src_entry$max)) { + return(c(min = src_entry$min %||% NA_real_, max = src_entry$max %||% NA_real_)) + } + c(min = NA_real_, max = NA_real_) +} + +# The initiation floor is an analysis decision (public issue #5), not a variable +# range, so it lives under apc: in config, per data source. +initiation_floor <- function(cfg) { + src <- cfg$data_source %||% "pumf" + val <- cfg$apc$initiation_floor_age[[src]] + if (is.null(val)) stop("cfg$apc$initiation_floor_age has no entry for source '", src, "'") + val +} + +# Literal bounds in config are legacy; variable ranges come from the worksheet +# through survey_range(). survey_bound <- function(cfg, key, bound) { entry <- cfg$survey[[key]] if (is.null(entry)) stop("survey_bound: unknown key '", key, "'") diff --git a/R/variable-details-sheet-utils.R b/R/variable-details-sheet-utils.R index 115c215..6b5c334 100644 --- a/R/variable-details-sheet-utils.R +++ b/R/variable-details-sheet-utils.R @@ -39,3 +39,57 @@ get_variable_type <- function(variable, variable_details_sheet) { get_variable_rows <- function(variable, variable_details_sheet) { variable_details_sheet[variable_details_sheet$variable == variable, ] } + + +#' Valid range of a variable for one database, from the variable-details rules +#' +#' The variable-details worksheet is the reference for minimum and maximum +#' values; config.yml points at it (`range: variable_details`) rather than +#' holding copies. The range is read from the recoding rules for `database`: +#' a `copy` rule with `recStart` of the form `[lo, hi]` contributes lo and hi; +#' a category-to-value rule contributes its numeric `recEnd` (the midpoint); +#' a derived variable (`DerivedVar::[...]` / `Func::`) takes the union of its +#' feeders' ranges. `NA::` and `else` rules are ignored. Ranges differ by +#' database because top-codes and category boundaries differ by cycle. +#' +#' @param variable Variable name in the details sheet +#' @param database Database name, e.g. "cchs2013_2014_p" +#' @param variable_details_sheet Combined variable-details data frame +#' @return Named numeric vector c(min, max); NA when the rules give no bound +details_range <- function(variable, database, variable_details_sheet, .seen = character()) { + none <- c(min = NA_real_, max = NA_real_) + rows <- variable_details_sheet[variable_details_sheet$variable == variable, , drop = FALSE] + if (nrow(rows) == 0) { + return(none) + } + in_db <- vapply(strsplit(as.character(rows$databaseStart), ","), function(x) database %in% trimws(x), logical(1)) + rows <- rows[in_db, , drop = FALSE] + if (nrow(rows) == 0) { + return(none) + } + vals <- numeric(0) + for (i in seq_len(nrow(rows))) { + rec_end <- trimws(as.character(rows$recEnd[i])) + rec_start <- trimws(as.character(rows$recStart[i])) + var_start <- as.character(rows$variableStart[i]) + if (grepl("^Func::", rec_end) || grepl("DerivedVar::", var_start)) { + inner <- sub(".*DerivedVar::\\[([^]]*)\\].*", "\\1", var_start) + feeders <- setdiff(trimws(strsplit(inner, ",")[[1]]), c(.seen, variable)) + for (f in feeders) { + r <- details_range(f, database, variable_details_sheet, c(.seen, variable)) + vals <- c(vals, r) + } + } else if (rec_end == "copy") { + m <- regmatches(rec_start, regexec("^\\[\\s*(-?[0-9.]+)\\s*,\\s*(-?[0-9.]+)\\s*\\]$", rec_start))[[1]] + if (length(m) == 3) vals <- c(vals, as.numeric(m[2:3])) + } else { + num <- suppressWarnings(as.numeric(rec_end)) + if (!is.na(num)) vals <- c(vals, num) + } + } + vals <- vals[is.finite(vals)] + if (length(vals) == 0) { + return(none) + } + c(min = min(vals), max = max(vals)) +} diff --git a/_targets.R b/_targets.R index 173d84c..8afa424 100644 --- a/_targets.R +++ b/_targets.R @@ -86,7 +86,7 @@ list( # Fitted on imputation 1 (protocol Appendix D documents the upgrade path # to per-imputation fits pooled by Rubin's rules). tar_target(apc_data, - prepare_apc_data(analysis_data$datasets[[1]], cfg) + prepare_apc_data(analysis_data$datasets[[1]], cfg, variable_details_sheet) ), # Stage 8: Fit APC models — four independent targets for parallel execution diff --git a/config.yml b/config.yml index 9653992..65e3077 100644 --- a/config.yml +++ b/config.yml @@ -105,6 +105,9 @@ default: # Core demographics # Value codes live here, not in R code: survey_code(cfg, "sex", "men_code") -> 1. + # Variable RANGES do not live here: `range: variable_details` means the valid + # minimum and maximum come from the variable-details worksheet rules for each + # cycle (details_range()); analytic thresholds are under apc: (e.g. initiation_floor_age). sex: pumf: var: DHH_SEX # Sex (1=male, 2=female) @@ -117,12 +120,10 @@ default: age: pumf: var: DHHGAGE_cont # Continuous age (years); midpoint-estimated from grouped categories - min: 18 # Study inclusion floor (12–17 excluded in cleaning stage) - max: 85 # PUMF top-coded age group (80+, midpoint 85, 2001-2018; 2019+ tops at 75) + range: variable_details master: var: DHH_AGE # True continuous age - min: 18 - max: 110 # Implausible age ceiling + range: variable_details province: pumf: var: GEOGPRV # Province/territory of residence @@ -173,23 +174,18 @@ default: age_first_cigarette: pumf: var: age_first_cigarette # Age smoked first whole cigarette (midpoint-estimated) - min: 13 # Initiation floor (PI decision 2026-08-27, public issue #5): the PUMF - # 5-11 category (midpoint 8) is too coarse to date initiation events - # below 13; cessation follow-up still starts at the reported midpoint. - max: 100 + range: variable_details + # 13 excludes it. Lowering to 8 is an open study decision. master: var: age_first_cigarette # Exact age from SMK_01C - min: 8 # Genuine early initiations captured in Master - max: 100 + range: variable_details age_start_daily: pumf: var: age_start_smoking # Age started daily smoking (midpoint-estimated) - min: 13 # Study floor — SMKG040_cont also has a 5-11 (midpoint 8) category - max: 100 + range: variable_details master: var: age_start_smoking # Exact age from SMK_040 - min: 8 - max: 100 + range: variable_details # Established-smoker criterion (estimand specification, section 2): at least 100 # cigarettes in lifetime. Experimental smokers (fewer than 100) are Never. # `yes_code` is the value meaning "smoked 100 or more" (CCHS SMK_01A: 1 = yes, 2 = no). @@ -206,42 +202,34 @@ default: years_since_quit_complete: pumf: var: time_quit_smoking_complete # midpoint-estimated; top-coded at 15 - min: 0 - max: 15 # PUMF top-code + range: variable_details master: var: time_quit_smoking_complete # exact years - min: 0 - max: 80 + range: variable_details # Years since stopped DAILY smoking. No longer the cessation exit (that is # complete cessation, above); retained for the intensity model and for the # daily-smoking sensitivity analysis. years_since_quit: pumf: var: time_quit_smoking_daily # Years since stopped daily (midpoint-estimated, top-coded at 15) - min: 0 - max: 15 # PUMF top-code + range: variable_details master: var: time_quit_smoking_daily # Exact years since quit daily (SMK_09C) - min: 0 - max: 80 + range: variable_details cigs_per_day: pumf: var: cigs_per_day # Unified: daily + former daily smokers - min: 1 - max: 99 + range: variable_details master: var: cigs_per_day - min: 1 - max: 99 + range: variable_details pack_years: pumf: var: pack_years_der # Cumulative pack-years (derived) - min: 0 - max: 165 + range: variable_details master: var: pack_years_der - min: 0 - max: 165 + range: variable_details # Validation strict_validation: false # TRUE = stop on critical coverage gaps; FALSE = warn only @@ -280,6 +268,14 @@ default: period_max: 2022 # statscan profile overrides to 2023 projection_max: 2050 # rate tables and smoking histories projected to this year cohort_min: 1920 + # Initiation floor age: an analysis decision, not a variable range (public + # issue #5, PI decision 2026-08-27). PUMF: the lowest age category (5-11, + # midpoint 8) is too coarse to date entry, so the floor is 13. Master: exact + # ages, floor 8. Established smokers who started below the floor contribute + # no initiation rows but remain in the cessation model. + initiation_floor_age: + pumf: 13 + master: 8 # A quit counts as cessation only if it has lasted this many years at the # survey; more recent quitters are current at survey and censored at the # quit age (estimand specification, section 4; Holford et al. 2014). @@ -362,7 +358,7 @@ default: # Survey year lookup: integer year per SurveyCycle code (1–11) # 2-year cycles use midpoint year (e.g. 2008 for 2007-08) cycle_survey_years: - "1": 2001 # CCHS 1.1 collected Sept 2000 to Nov 2001; assigned 2001 (PI decision 2026-08-27, issue #5) + "1": 2001 "2": 2003 "3": 2005 "4": 2008 # 2007-08 diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index d1e5b1f..93a8a86 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -29,7 +29,8 @@ Configuration used: | `survey$established_smoker` | `config.yml` | 100-cigarette criterion; `yes_code` is the value that includes a person in the smoking models | | `survey$smoking_status` `*_codes` | `config.yml` | Which `SMKDSTY_original` codes count as ever, current, former, never | | `survey$sex` `*_code` | `config.yml` | Sex codes used to stratify the models | -| `survey_bound(cfg, "age_first_cigarette", "min")` | `config.yml` | APC floor for initiation age (PUMF: 13, Master: 8) | +| `apc$initiation_floor_age` | `config.yml` | Initiation floor age, an analysis decision (PUMF 13, Master 8; public issue #5) | +| `survey$*$range: variable_details` | `config.yml` | Variable ranges are read from the variable-details worksheet rules per cycle (`details_range()`), not stored in config | | `apc$cessation_durability_years` | `config.yml` | A quit counts as cessation only after this many years (2) | | `apc$age_knots` | `config.yml` | `[10, 15, 20, 50, 60]` | | `apc$period_knots` | `config.yml` | `[1940, 1950, 1960, 1970, 1980]` | diff --git a/tests/testthat/helper-apc.R b/tests/testthat/helper-apc.R index b719a77..59feb26 100644 --- a/tests/testthat/helper-apc.R +++ b/tests/testthat/helper-apc.R @@ -22,7 +22,8 @@ make_apc_test_data <- function(cfg, n = 100, seed = 42) { # Most ever-smokers pass; some are experimental (2); never smokers are NA(a). smoked_100 <- ifelse(smkdsty == 6, NA_real_, ifelse(runif(n) < 0.85, 1, 2)) # Years since stopped smoking completely: former smokers (4, 5) only. - yrs_quit_complete <- ifelse(smkdsty %in% c(4, 5), round(runif(n, 0, 20)), NA_real_) + # PUMF 2003+ groups quit duration to a largest midpoint of 5 (worksheet rules) + yrs_quit_complete <- ifelse(smkdsty %in% c(4, 5), sample(c(0.5, 1.5, 2.5, 5), n, replace = TRUE), NA_real_) # keep quit age at or after the entry age so the base data are internally consistent yrs_quit_complete <- pmin(yrs_quit_complete, ages - age_first) diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R index fad3ec3..b637f4c 100644 --- a/tests/testthat/setup.R +++ b/tests/testthat/setup.R @@ -9,8 +9,15 @@ project_root <- normalizePath(file.path(dirname(dirname(getwd())))) r_files <- list.files( file.path(project_root, "R"), - pattern = "\\.R$", + pattern = "\\.R$", full.names = TRUE, - recursive = FALSE + recursive = FALSE ) invisible(lapply(r_files, source)) + +# The variable-details worksheet is the reference for variable ranges; the APC +# builders take it as an argument. Load it once for all tests. +TEST_DETAILS <- as.data.frame(dplyr::bind_rows( + read.csv(file.path(project_root, "worksheets", "cchsflow-variable-details.csv")), + read.csv(file.path(project_root, "worksheets", "cshm-variable-details.csv")) +)) diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index e3fd91e..6f334ab 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -56,17 +56,17 @@ test_that("build_initiation_data: no numerator rows with age < initiation floor" data <- make_apc_test_data(cfg) sex_col <- survey_var(cfg, "sex") - result <- build_initiation_data(data[data[[sex_col]] == 1, ], cfg) + result <- build_initiation_data(data[data[[sex_col]] == 1, ], cfg, TEST_DETAILS) init_rows <- result[result$event == 1, ] - expect_true(all(init_rows$age >= survey_bound(cfg, "age_first_cigarette", "min"))) + expect_true(all(init_rows$age >= initiation_floor(cfg))) }) test_that("build_initiation_data: no rows with cohort < cohort_min", { cfg <- config::get() data <- make_apc_test_data(cfg) - result <- build_initiation_data(data, cfg) + result <- build_initiation_data(data, cfg, TEST_DETAILS) expect_true(all(result$cohort >= cfg$apc$cohort_min)) }) @@ -75,7 +75,7 @@ test_that("build_initiation_data: denominator period within [period_min, period_ data <- make_apc_test_data(cfg) sex_col <- survey_var(cfg, "sex") - result <- build_initiation_data(data[data[[sex_col]] == 1, ], cfg) + result <- build_initiation_data(data[data[[sex_col]] == 1, ], cfg, TEST_DETAILS) denom <- result[result$event == 0, ] expect_true(all(denom$period >= cfg$apc$period_min)) @@ -111,7 +111,7 @@ one_person <- function(cfg, status, smoked_100 = 1, age_first = 16, yrs_quit_com test_that("build_cessation_data: includes established smokers of every ever-smoker status", { cfg <- cess_cfg() data <- make_apc_test_data(cfg) - result <- suppressMessages(build_cessation_data(data, cfg)) + result <- suppressMessages(build_cessation_data(data, cfg, TEST_DETAILS)) diag <- attr(result, "cessation_diagnostics") expect_true(is.data.frame(diag)) established <- sum(diag$n[diag$group == "established"]) @@ -126,14 +126,14 @@ test_that("build_cessation_data: includes established smokers of every ever-smok test_that("build_cessation_data: experimental smokers (under 100 cigarettes) are not included", { cfg <- cess_cfg() exp_smoker <- one_person(cfg, status = 4, smoked_100 = 2, age_first = 15, yrs_quit_complete = 10) - result <- suppressMessages(build_cessation_data(exp_smoker, cfg)) + result <- suppressMessages(build_cessation_data(exp_smoker, cfg, TEST_DETAILS)) expect_equal(nrow(result), 0) }) test_that("build_cessation_data: no person-year precedes the person's own entry age", { cfg <- cess_cfg() cur <- one_person(cfg, status = 1, age_first = 22, age = 40, survey_year = 2005) - result <- suppressMessages(build_cessation_data(cur, cfg)) + result <- suppressMessages(build_cessation_data(cur, cfg, TEST_DETAILS)) expect_true(all(result$age >= 22)) # at risk from entry through the survey year, inclusive expect_equal(sort(result$age), 22:40) @@ -142,17 +142,17 @@ test_that("build_cessation_data: no person-year precedes the person's own entry test_that("build_cessation_data: durable quitter has one event at the quit age and risk rows before it", { cfg <- cess_cfg() - q <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 10, age = 50, survey_year = 2010) - result <- suppressMessages(build_cessation_data(q, cfg)) + q <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 4, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(q, cfg, TEST_DETAILS)) expect_equal(sum(result$event), 1L) - expect_equal(result$age[result$event == 1L], 40L) - expect_equal(sort(result$age[result$event == 0L]), 18:39) + expect_equal(result$age[result$event == 1L], 46L) + expect_equal(sort(result$age[result$event == 0L]), 18:45) }) test_that("build_cessation_data: recent quitter is censored at the quit age with no event", { cfg <- cess_cfg() r <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 1, age = 50, survey_year = 2010) - result <- suppressMessages(build_cessation_data(r, cfg)) + result <- suppressMessages(build_cessation_data(r, cfg, TEST_DETAILS)) expect_equal(sum(result$event), 0L) expect_equal(max(result$age), 48L) # quit at 49; the quit year is not observed diag <- attr(result, "cessation_diagnostics") @@ -161,11 +161,11 @@ test_that("build_cessation_data: recent quitter is censored at the quit age with test_that("build_cessation_data: starting and stopping at the same age is one trial with the event", { cfg <- cess_cfg() - s <- one_person(cfg, status = 5, age_first = 40, yrs_quit_complete = 10, age = 50, survey_year = 2010) - result <- suppressMessages(build_cessation_data(s, cfg)) + s <- one_person(cfg, status = 5, age_first = 46, yrs_quit_complete = 4, age = 50, survey_year = 2010) + result <- suppressMessages(build_cessation_data(s, cfg, TEST_DETAILS)) expect_equal(nrow(result), 1L) expect_equal(result$event, 1L) - expect_equal(result$age, 40L) + expect_equal(result$age, 46L) diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "same_age_quits"]), 1L) }) @@ -173,7 +173,7 @@ test_that("build_cessation_data: starting and stopping at the same age is one tr test_that("build_cessation_data: missing quit timing (e.g. 2001, NA(c)) is excluded and counted, not reclassified", { cfg <- cess_cfg() m <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = NA, age = 50, survey_year = 2001, cycle = "1") - result <- suppressMessages(build_cessation_data(m, cfg)) + result <- suppressMessages(build_cessation_data(m, cfg, TEST_DETAILS)) expect_equal(nrow(result), 0L) diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "excluded_timing_missing"]), 1L) @@ -181,8 +181,8 @@ test_that("build_cessation_data: missing quit timing (e.g. 2001, NA(c)) is exclu test_that("build_cessation_data: a quit before entry is excluded and counted", { cfg <- cess_cfg() - bad <- one_person(cfg, status = 4, age_first = 45, yrs_quit_complete = 10, age = 50, survey_year = 2010) # quit at 40, before entry at 45 - result <- suppressMessages(build_cessation_data(bad, cfg)) + bad <- one_person(cfg, status = 4, age_first = 48, yrs_quit_complete = 4, age = 50, survey_year = 2010) # quit at 46, before entry at 48 + result <- suppressMessages(build_cessation_data(bad, cfg, TEST_DETAILS)) expect_equal(nrow(result), 0L) diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "excluded_quit_before_entry"]), 1L) @@ -191,7 +191,7 @@ test_that("build_cessation_data: a quit before entry is excluded and counted", { test_that("build_initiation_data: experimental smokers contribute no initiation event", { cfg <- cess_cfg() exp_smoker <- one_person(cfg, status = 3, smoked_100 = 2, age_first = 15, age = 40, survey_year = 2005) - result <- suppressMessages(build_initiation_data(exp_smoker, cfg)) + result <- suppressMessages(build_initiation_data(exp_smoker, cfg, TEST_DETAILS)) expect_equal(sum(result$event), 0L) expect_true(nrow(result) > 0) # at risk, like a never smoker }) @@ -200,8 +200,8 @@ test_that("no missing weight in any output element", { cfg <- config::get() data <- make_apc_test_data(cfg) - result_init <- build_initiation_data(data, cfg) - result_cess <- build_cessation_data(data, cfg) + result_init <- build_initiation_data(data, cfg, TEST_DETAILS) + result_cess <- build_cessation_data(data, cfg, TEST_DETAILS) expect_false(anyNA(result_init$weight)) expect_false(anyNA(result_cess$weight)) @@ -273,7 +273,7 @@ test_that("assert_correction_applied: a correction may change only the weights", test_that("fit_apc_model carries the mortality-correction label and estimand note", { cfg <- config::get() cfg$apc$mortality_method <- "none" - apc_data <- prepare_apc_data(make_apc_test_data(cfg), cfg) + apc_data <- prepare_apc_data(make_apc_test_data(cfg), cfg, TEST_DETAILS) ds <- apc_data$initiation_men expect_identical(attr(ds, "mortality_correction"), "none") fit <- fit_apc_model(ds, "initiation", 1, cfg) # sex is coded 1 = men @@ -298,7 +298,7 @@ test_that("value codes are read from config, not hard-coded", { cfg2$survey$smoking_status$pumf$former_codes <- c(4) cfg2$survey$smoking_status$pumf$current_codes <- c(1, 2, 3, 5) q <- one_person(cfg2, status = 5, age_first = 20, yrs_quit_complete = 10, age = 50, survey_year = 2010) - result <- suppressMessages(build_cessation_data(q, cfg2)) + result <- suppressMessages(build_cessation_data(q, cfg2, TEST_DETAILS)) # status 5 is now "current": at risk to survey, no event expect_equal(sum(result$event), 0L) expect_equal(max(result$age), 50L) @@ -331,28 +331,28 @@ test_that("fit_binomial_apc: fits and reports convergence when events exist", { test_that("cessation: risk begins at the person's own entry age, even below the reporting floor", { cfg <- cess_cfg() early <- one_person(cfg, status = 1, age_first = 8, age = 40, survey_year = 2005) - result <- suppressMessages(build_cessation_data(early, cfg)) + result <- suppressMessages(build_cessation_data(early, cfg, TEST_DETAILS)) expect_equal(min(result$age), 8L) - expect_true(survey_bound(cfg, "age_first_cigarette", "min") > 8) + expect_true(initiation_floor(cfg) > 8) }) test_that("cessation: a negative or out-of-bounds quit duration is excluded and counted, never post-survey time", { cfg <- cess_cfg() bad <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = -5, age = 50, survey_year = 2010) - result <- suppressMessages(build_cessation_data(bad, cfg)) + result <- suppressMessages(build_cessation_data(bad, cfg, TEST_DETAILS)) expect_equal(nrow(result), 0L) diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "excluded_quit_timing_invalid"]), 1L) - big <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 99, age = 50, survey_year = 2010) - expect_equal(nrow(suppressMessages(build_cessation_data(big, cfg))), 0L) + big <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 99, age = 50, survey_year = 2010) # far beyond any cycle's range + expect_equal(nrow(suppressMessages(build_cessation_data(big, cfg, TEST_DETAILS))), 0L) }) test_that("cessation and initiation: no person-year after the survey age, none before entry", { cfg <- cess_cfg() data <- make_apc_test_data(cfg, n = 300, seed = 7) age_survey <- data[[survey_var(cfg, "age")]] - cess <- suppressMessages(build_cessation_data(data, cfg)) - init <- suppressMessages(build_initiation_data(data, cfg)) + cess <- suppressMessages(build_cessation_data(data, cfg, TEST_DETAILS)) + init <- suppressMessages(build_initiation_data(data, cfg, TEST_DETAILS)) expect_true(all(cess$age <= max(age_survey))) expect_true(all(init$age <= max(age_survey))) expect_true(all(cess$age >= min(data[[survey_var(cfg, "age_first_cigarette")]], na.rm = TRUE))) @@ -363,14 +363,14 @@ test_that("cessation and initiation: no person-year after the survey age, none b test_that("initiation: missing status, missing 100-cigarette answer, or invalid entry are excluded, not Never", { cfg <- cess_cfg() no_crit <- one_person(cfg, status = 1, smoked_100 = NA, age_first = 16, age = 40, survey_year = 2005) - r1 <- suppressMessages(build_initiation_data(no_crit, cfg)) + r1 <- suppressMessages(build_initiation_data(no_crit, cfg, TEST_DETAILS)) expect_equal(nrow(r1), 0L) d1 <- attr(r1, "initiation_diagnostics") expect_equal(sum(d1$n[d1$group == "excluded_criterion_missing"]), 1L) late <- one_person(cfg, status = 1, age_first = 45, age = 40, survey_year = 2005) - expect_equal(nrow(suppressMessages(build_initiation_data(late, cfg))), 0L) + expect_equal(nrow(suppressMessages(build_initiation_data(late, cfg, TEST_DETAILS))), 0L) no_age <- one_person(cfg, status = 1, age_first = NA, age = 40, survey_year = 2005) - r3 <- suppressMessages(build_initiation_data(no_age, cfg)) + r3 <- suppressMessages(build_initiation_data(no_age, cfg, TEST_DETAILS)) expect_equal(nrow(r3), 0L) d3 <- attr(r3, "initiation_diagnostics") expect_equal(sum(d3$n[d3$group == "excluded_missing_entry"]), 1L) @@ -378,13 +378,13 @@ test_that("initiation: missing status, missing 100-cigarette answer, or invalid test_that("initiation: never smokers are at risk from the floor to the survey; initiators have one event", { cfg <- cess_cfg() - floor_age <- survey_bound(cfg, "age_first_cigarette", "min") + floor_age <- initiation_floor(cfg) nev <- one_person(cfg, status = 6, smoked_100 = NA, age_first = NA, age = 30, survey_year = 2010) - r <- suppressMessages(build_initiation_data(nev, cfg)) + r <- suppressMessages(build_initiation_data(nev, cfg, TEST_DETAILS)) expect_equal(sum(r$event), 0L) expect_equal(sort(r$age), floor_age:30) st <- one_person(cfg, status = 1, age_first = 20, age = 30, survey_year = 2010) - r2 <- suppressMessages(build_initiation_data(st, cfg)) + r2 <- suppressMessages(build_initiation_data(st, cfg, TEST_DETAILS)) expect_equal(sum(r2$event), 1L) expect_equal(r2$age[r2$event == 1L], 20L) expect_equal(sort(r2$age[r2$event == 0L]), floor_age:19) @@ -393,8 +393,49 @@ test_that("initiation: never smokers are at risk from the floor to the survey; i test_that("initiation: an established smoker who started below the floor contributes no initiation rows", { cfg <- cess_cfg() early <- one_person(cfg, status = 1, age_first = 8, age = 40, survey_year = 2005) - r <- suppressMessages(build_initiation_data(early, cfg)) + r <- suppressMessages(build_initiation_data(early, cfg, TEST_DETAILS)) expect_equal(nrow(r), 0L) d <- attr(r, "initiation_diagnostics") expect_equal(sum(d$n[d$group == "entered_before_floor"]), 1L) }) + +# ---- ranges come from the variable-details worksheet, not from config ---- + +test_that("details_range reads copy-rule ranges, midpoint sets, and derived unions per database", { + det <- data.frame( + variable = c("exact", "exact", "grouped", "grouped", "grouped", "derived", "derived"), + databaseStart = c("cchs2001_m", "cchs2001_m", "cchs2001_p, cchs2003_p", "cchs2001_p, cchs2003_p", "cchs2001_p, cchs2003_p", "cchs2001_p", "cchs2001_p"), + variableStart = c("cchs2001_m::X", "cchs2001_m::X", "[G]", "[G]", "[G]", "DerivedVar::[grouped, exact]", "DerivedVar::[grouped, exact]"), + recEnd = c("copy", "NA::b", "8", "13", "NA::a", "Func::f", "NA::a"), + recStart = c("[8,99]", "else", "1", "2", "6", "N/A", "N/A"), + stringsAsFactors = FALSE + ) + expect_equal(details_range("exact", "cchs2001_m", det), c(min = 8, max = 99)) + expect_equal(details_range("grouped", "cchs2003_p", det), c(min = 8, max = 13)) + expect_equal(details_range("grouped", "cchs2001_m", det), c(min = NA_real_, max = NA_real_)) + expect_equal(details_range("derived", "cchs2001_p", det), c(min = 8, max = 13)) # 'exact' has no 2001_p rows +}) + +test_that("survey_range follows the config pointer to the worksheet; the real sheets give the expected bounds", { + cfg <- cess_cfg() + r <- survey_range(cfg, "years_since_quit_complete", "cchs2013_2014_p", TEST_DETAILS) + expect_equal(r[["min"]], 0.5) + # From 2003 the PUMF groups quit duration as <1, 1-2, 2-3, 3+ years: the largest + # midpoint is 5, not the 15 the old config comments claimed. Only 2001 reaches 15. + expect_equal(r[["max"]], 5) + expect_equal(survey_range(cfg, "years_since_quit", "cchs2001_p", TEST_DETAILS)[["max"]], 15) + a <- survey_range(cfg, "age_first_cigarette", "cchs2001_p", TEST_DETAILS) + expect_equal(a[["min"]], 8) + expect_true(a[["max"]] >= 45) + expect_null(cfg$survey$age_first_cigarette$pumf$max) # no literal bounds remain in config + expect_equal(initiation_floor(cfg), 13) +}) + +test_that("cessation: a quit duration above the worksheet top-code for the cycle is excluded", { + cfg <- cess_cfg() + over <- one_person(cfg, status = 4, age_first = 18, yrs_quit_complete = 6, age = 50, survey_year = 2010, cycle = "7") # above the 2013-14 range (0.5-5) + result <- suppressMessages(build_cessation_data(over, cfg, TEST_DETAILS)) + expect_equal(nrow(result), 0L) + diag <- attr(result, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "excluded_quit_timing_invalid"]), 1L) +}) diff --git a/worksheets/cshm-variables.csv b/worksheets/cshm-variables.csv index 9dfe63b..04da082 100644 --- a/worksheets/cshm-variables.csv +++ b/worksheets/cshm-variables.csv @@ -13,8 +13,8 @@ "SMKDSTY_original","Smoking status (6-cat)","Type of smoker derived - 6-category (cchsflow v3, original StatCan scheme)","Categorical","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m","cchs2001_p::SMKADSTY, cchs2003_p::SMKCDSTY, cchs2005_p::SMKEDSTY, cchs2007_2008_p::SMKDSTY, cchs2009_2010_p::SMKDSTY, cchs2010_p::SMKDSTY, cchs2011_2012_p::SMKDSTY, cchs2012_p::SMKDSTY, cchs2013_2014_p::SMKDSTY, cchs2001_m::SMKADSTY, cchs2003_m::SMKCDSTY, cchs2005_m::SMKEDSTY, cchs2007_2008_m::SMKDSTY, cchs2009_2010_m::SMKDSTY, cchs2009_m::SMKDSTY, cchs2010_m::SMKDSTY, cchs2011_2012_m::SMKDSTY, cchs2012_m::SMKDSTY, cchs2013_2014_m::SMKDSTY, cchs2014_m::SMKDSTY, DerivedVar::[SMK_202, SMK_05D, SMK_01A]","smoking","Health behaviour","N/A","NA","cchsflow v3 harmonized 6-cat smoking status: 1=daily, 2=occ(fmr daily), 3=always occasional, 4=former daily, 5=former occasional, 6=never. Consistent categories across all cycles.","Primary smoking classification for APC numerator construction and Table 1. cchsflow v3: 2001-2014 pass-through from SMKDSTY; 2015-2021 derived from SMK_202, SMK_05D, SMK_01A. Not supported PUMF 2022/2023.","0.1.0","2026-06-11","Renamed from SMKDSTY_A per CEP-002 year-based naming","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" "age_first_cigarette","Age 1st cig (unified)*","Age smoked first whole cigarette - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG01C_cont, SMK_01C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: ever smoked 100+ cigarettes. Priority: SMK_01C (Master exact) > SMKG01C_cont (PUMF midpoint). PUMF 2001-2021; Master 2001-2023.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_01C); PUMF: midpoint-estimated (SMKG01C_cont). Primary initiation age input","Age at first whole cigarette (unified cchsflow v3 variable). Primary input for the initiation APC numerator. Routes to exact values (Master) or midpoint estimates (PUMF) automatically. Also the age at which each established smoker's time at risk of cessation begins (task 1.3).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" "age_start_smoking","Age daily (unified)*","Age started smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2022_m, cchs2023_m","DerivedVar::[SMKG040_cont, SMK_040]","smoking","Health behaviour","Years","Raw SMKG040 absent from 2019-20 PUMF (DDI-confirmed): age_start_smoking unavailable for cchs2019_2020_p and cchs2022_p; Master covers 2001-2023. See cchsflow#185.","Unified variable (cchsflow v3 PR #163). Master: exact age (SMK_040); PUMF: midpoint-estimated (SMKG040_cont). Primary daily initiation age","Age started smoking daily (unified cchsflow v3 variable). Used in the initiation APC model as an alternative or supplementary age measure.","0.1.0","2026-06-11","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, apc-numerator, imputation-predictor","both" -"time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023.","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Years since stopped daily smoking. Retained for the intensity model and the daily-smoking sensitivity analysis; no longer the cessation exit (see time_quit_smoking_complete).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, imputation-predictor","both" -"time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003. Not available in 2001 or the 2022 PUMF (NA(c); imputed per Appendix D); available in the 2023 Master file.","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated, top-coded at 15; Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" +"time_quit_smoking_daily","Yrs quit daily (unified)*","Years since stopped smoking daily - unified (cchsflow v3)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former daily smokers. DerivedVar::[SMKDSTY_cat5, SMK_09A_cont, SMK_09C]: Master priority via SMK_09C exact years; PUMF fallback via SMK_09A_cont midpoint. Not supported 2022 or PUMF 2023. PUMF grouping from 2003 tops out at 3 or more years (midpoint 5).","Unified variable (cchsflow v3). PUMF: midpoint from SMK_09A_cont; Master: exact from SMK_09C. Former daily smokers only.","Years since stopped daily smoking. Retained for the intensity model and the daily-smoking sensitivity analysis; no longer the cessation exit (see time_quit_smoking_complete).","0.1.0","2026-08-27","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","predictor, table1, imputation-predictor","both" +"time_quit_smoking_complete","Yrs quit completely (unified)*","Years since stopped smoking completely - unified (cchsflow v3)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","DerivedVar::[SMKDSTY_cat5, SMK_10_gate, SMK_06A_cont, SMK_09A_cont, SMK_10A_cont]","smoking","Health behaviour","Years","Unified variable (cchsflow v3). Universe: former smokers (daily or occasional). Derived from the stopped-completely questions (SMK_10 gate, SMK_06A/09A/10A), first asked in 2003. Not available in 2001 or the 2022 PUMF (NA(c); imputed per Appendix D); available in the 2023 Master file.","Years since the respondent stopped smoking completely. PUMF: midpoint-estimated from grouped categories (from 2003: under 1, 1-2, 2-3, 3 or more years; largest midpoint 5); Master: exact.","Cessation exit variable: the APC cessation event is stopping smoking completely (estimand specification, section 3).","0.2.0","2026-08-27","Added under remediation task 1.3 (established-smoking estimand).","","Replaces time_quit_smoking_daily as the cessation exit variable.","predictor, table1, apc-numerator, apc-denominator, imputation-predictor","both" "SMK_09A_cont","Yrs quit daily (PUMF)","Years since stopped smoking daily - former daily (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_09A, cchs2003_p::SMKC_09A, cchs2005_p::SMKE_09A, cchs2015_2016_p::SMK_080, cchs2017_2018_p::SMK_080, cchs2019_2020_p::SMK_080, cchs2001_m::SMKA_09A, cchs2003_m::SMKC_09A, cchs2005_m::SMKE_09A, cchs2015_2016_m::SMK_080, cchs2017_2018_m::SMK_080, cchs2019_2020_m::SMK_080, cchs2021_m::SMK_080, cchs2023_m::SPU_25, [SMK_09A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former daily smokers. Feeder for time_quit_smoking and time_quit_smoking_daily (cchsflow v3). Not available 2022 or PUMF 2023 (SPU_25 is Master-only).","PUMF-derived continuous years since quit. Superseded by time_quit_smoking once cchsflow v3 merges. Keep for pre-v3 fallback","Intermediate: PUMF/Master midpoint feeder for the unified cessation variables.","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_06A_cont","Yrs quit occ (PUMF)","Years since stopped smoking - former occasional smokers (PUMF continuous)","Continuous","cchs2001_p, cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2001_m, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2001_p::SMKA_06A, cchs2003_p::SMKC_06A, cchs2005_p::SMKE_06A, cchs2015_2016_p::SMK_060, cchs2017_2018_p::SMK_060, cchs2019_2020_p::SMK_060, cchs2001_m::SMKA_06A, cchs2003_m::SMKC_06A, cchs2005_m::SMKE_06A, cchs2015_2016_m::SMK_060, cchs2017_2018_m::SMK_060, cchs2019_2020_m::SMK_060, cchs2021_m::SMK_060, cchs2023_m::SPU_10, [SMK_06A]","smoking","Health behaviour","Years","Midpoint-imputed years since quit, former occasional smokers. Feeder for time_quit_smoking (cchsflow v3 falls back to it when SMK_09A_cont is not applicable). Not available 2022 or PUMF 2023.","PUMF-derived years since quit for former occasional smokers. Not covered by time_quit_smoking (daily only)","Intermediate: occasional-smoker feeder for time_quit_smoking (all former smokers).","0.1.0","2026-06-09","","","Synced to cchsflow v3 (smoking merged 2026-04-29, commit bd0df3ac)","intermediate","both" "SMK_10A_cont","Yrs quit (reducer)","Years since quit completely (former daily who continued occasional, continuous)","Continuous","cchs2003_p, cchs2005_p, cchs2007_2008_p, cchs2009_2010_p, cchs2010_p, cchs2011_2012_p, cchs2012_p, cchs2013_2014_p, cchs2014_p, cchs2015_2016_p, cchs2017_2018_p, cchs2019_2020_p, cchs2003_m, cchs2005_m, cchs2007_2008_m, cchs2009_2010_m, cchs2009_m, cchs2010_m, cchs2011_2012_m, cchs2012_m, cchs2013_2014_m, cchs2014_m, cchs2015_2016_m, cchs2017_2018_m, cchs2019_2020_m, cchs2021_m, cchs2023_m","cchs2003_p::SMKC_10A, cchs2005_p::SMKE_10A, cchs2015_2016_p::SMK_100, cchs2017_2018_p::SMK_100, cchs2019_2020_p::SMK_100, cchs2003_m::SMKC_10A, cchs2005_m::SMKE_10A, cchs2015_2016_m::SMK_100, cchs2017_2018_m::SMK_100, cchs2019_2020_m::SMK_100, cchs2021_m::SMK_100, cchs2023_m::SPU_35, [SMK_10A]","Smoking","Health behaviour","years","cchsflow v3 building block; not used directly by pipeline code.","Years since quit completely (former daily who continued occasional, continuous)","Intermediate: years since stopped smoking completely (midpoint, gradual quitters); feeder for time_quit_smoking_complete.","0.2.0","2026-08-27","Added under task 1.3: feeder closure for time_quit_smoking_complete (found by the CI pipeline run).","","","intermediate","both" From f134fa71b9ebb3b3966052d7aec67b1f9c5fce35 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 16:46:49 -0400 Subject: [PATCH 27/29] Config comments: PUMF quit-duration grouping is 3+ years from 2003, not a top-code of 15 --- config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yml b/config.yml index 65e3077..ddd9e8b 100644 --- a/config.yml +++ b/config.yml @@ -201,7 +201,7 @@ default: # the 2022 PUMF (NA(c)); handled by the imputation path for cycle-level absence. years_since_quit_complete: pumf: - var: time_quit_smoking_complete # midpoint-estimated; top-coded at 15 + var: time_quit_smoking_complete # midpoint-estimated; 2003+ PUMF groups to 3+ years (largest midpoint 5) -- see the worksheet rules range: variable_details master: var: time_quit_smoking_complete # exact years @@ -211,7 +211,7 @@ default: # daily-smoking sensitivity analysis. years_since_quit: pumf: - var: time_quit_smoking_daily # Years since stopped daily (midpoint-estimated, top-coded at 15) + var: time_quit_smoking_daily # Years since stopped daily (midpoint-estimated; 2001 reaches 15, 2003+ groups to 3+ years) range: variable_details master: var: time_quit_smoking_daily # Exact years since quit daily (SMK_09C) From 189fc685ca81c970572c2456650120a059b888ac Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 18:09:30 -0400 Subject: [PATCH 28/29] Protocol v0.4.1: complete the version-history entry --- docs/protocol/_docstyle/section-map.json | 20 ++++++++++---------- docs/protocol/full-protocol.qmd | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/protocol/_docstyle/section-map.json b/docs/protocol/_docstyle/section-map.json index b96796a..bd5f04f 100644 --- a/docs/protocol/_docstyle/section-map.json +++ b/docs/protocol/_docstyle/section-map.json @@ -18,8 +18,8 @@ "field_code_payload": { "type": "section", "version": 2, - "class": "section-body", - "line-numbers": "continuous" + "line-numbers": "continuous", + "class": "section-body" } }, { @@ -31,8 +31,8 @@ "field_code_payload": { "type": "section", "version": 2, - "class": "section-body-end", - "line-numbers": "continuous" + "line-numbers": "continuous", + "class": "section-body-end" } }, { @@ -44,8 +44,8 @@ "field_code_payload": { "type": "section", "version": 2, - "class": "section-body", - "page-break": true + "page-break": true, + "class": "section-body" } }, { @@ -69,8 +69,8 @@ "field_code_payload": { "type": "section", "version": 2, - "class": "section-body", - "page-break": true + "page-break": true, + "class": "section-body" } }, { @@ -94,8 +94,8 @@ "field_code_payload": { "type": "section", "version": 2, - "class": "section-body", - "page-break": true + "page-break": true, + "class": "section-body" } }, { diff --git a/docs/protocol/full-protocol.qmd b/docs/protocol/full-protocol.qmd index 764556d..9f4e3b6 100644 --- a/docs/protocol/full-protocol.qmd +++ b/docs/protocol/full-protocol.qmd @@ -7,7 +7,7 @@ version-summary: version-history: - version: "0.4.1" date: "2026-08-27" - description: "Editorial revisions from two rounds of PI review of the Word render (wording and plain-language edits throughout; no methodological change). Ethics statement rewritten to describe secondary analysis of de-identified data and the Statistics Canada Open Licence. Study team updated (co-led with BC Cancer Research Institute; Canadian Partnership Against Cancer named among knowledge users). Fixed NHIS typo." + description: "Editorial revisions from PI review of the Word render (wording and plain-language edits throughout; no methodological change). Ethics statement rewritten to describe secondary analysis of de-identified data and the Statistics Canada Open Licence. Study team updated (co-led with BC Cancer Research Institute; Canadian Partnership Against Cancer named among knowledge users). Fixed NHIS typo. Document production: Word rendering restored as a docstyle subproject (docs/protocol/_quarto.yml) with protocol-specific spacing; reporting-guideline placeholder moved so the sensitivity-analysis list renders as a list; redundant page break before Background removed; the three references previously rendered by citeproc (Holford 2006; Rao, Wu and Yue 1992; Statistics Canada 2023) added to the Zotero citation store so the reference list is complete and in one place." - version: "0.4.0" date: "2026-08-07" description: "Methodological amendments following the review (2026-08-07). New section 3.4.1 defines the smoking states and transitions: never, current, and former, following Holford et al. (2014) and Manuel et al. (2020); initiation is the first whole cigarette; cessation is stopping smoking completely; daily smoking and intensity are characteristics of current smokers; one smoking spell per person, with cessation risk starting at the age of entry; a two-year definition of cessation; the rule for same-age initiation and cessation to be set before fitting, with a sensitivity analysis. Section 3.4.3: the split of the linear trend between age, period, and cohort is stated as an explicit assumption (cohort linear trend set to zero, following Manuel et al. 2020; the alternative allocation is a sensitivity analysis); the rules that extend the model for projection are distinguished from the rule that fixes it; the spline basis is saved at fitting and reused; each fitted model is checked before use (Holford 2006). Section 3.5: uncertainty from CCHS bootstrap replicate weights in the Master-file analysis, combined across imputations and carried through to the rate tables; an approximate bootstrap for public-use analyses; simulated weights (MockData) test code only. Section 3.4.5: MPoRT remains the primary mortality adjustment if it passes life-table and simulation checks; where the adjustment enters the calculation to be settled before implementation; unadjusted results labelled as estimates among survivors. Section 3.4.2 and Appendix D: imputation follows the structure of the smoking questions, with ordering rules applied within the procedure, every imputed dataset analysed, and the number of imputations set by stability (at least five). Validation: within the CCHS, using held-out cycles and recall-consistency checks; no comparison with other surveys in this version, with reasons given. Subsections of 3.4 renumbered." From 2652c491e2e2f964e059750b39ec4b1c754574f6 Mon Sep 17 00:00:00 2001 From: Doug Manuel Date: Thu, 27 Aug 2026 18:17:15 -0400 Subject: [PATCH 29/29] Task 1.3: address second-round review of the cessation risk set - The entry age in the cessation model is checked against the worksheet range for age_first_cigarette in the respondent's database, as the initiation model does; an entry age outside it is excluded and counted (excluded_entry_invalid). - Missing smoking status and a missing 100-cigarette answer are counted before the established-smoker filter (excluded_status_missing, excluded_criterion_missing), with experimental smokers reported as not_established_ever_smokers. - Range resolution fails closed: a cycle code not listed in cchs_cycles stops, and a non-missing value from a database with no worksheet range stops rather than passing. Test fixtures now carry NA(c) where the real PUMF does (2001/2022 quit timing, 2022 age at first cigarette). - config/statscan.yml.example uses the worksheet database names (cchs2001_m, ...). - check_feeder_closure() is database-specific: a feeder rule in another cycle does not close the chain. A feeder absent from cshm-variables.csv stops; a feeder with no rule in a study database warns (the 2019-20 PUMF has no SMKG040, which the cchsflow rules for SMKG203_cont, SMKG207_cont and age_start_smoking assume). - Manuscript, variable reference, estimand specification and Stage 7 workflow no longer describe a fixed cessation floor or survey_bound(); they describe the respondent-specific entry age and the worksheet range check. --- R/apc-model.R | 63 ++++++++++++++++++---- R/validate-coverage.R | 58 +++++++++++++++----- _targets.R | 2 +- config/statscan.yml.example | 24 ++++----- docs/development/estimand-specification.md | 4 +- docs/reference/variables.qmd | 4 +- docs/workflow/7-apc-data-preparation.qmd | 2 +- manuscript/manuscript.qmd | 4 +- tests/testthat/helper-apc.R | 5 ++ tests/testthat/test-apc-data.R | 47 ++++++++++++++++ tests/testthat/test-validate-coverage.R | 27 +++++++++- 11 files changed, 197 insertions(+), 43 deletions(-) diff --git a/R/apc-model.R b/R/apc-model.R index 6942010..13d006d 100644 --- a/R/apc-model.R +++ b/R/apc-model.R @@ -208,18 +208,39 @@ build_initiation_data <- function(data, cfg, variable_details_sheet) { #' @return List with numeric vectors `min` and `max` (NA where unbounded) per_respondent_range <- function(cfg, key, cycle, variable_details_sheet) { dbs <- cycle_database(cfg, cycle) - known <- unique(dbs[!is.na(dbs)]) + if (anyNA(dbs)) { + stop( + "Cycle codes not listed in cfg$cchs_cycles: ", + paste(unique(cycle[is.na(dbs)]), collapse = ", "), + ". Every cycle in the data must map to a database name (e.g. cchs2001_p, cchs2001_m)." + ) + } + known <- unique(dbs) ranges <- lapply(known, function(db) survey_range(cfg, key, db, variable_details_sheet)) names(ranges) <- known + # A database with no worksheet rows for the variable gets an NA range. That is fine while + # every value from that database is missing (e.g. the variable is not asked in the cycle); + # outside_range() stops if a non-missing value meets an NA range, so nothing is guessed. idx <- match(dbs, known) list( - min = vapply(idx, function(i) if (is.na(i)) NA_real_ else ranges[[i]][["min"]], numeric(1)), - max = vapply(idx, function(i) if (is.na(i)) NA_real_ else ranges[[i]][["max"]], numeric(1)) + key = key, + database = dbs, + min = vapply(idx, function(i) ranges[[i]][["min"]], numeric(1)), + max = vapply(idx, function(i) ranges[[i]][["max"]], numeric(1)) ) } #' TRUE where a value lies outside its per-respondent range (NA bounds ignored) outside_range <- function(x, range) { + unresolved <- !is.na(x) & (is.na(range$min) | is.na(range$max)) + if (any(unresolved)) { + stop( + sum(unresolved), " non-missing value(s) of '", range$key %||% "variable", + "' come from database(s) with no range in the variable-details worksheet: ", + paste(unique(range$database[unresolved]), collapse = ", "), + ". Add worksheet rows for those databases; the range is not guessed." + ) + } below <- !is.na(range$min) & x < range$min above <- !is.na(range$max) & x > range$max !is.na(x) & (below | above) @@ -349,7 +370,22 @@ build_cessation_data <- function(data, cfg, variable_details_sheet) { former_codes <- survey_code(cfg, "smoking_status", "former_codes") smk <- data[[status_col]] smoked_100 <- data[[smoked_100_col]] - established <- !is.na(smk) & smk %in% ever_codes & !is.na(smoked_100) & smoked_100 == smoked_100_yes + all_cycle <- as.character(data[[cycle_col]]) + all_weight <- data[[weight_col]] + # Who is missing before the established filter is applied. These respondents never reach + # the risk set, so they are counted here or not at all. + status_missing <- is.na(smk) + ever <- !status_missing & smk %in% ever_codes + criterion_missing <- ever & is.na(smoked_100) + not_established <- ever & !is.na(smoked_100) & smoked_100 != smoked_100_yes + established <- ever & !is.na(smoked_100) & smoked_100 == smoked_100_yes + pre_groups <- list( + respondents = rep(TRUE, nrow(data)), + excluded_status_missing = status_missing, + excluded_criterion_missing = criterion_missing, + not_established_ever_smokers = not_established + ) + pre_diag <- summarise_groups(pre_groups, all_cycle, all_weight) d <- data[established, ] smk <- d[[status_col]] @@ -360,13 +396,17 @@ build_cessation_data <- function(data, cfg, variable_details_sheet) { weight <- d[[weight_col]] cycle <- as.character(d[[cycle_col]]) # observed cycles only; avoids NA sums for empty levels quit_range <- per_respondent_range(cfg, "years_since_quit_complete", cycle, variable_details_sheet) + init_range <- per_respondent_range(cfg, "age_first_cigarette", cycle, variable_details_sheet) current <- smk %in% current_codes former <- smk %in% former_codes # Classification: each established smoker falls in exactly one group missing_entry <- is.na(age_init) - entry_after_survey <- !missing_entry & age_init > age_survey + # The entry age must lie within the worksheet range for age_first_cigarette in the + # respondent's database (the same check the initiation model applies). + entry_invalid <- !missing_entry & outside_range(age_init, init_range) + entry_after_survey <- !missing_entry & !entry_invalid & age_init > age_survey timing_missing <- former & is.na(yrs_quit) # Quit timing must be finite, within the configured bounds for the source # (PUMF top-code, Master ceiling), and place the quit no later than the survey. @@ -374,7 +414,7 @@ build_cessation_data <- function(data, cfg, variable_details_sheet) { (!is.finite(yrs_quit) | outside_range(yrs_quit, quit_range) | is.na(age_quit) | age_quit > age_survey | age_quit < 0) quit_before_entry <- former & !is.na(age_quit) & !timing_invalid & !missing_entry & age_quit < age_init - excluded <- missing_entry | entry_after_survey | timing_missing | timing_invalid | quit_before_entry + excluded <- missing_entry | entry_invalid | entry_after_survey | timing_missing | timing_invalid | quit_before_entry recent <- !excluded & former & yrs_quit < durability durable <- !excluded & former & yrs_quit >= durability same_age <- durable & age_quit == age_init @@ -386,19 +426,24 @@ build_cessation_data <- function(data, cfg, variable_details_sheet) { recent_quitters_censored = recent, same_age_quits = same_age, excluded_missing_entry = missing_entry, + excluded_entry_invalid = entry_invalid, excluded_entry_after_survey = entry_after_survey, excluded_timing_missing = timing_missing, excluded_quit_timing_invalid = timing_invalid, excluded_quit_before_entry = quit_before_entry ) - diag <- summarise_groups(groups, cycle, weight) - totals <- vapply(groups, sum, numeric(1)) + diag <- rbind(pre_diag, summarise_groups(groups, cycle, weight)) + totals <- vapply(c(pre_groups, groups), sum, numeric(1)) message( - "Cessation risk set: ", totals[["established"]], " established smokers; ", + "Cessation risk set: ", totals[["respondents"]], " respondents; ", + totals[["excluded_status_missing"]], " missing smoking status; ", + totals[["excluded_criterion_missing"]], " ever smokers missing the 100-cigarette criterion; ", + totals[["established"]], " established smokers; ", totals[["durable_quitters"]], " durable quitters (events); ", totals[["recent_quitters_censored"]], " recent quitters censored; ", totals[["same_age_quits"]], " started and stopped at the same age. Excluded pending imputation: ", totals[["excluded_missing_entry"]], " missing entry age, ", + totals[["excluded_entry_invalid"]], " entry age outside the worksheet range, ", totals[["excluded_entry_after_survey"]], " entry after survey, ", totals[["excluded_timing_missing"]], " missing quit timing, ", totals[["excluded_quit_timing_invalid"]], " quit timing out of bounds, ", diff --git a/R/validate-coverage.R b/R/validate-coverage.R index 1a76e6a..0aafd1d 100644 --- a/R/validate-coverage.R +++ b/R/validate-coverage.R @@ -169,32 +169,64 @@ validate_cycle_coverage <- function(variables_sheet, #' @param variable_details_sheet Combined variable-details worksheet (data frame) #' @return Invisibly, a data frame of (variable, missing_feeder) pairs; stops if #' any row exists -check_feeder_closure <- function(variables_sheet, variable_details_sheet) { +check_feeder_closure <- function(variables_sheet, variable_details_sheet, databases = NULL) { + # `databases`: restrict the check to these database names (normally the study cycles, + # unlist(cfg$cchs_cycles)); NULL checks every database a derived rule names. study <- unique(variables_sheet$variable) - det <- variable_details_sheet[variable_details_sheet$variable %in% study, c("variable", "variableStart")] - rules <- det[grepl("DerivedVar::\\[", det$variableStart), ] + det <- variable_details_sheet[, c("variable", "variableStart", "databaseStart")] + split_dbs <- function(x) trimws(strsplit(as.character(x), ",")[[1]]) + det_dbs <- lapply(det$databaseStart, split_dbs) + rules <- which(det$variable %in% study & grepl("DerivedVar::\\[", det$variableStart)) gaps <- list() - for (i in seq_len(nrow(rules))) { - inner <- sub(".*DerivedVar::\\[([^]]*)\\].*", "\\1", rules$variableStart[i]) + for (i in rules) { + inner <- sub(".*DerivedVar::\\[([^]]*)\\].*", "\\1", det$variableStart[i]) feeders <- trimws(strsplit(inner, ",")[[1]]) - missing <- setdiff(feeders, study) - if (length(missing)) { - gaps[[length(gaps) + 1]] <- data.frame( - variable = rules$variable[i], missing_feeder = missing, stringsAsFactors = FALSE - ) + dbs_i <- det_dbs[[i]] + if (!is.null(databases)) dbs_i <- intersect(dbs_i, databases) + for (db in dbs_i) { + for (f in feeders) { + in_study <- f %in% study + # A feeder only closes the chain for this database if the details sheet has a rule + # for it in the same database; a rule in another cycle does not count. + has_rule <- any(det$variable == f & vapply(det_dbs, function(x) db %in% x, logical(1))) + if (!in_study || !has_rule) { + gaps[[length(gaps) + 1]] <- data.frame( + variable = det$variable[i], missing_feeder = f, database = db, + reason = if (!in_study) "not in cshm-variables.csv" else "no variable-details rule for this database", + stringsAsFactors = FALSE + ) + } + } } } gaps <- if (length(gaps)) { unique(do.call(rbind, gaps)) } else { - data.frame(variable = character(0), missing_feeder = character(0)) + data.frame( + variable = character(0), missing_feeder = character(0), + database = character(0), reason = character(0) + ) } - if (nrow(gaps) > 0) { + not_in_study <- gaps[gaps$reason == "not in cshm-variables.csv", , drop = FALSE] + no_rule <- gaps[gaps$reason != "not in cshm-variables.csv", , drop = FALSE] + if (nrow(not_in_study) > 0) { + # Fixable in this repo: add the feeder as an intermediate row. stop( "Derived study variables with feeders missing from worksheets/cshm-variables.csv: ", - paste(unique(paste0(gaps$variable, " needs ", gaps$missing_feeder)), collapse = "; "), + paste(unique(paste0(not_in_study$variable, " needs ", not_in_study$missing_feeder)), collapse = "; "), ". Add the feeders as intermediate rows; cchsflow skips the derivation silently otherwise." ) } + if (nrow(no_rule) > 0) { + # The derived rule names a database in which its feeder has no rule (the feeder is not in + # that cycle's file). cchsflow returns NA for the derived variable there. This is recorded + # as a warning so the gap is visible in the pipeline log and in the coverage_check target. + warning( + "Derived study variables whose feeders have no variable-details rule in a database they are derived for ", + "(the derived variable will be missing for that cycle): ", + paste(unique(paste0(no_rule$variable, " needs ", no_rule$missing_feeder, " in ", no_rule$database)), collapse = "; "), + call. = FALSE + ) + } invisible(gaps) } diff --git a/_targets.R b/_targets.R index 8afa424..660d162 100644 --- a/_targets.R +++ b/_targets.R @@ -31,7 +31,7 @@ list( # Stage 0: Pre-flight validation — verify variable coverage before loading data # Returns gap report (declared + critical); warns or errors per cfg$strict_validation tar_target(coverage_check, { - check_feeder_closure(variables_sheet, variable_details_sheet) + check_feeder_closure(variables_sheet, variable_details_sheet, databases = unlist(cfg$cchs_cycles)) validate_cycle_coverage(variables_sheet, variable_details_sheet, cfg, strict = cfg$strict_validation %||% FALSE) }), diff --git a/config/statscan.yml.example b/config/statscan.yml.example index 8e9648e..d800786 100644 --- a/config/statscan.yml.example +++ b/config/statscan.yml.example @@ -15,18 +15,18 @@ statscan: # Master cycles available at RDC (2001–2023) cchs_cycles: - - cchs2001_master - - cchs2003_master - - cchs2005_master - - cchs2007_2008_master - - cchs2009_2010_master - - cchs2011_2012_master - - cchs2013_2014_master - - cchs2015_2016_master - - cchs2017_2018_master - - cchs2019_2020_master - - cchs2022_master - - cchs2023_master + - cchs2001_m + - cchs2003_m + - cchs2005_m + - cchs2007_2008_m + - cchs2009_2010_m + - cchs2011_2012_m + - cchs2013_2014_m + - cchs2015_2016_m + - cchs2017_2018_m + - cchs2019_2020_m + - cchs2022_m + - cchs2023_m # Output paths within RDC vetting folder derived_data: diff --git a/docs/development/estimand-specification.md b/docs/development/estimand-specification.md index 1794427..77f657c 100644 --- a/docs/development/estimand-specification.md +++ b/docs/development/estimand-specification.md @@ -42,8 +42,8 @@ The CCHS does not ask the age at which the 100th cigarette was smoked, so the ag - **Time step and interval.** One year. The row for age *a* covers the year from the person's *a*-th birthday to the day before the next one. An event at age *a* happened during that year. The event row is part of the risk set: it carries one trial, with the event. Within a year, initiation is applied before cessation, so a person who starts and stops at the same age is counted as having smoked for one year (below). The year of the survey is the last observed row for everyone; it is treated as a full year of exposure, a simplification shared with the Manuel and Holford implementations. - **One period of smoking per person.** A person enters Current once and leaves it at most once. -- **Initiation risk.** From the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. -- **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. A fixed minimum age, if used, is a reporting boundary only. +- **Initiation risk.** From the initiation floor (`initiation_floor(cfg)`; PUMF 13, Master 8) to the age at first cigarette (event) or the survey age (censored), whichever comes first. Never smokers are at risk at every age up to the survey. +- **Cessation risk.** From the person's own age at first cigarette to the age they stopped completely (event) or the survey age (censored). No person-year before entry. There is no fixed minimum age; the entry age must fall within the worksheet range for `age_first_cigarette`, and a respondent whose entry age falls outside it is excluded and counted (`excluded_entry_invalid`). - **Durable cessation and recent quitters.** The primary definition of cessation is a quit that has lasted at least two years at the survey. Three things are distinguished for a person who quit less than two years before the survey. *Observed status:* Former (`SMKDSTY_original`). *Modelled state:* Current at every age up to the survey, because the quit is not yet known to be durable; this is the state used for prevalence and passed to the generator. *Cessation risk set:* person-years from entry to the reported quit age, then censored with no event; the years between the quit age and the survey are not in the risk set because whether the quit will hold cannot yet be observed. The person therefore has exactly one modelled state at each age (Current) while contributing to the risk set only up to the quit age. Risk-set membership describes what can be observed about the outcome; it is not the state. - **Same-age initiation and cessation (ratified 2026-08-27).** When the age at first cigarette equals the age at stopping, the data cannot show which came first within the year. *Primary rule:* the person smoked for one year. They enter Current at that age, contribute one person-year at risk of cessation at that age, and have the cessation event in it. *Prespecified sensitivity:* remove the person from both transition models -- no initiation event and no cessation record -- treating them as never having established smoking, so that reconstructed prevalence does not acquire an initiation without its cessation. Few respondents, possibly none, are expected to have started and stopped at the same age; the pipeline reports the unweighted and weighted count per cycle so the expectation is checked rather than assumed. - **Reported ages that cannot be right** (initiation after survey age, cessation before initiation) are treated as missing and enter the imputation procedure (task 1.8c). No person is silently reclassified. diff --git a/docs/reference/variables.qmd b/docs/reference/variables.qmd index ecfa81e..0cddb02 100644 --- a/docs/reference/variables.qmd +++ b/docs/reference/variables.qmd @@ -480,10 +480,10 @@ initiation × {male, female} and cessation × {male, female}. ### Rate definitions **Initiation rate:** P(initiate at age *a* | never smoker at age *a*−1). -Zero for ages below `survey_bound(cfg, "age_first_cigarette", "min")` (PUMF: 13, Master: 8). +Modelled from the initiation floor, `initiation_floor(cfg)` (`apc.initiation_floor_age` in `config.yml`: PUMF 13, Master 8). Reported ages at first cigarette are checked against the range in the variable-details worksheet (`survey_range()` / `details_range()`), not against a value in `config.yml`. **Cessation rate:** P(quit at age *a* | current smoker at age *a*−1). -Zero for ages below `survey_bound(cfg, "age_first_cigarette", "min")` for cessation as well. +Risk starts at each person's own age at first whole cigarette and ends at the age they stopped completely (event) or the survey age (censored). No fixed minimum age applies; a reported entry age outside the worksheet range for `age_first_cigarette` excludes the respondent from the cessation model (`excluded_entry_invalid`). ### Period constraint years diff --git a/docs/workflow/7-apc-data-preparation.qmd b/docs/workflow/7-apc-data-preparation.qmd index 93a8a86..90f52f0 100644 --- a/docs/workflow/7-apc-data-preparation.qmd +++ b/docs/workflow/7-apc-data-preparation.qmd @@ -91,7 +91,7 @@ The spline basis columns are built in Stage 8 (`build_spline_basis()`), not stor **Mortality adjustment.** Ever-smokers are less likely to survive to survey date than never-smokers, creating survival bias. No correction is applied yet: `cfg$apc$mortality_method` is `"none"`, the `weight` column holds the survey weight alone, and the datasets carry an `estimand_note` attribute stating that results describe respondents who survived to be surveyed. MPoRT (primary) and Peto (sensitivity) are not yet implemented (protocol section 3.4.5). -**Cessation model: who is included and when time at risk begins.** The cessation model includes established smokers: people who have smoked 100 or more cigarettes in their lifetime (`smoked_100_lifetime`), whatever their current smoking pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person's time at risk begins at their own age at first whole cigarette; the study floor age (`survey_bound(cfg, "age_first_cigarette", "min")`) is a reporting boundary only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count as cessation: the person is a current smoker at the survey and their time at risk ends at the quit age without an event. A person who started and stopped at the same age contributes one year at risk with the event in it. People with a missing entry age or missing quit timing (including the whole 2001 cycle and the 2022 PUMF cycle, where the stopped-completely timing is not available) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. +**Cessation model: who is included and when time at risk begins.** The cessation model includes established smokers: people who have smoked 100 or more cigarettes in their lifetime (`smoked_100_lifetime`), whatever their current smoking pattern. The event is stopping smoking completely, dated by `time_quit_smoking_complete`. Each person's time at risk begins at their own age at first whole cigarette; there is no fixed minimum age. The entry age must lie within the range for `age_first_cigarette` in the variable-details worksheet for the respondent's database (the same check the initiation model applies); an entry age outside it is excluded and counted as `excluded_entry_invalid`. The initiation floor (`initiation_floor(cfg)`, `apc.initiation_floor_age`) applies to the initiation model only. A quit that has lasted fewer than `cfg$apc$cessation_durability_years` (2) years at the survey does not count as cessation: the person is a current smoker at the survey and their time at risk ends at the quit age without an event. A person who started and stopped at the same age contributes one year at risk with the event in it. People with a missing smoking status or a missing 100-cigarette answer are counted before the established-smoker filter (`excluded_status_missing`, `excluded_criterion_missing`). People with a missing entry age or missing quit timing (including the whole 2001 cycle and the 2022 PUMF cycle, where the stopped-completely timing is not available) are excluded here and counted in the `cessation_diagnostics` attribute; imputation (task 1.8c) will supply their values. See `docs/development/estimand-specification.md`. **Four separate data frames, not one.** Men and women are modelled separately (consistent with Manuel et al. 2020). Initiation and cessation are separate models. Keeping four data frames lets Stage 8 fit all four models in parallel using `{targets}` branching. diff --git a/manuscript/manuscript.qmd b/manuscript/manuscript.qmd index f1eac9b..98a7977 100644 --- a/manuscript/manuscript.qmd +++ b/manuscript/manuscript.qmd @@ -96,9 +96,9 @@ We used all available cycles of the CCHS (2001–2022 PUMF; 2001–2023 Master f We fit two logistic regression models using an age-period-cohort framework: -1. **Initiation model** — annual probability of transitioning from never smoker to current smoker, conditional on being a never smoker at age *a*−1. Probability assumed zero before age `r survey_bound(cfg, "age_first_cigarette", "min")`. +1. **Initiation model** — annual probability of transitioning from never smoker to current smoker, conditional on being a never smoker at age *a*−1. Initiation is modelled from age `r initiation_floor(cfg)` (PUMF 13; Master 8, section 3.3 of the protocol); the model gives no probability of initiation below that age. -2. **Cessation model** — conditional probability of a current smoker quitting at age *a*. Probability assumed zero before `r survey_bound(cfg, "years_since_quit", "min")` years since quitting. +2. **Cessation model** — conditional probability of a current smoker quitting at age *a*. Each established smoker is at risk of quitting from their own age at first whole cigarette to the age they stopped completely (event) or the survey age (censored); there is no fixed minimum age for cessation. A quit counts as an event only when it has lasted at least `r cfg$apc$cessation_durability_years` years at the survey. Both models used constrained natural cubic splines with the knot structure from Holford et al. [-@Holford_AJPM_2014]: age knots `r paste0("[", paste(cfg$apc$age_knots, collapse = ", "), "]")`, period knots `r paste0("[", paste(cfg$apc$period_knots, collapse = ", "), "]")`, and cohort knots `r paste0("[", paste(cfg$apc$cohort_knots, collapse = ", "), "]")`. diff --git a/tests/testthat/helper-apc.R b/tests/testthat/helper-apc.R index 59feb26..037e51e 100644 --- a/tests/testthat/helper-apc.R +++ b/tests/testthat/helper-apc.R @@ -25,7 +25,12 @@ make_apc_test_data <- function(cfg, n = 100, seed = 42) { # PUMF 2003+ groups quit duration to a largest midpoint of 5 (worksheet rules) yrs_quit_complete <- ifelse(smkdsty %in% c(4, 5), sample(c(0.5, 1.5, 2.5, 5), n, replace = TRUE), NA_real_) # keep quit age at or after the entry age so the base data are internally consistent + # 2022 PUMF (cycle 11): age_first_cigarette has no variable-details rule, so the real data + # carry NA there; a non-missing value with no worksheet range is an error by design. + age_first[cycles == "11"] <- NA_real_ yrs_quit_complete <- pmin(yrs_quit_complete, ages - age_first) + # 2001 and 2022 PUMF (cycles 1 and 11): time_quit_smoking_complete is not available, NA(c). + yrs_quit_complete[cycles %in% c("1", "11")] <- NA_real_ # Simulate survey years (2002–2022 range) and cohorts survey_years <- sample(2002:2022, n, replace = TRUE) diff --git a/tests/testthat/test-apc-data.R b/tests/testthat/test-apc-data.R index 6f334ab..5180334 100644 --- a/tests/testthat/test-apc-data.R +++ b/tests/testthat/test-apc-data.R @@ -439,3 +439,50 @@ test_that("cessation: a quit duration above the worksheet top-code for the cycle diag <- attr(result, "cessation_diagnostics") expect_equal(sum(diag$n[diag$group == "excluded_quit_timing_invalid"]), 1L) }) + + +test_that("cessation: an entry age outside the worksheet range is excluded and counted (finding 1)", { + cfg <- cess_cfg() + df <- rbind( + one_person(cfg, status = 1, age_first = 1, age = 40), # below the worksheet minimum for age_first_cigarette + one_person(cfg, status = 1, age_first = 16, age = 40) + ) + out <- build_cessation_data(df, cfg, TEST_DETAILS) + diag <- attr(out, "cessation_diagnostics") + expect_equal(sum(diag$n[diag$group == "excluded_entry_invalid"]), 1) + expect_equal(sum(diag$n[diag$group == "current_at_survey"]), 1) + expect_equal(min(out$age), 16) # no person-year from the invalid entry + expect_equal(nrow(out), 40 - 16 + 1) +}) + +test_that("cessation: missing status and missing 100-cigarette criterion are counted before the established filter (finding 4)", { + cfg <- cess_cfg() + df <- rbind( + one_person(cfg, status = NA, age_first = 16), + one_person(cfg, status = 1, smoked_100 = NA, age_first = 16), + one_person(cfg, status = 1, smoked_100 = 2, age_first = 16), # experimental: not established + one_person(cfg, status = 1, age_first = 16) + ) + out <- build_cessation_data(df, cfg, TEST_DETAILS) + diag <- attr(out, "cessation_diagnostics") + n_of <- function(g) sum(diag$n[diag$group == g]) + expect_equal(n_of("respondents"), 4) + expect_equal(n_of("excluded_status_missing"), 1) + expect_equal(n_of("excluded_criterion_missing"), 1) + expect_equal(n_of("not_established_ever_smokers"), 1) + expect_equal(n_of("established"), 1) +}) + +test_that("per_respondent_range fails closed on unknown cycle codes and unresolved worksheet ranges (finding 2)", { + cfg <- cess_cfg() + expect_error( + per_respondent_range(cfg, "age_first_cigarette", c("5", "99"), TEST_DETAILS), + "Cycle codes not listed in cfg\\$cchs_cycles: 99" + ) + no_rows <- TEST_DETAILS[TEST_DETAILS$variable != survey_var(cfg, "age_first_cigarette"), ] + rng <- per_respondent_range(cfg, "age_first_cigarette", c("5", "5"), no_rows) + # A missing value never needs a range (the variable may not be asked in that cycle) ... + expect_false(outside_range(NA_real_, per_respondent_range(cfg, "age_first_cigarette", "5", no_rows))) + # ... but a non-missing value with no worksheet range is an error, not a pass. + expect_error(outside_range(c(16, NA), rng), "no range in the variable-details worksheet") +}) diff --git a/tests/testthat/test-validate-coverage.R b/tests/testthat/test-validate-coverage.R index 8c21f22..20279f2 100644 --- a/tests/testthat/test-validate-coverage.R +++ b/tests/testthat/test-validate-coverage.R @@ -6,7 +6,16 @@ test_that("check_feeder_closure passes on the project worksheets", { read.csv(ws("cchsflow-variable-details.csv")), read.csv(ws("cshm-variable-details.csv")) )) - expect_silent(check_feeder_closure(vars, det)) + # Restricted to the study cycles. The 2019-20 PUMF has no SMKG040 (grouped age started + # daily), yet the cchsflow rules for SMKG203_cont/SMKG207_cont/age_start_smoking name + # cchs2019_2020_p; the check reports that gap rather than passing over it. + study_dbs <- unlist(config::get()$cchs_cycles) + expect_warning( + check_feeder_closure(vars, det, databases = study_dbs), + "SMKG040_cont in cchs2019_2020_p" + ) + # Every feeder is at least declared in cshm-variables.csv (no stop). + expect_no_error(suppressWarnings(check_feeder_closure(vars, det, databases = study_dbs))) }) test_that("check_feeder_closure stops when a derived variable's feeder is missing", { @@ -14,7 +23,23 @@ test_that("check_feeder_closure stops when a derived variable's feeder is missin det <- data.frame( variable = c("derived_x", "derived_x", "feeder_a"), variableStart = c("DerivedVar::[feeder_a, feeder_b]", "DerivedVar::[feeder_a, feeder_b]", "cchs2001_p::A"), + databaseStart = c("cchs2001_p", "cchs2003_p", "cchs2001_p, cchs2003_p"), stringsAsFactors = FALSE ) expect_error(check_feeder_closure(vars, det), "derived_x needs feeder_b") }) + +test_that("check_feeder_closure is cycle-specific: a feeder present only in another cycle does not close the chain", { + vars <- data.frame(variable = c("derived_x", "feeder_a"), stringsAsFactors = FALSE) + det <- data.frame( + variable = c("derived_x", "feeder_a"), + variableStart = c("DerivedVar::[feeder_a]", "cchs2001_p::A"), + databaseStart = c("cchs2001_p, cchs2003_p", "cchs2001_p"), # feeder_a has no 2003 rule + stringsAsFactors = FALSE + ) + expect_warning(check_feeder_closure(vars, det), "derived_x needs feeder_a in cchs2003_p") + # Restricting the check to the databases in use removes the gap + expect_silent(check_feeder_closure(vars, det, databases = "cchs2001_p")) + det$databaseStart[2] <- "cchs2001_p, cchs2003_p" + expect_silent(check_feeder_closure(vars, det)) +})