Code
library(dplyr)
library(ggplot2)
library(knitr)
library(readr)
library(scales)
library(tidyr)First-pass aggregate reconstruction of the 2024 single-cohort lifetime analysis
library(dplyr)
library(ggplot2)
library(knitr)
library(readr)
library(scales)
library(tidyr)model_inputs <- read_csv("inputs/model_inputs.csv", show_col_types = FALSE)
life_table <- read_csv("inputs/us_female_life_table_2023.csv", show_col_types = FALSE)
scalar_input <- function(input_id) {
value <- model_inputs$value[model_inputs$input_id == input_id]
if (length(value) != 1) {
stop("Expected exactly one input for ", input_id, call. = FALSE)
}
as.numeric(value)
}
text_input <- function(input_id, column) {
value <- model_inputs[[column]][model_inputs$input_id == input_id]
if (length(value) != 1) {
stop("Expected exactly one input for ", input_id, call. = FALSE)
}
value
}
fmt_count <- function(x) comma(round(x, 0), accuracy = 1)
fmt_decimal <- function(x, digits = 3) formatC(x, format = "f", digits = digits)
fmt_pct <- function(x, accuracy = 0.1) percent(x, accuracy = accuracy)
fmt_pp <- function(x, digits = 1) {
paste(formatC(100 * x, format = "f", digits = digits), "percentage points")
}
fmt_years <- function(x) comma(round(x, 1), accuracy = 0.1)
source_link <- function(label, url) {
paste0("[", label, "](", url, ")")
}This report is a first-pass R/Quarto aggregate reconstruction of Kasle et al.’s population impact model for BRCA testing and adjuvant olaparib in high-risk HER2-negative early breast cancer. The source paper estimated 3,938 olaparib-eligible patients in the 2024 US cohort and reported lifetime reductions of 272 metastatic breast cancer (mBC) recurrences, 68 non-mBC recurrences, and 8,677 life-years gained under testing and olaparib treatment versus no testing and no adjuvant treatment.
The extension here keeps that published analysis as a reproduction benchmark and layers in an exploratory age-stratified treatment-effect modifier for <45 years and >=45 years. The age-stratified evidence is uncertain and should not be read as a definitive age-specific olaparib effect.
source_summary <- model_inputs |>
count(category, source_url, name = "inputs") |>
arrange(category, source_url)
kable(
source_summary,
col.names = c("Input category", "Source", "Rows"),
align = c("l", "l", "r")
)| Input category | Source | Rows |
|---|---|---|
| age_distribution | https://doi.org/10.1038/s41523-024-00632-8 | 4 |
| age_modifier | info/BRCA_pilot_age_group_het_trials.docx | 6 |
| benchmark | https://doi.org/10.1080/13696998.2025.2583870 | 9 |
| clinical_rate | https://doi.org/10.1080/13696998.2025.2583870 | 12 |
| population | https://doi.org/10.1080/13696998.2025.2583870 | 3 |
| representative_age | https://doi.org/10.1038/s41523-024-00632-8 | 1 |
| representative_age | https://doi.org/10.1080/13696998.2025.2583870 | 1 |
Key sources:
info/BRCA_pilot_age_group_het_trials.docx and extracted subgroup data at info/d.human.trials.subgroup.data.csv.<45/>=45 split or subtype-specific age moments, so 50%/50% is used as a round proxy consistent with the sub-45 median.The published model splits the eligible cohort into HR-positive and TNBC subpopulations. This report reconstructs those pieces from the published subtype population sizes and rounded clinical inputs.
subtype_inputs <- tibble(
subtype = c("HR-positive", "TNBC"),
patients = c(
scalar_input("eligible_hr_positive"),
scalar_input("eligible_tnbc")
),
no_mbc_rate = c(
scalar_input("hrp_no_treatment_mbc_rate"),
scalar_input("tnbc_no_treatment_mbc_rate")
),
no_non_mbc_rate = c(
scalar_input("hrp_no_treatment_non_mbc_rate"),
scalar_input("tnbc_no_treatment_non_mbc_rate")
),
no_life_years_per_patient = c(
scalar_input("hrp_no_treatment_life_years"),
scalar_input("tnbc_no_treatment_life_years")
),
olaparib_mbc_rate = c(
scalar_input("hrp_olaparib_mbc_rate"),
scalar_input("tnbc_olaparib_mbc_rate")
),
olaparib_non_mbc_rate = c(
scalar_input("hrp_olaparib_non_mbc_rate"),
scalar_input("tnbc_olaparib_non_mbc_rate")
),
olaparib_life_years_per_patient = c(
scalar_input("hrp_olaparib_life_years"),
scalar_input("tnbc_olaparib_life_years")
)
) |>
mutate(
no_mbc = patients * no_mbc_rate,
olaparib_mbc = patients * olaparib_mbc_rate,
mbc_prevented = no_mbc - olaparib_mbc,
no_non_mbc = patients * no_non_mbc_rate,
olaparib_non_mbc = patients * olaparib_non_mbc_rate,
non_mbc_prevented = no_non_mbc - olaparib_non_mbc,
no_life_years = patients * no_life_years_per_patient,
olaparib_life_years = patients * olaparib_life_years_per_patient,
life_years_gained = olaparib_life_years - no_life_years
)
subtype_display <- subtype_inputs |>
transmute(
Subtype = subtype,
Patients = fmt_count(patients),
`mBC avoided` = fmt_count(mbc_prevented),
`non-mBC avoided` = fmt_count(non_mbc_prevented),
`Life-years gained` = fmt_count(life_years_gained)
)
kable(subtype_display, align = c("l", "r", "r", "r", "r"))| Subtype | Patients | mBC avoided | non-mBC avoided | Life-years gained |
|---|---|---|---|---|
| HR-positive | 1,433 | 77 | 19 | 2,579 |
| TNBC | 2,505 | 195 | 48 | 6,012 |
calculated_totals <- subtype_inputs |>
summarise(
`mBC recurrences, no testing` = sum(no_mbc),
`mBC recurrences, olaparib` = sum(olaparib_mbc),
`mBC recurrences avoided` = sum(mbc_prevented),
`non-mBC recurrences, no testing` = sum(no_non_mbc),
`non-mBC recurrences, olaparib` = sum(olaparib_non_mbc),
`non-mBC recurrences avoided` = sum(non_mbc_prevented),
`Life-years, no testing` = sum(no_life_years),
`Life-years, olaparib` = sum(olaparib_life_years),
`Life-years gained` = sum(life_years_gained)
) |>
pivot_longer(everything(), names_to = "metric", values_to = "calculated")
published_totals <- tibble(
metric = c(
"mBC recurrences, no testing",
"mBC recurrences, olaparib",
"mBC recurrences avoided",
"non-mBC recurrences, no testing",
"non-mBC recurrences, olaparib",
"non-mBC recurrences avoided",
"Life-years, no testing",
"Life-years, olaparib",
"Life-years gained"
),
published = c(
scalar_input("published_mbc_no_testing"),
scalar_input("published_mbc_olaparib"),
scalar_input("published_mbc_prevented"),
scalar_input("published_non_mbc_no_testing"),
scalar_input("published_non_mbc_olaparib"),
scalar_input("published_non_mbc_prevented"),
scalar_input("published_life_years_no_testing"),
scalar_input("published_life_years_olaparib"),
scalar_input("published_life_years_gained")
)
)
benchmark_comparison <- calculated_totals |>
left_join(published_totals, by = "metric") |>
mutate(
difference = calculated - published,
relative_difference = difference / published
)
kable(
benchmark_comparison |>
transmute(
Metric = metric,
Calculated = fmt_count(calculated),
Published = fmt_count(published),
Difference = fmt_count(difference),
`Relative difference` = fmt_pct(relative_difference, accuracy = 0.1)
),
align = c("l", "r", "r", "r", "r")
)| Metric | Calculated | Published | Difference | Relative difference |
|---|---|---|---|---|
| mBC recurrences, no testing | 1,220 | 1,218 | 2 | 0.2% |
| mBC recurrences, olaparib | 947 | 946 | 1 | 0.1% |
| mBC recurrences avoided | 273 | 272 | 1 | 0.3% |
| non-mBC recurrences, no testing | 304 | 304 | 0 | 0.0% |
| non-mBC recurrences, olaparib | 238 | 236 | 2 | 0.7% |
| non-mBC recurrences avoided | 66 | 68 | -2 | -2.6% |
| Life-years, no testing | 99,055 | 99,084 | -29 | 0.0% |
| Life-years, olaparib | 107,646 | 107,761 | -115 | -0.1% |
| Life-years gained | 8,591 | 8,677 | -86 | -1.0% |
The reconstruction is close for recurrence outputs because the published table gives subtype-specific recurrence rates. Life-years differ modestly because the report only exposes rounded life-year inputs and aggregate published totals.
This report uses an aggregate population-impact model rather than a cycle-based health-state model. The model first reconstructs all-age recurrence outputs from subtype-specific published inputs, then redistributes the treatment effect across age strata while preserving the published all-age treatment effect on the log-hazard-ratio scale.
Let \(s \in \mathcal{S}\) index subtype strata, where \(\mathcal{S} = \{\text{HR-positive}, \text{TNBC}\}\), and let \(a \in \mathcal{A}\) index age strata, where \(\mathcal{A} = \{<45, \ge 45\}\). Let \(e \in \mathcal{E}\) index recurrence endpoints, where \(\mathcal{E} = \{\text{mBC recurrence}, \text{non-mBC recurrence}\}\).
For each subtype \(s\):
The expected untreated recurrence count in each subtype-age-endpoint cell is:
\[ B_{s,a,e} = N_s w_{s,a} r^0_{s,e}. \]
The published all-age recurrence effect is represented as a base hazard ratio:
\[ H_0 = 1 - \frac{P^{\text{pub}}_{\text{mBC}} + P^{\text{pub}}_{\text{non-mBC}}} {B^{\text{pub}}_{\text{mBC}} + B^{\text{pub}}_{\text{non-mBC}}}, \]
where \(P^{\text{pub}}_e\) is the published number of recurrences avoided for endpoint \(e\), and \(B^{\text{pub}}_e\) is the published no-testing recurrence count for endpoint \(e\). Let \(L = \log(H_0)\).
The age-modifier meta-regression estimates the older-versus-younger log-hazard-ratio contrast:
\[ \delta = \log(H_{\ge 45}) - \log(H_{<45}). \]
Let \(W_a\) be the total olaparib-eligible cohort share in age stratum \(a\):
\[ W_a = \frac{\sum_s N_s w_{s,a}}{\sum_s N_s}. \]
The age-specific hazard ratios are centered around the published all-age effect:
\[ \log(H_{<45}) = L - W_{\ge 45}\delta, \]
\[ \log(H_{\ge 45}) = L + W_{<45}\delta. \]
This construction preserves both the estimated age contrast and the all-age anchor:
\[ \log(H_{\ge 45}) - \log(H_{<45}) = \delta, \]
\[ W_{<45}\log(H_{<45}) + W_{\ge 45}\log(H_{\ge 45}) = L. \]
For the age-stratified treatment-effect analysis, expected olaparib-treated recurrences are:
\[ T_{s,a,e} = B_{s,a,e} H_a. \]
Recurrences avoided are therefore:
\[ P_{s,a,e} = B_{s,a,e} - T_{s,a,e} = B_{s,a,e}(1 - H_a). \]
Aggregate age-stratified recurrence outputs are:
\[ P_{a,e} = \sum_s P_{s,a,e}. \]
The homogeneous-effect counterfactual used in the HTE communication section applies the same all-age hazard ratio \(H_0\) to every age stratum:
\[ P^{\text{hom}}_{s,a,e} = B_{s,a,e}(1 - H_0). \]
This counterfactual keeps age-specific life expectancy but removes age-specific treatment-effect heterogeneity.
The published model gives an aggregate life-year gain per recurrence avoided:
\[ c_0 = \frac{Y^{\text{pub}}} {P^{\text{pub}}_{\text{mBC}} + P^{\text{pub}}_{\text{non-mBC}}}, \]
where \(Y^{\text{pub}}\) is the published total life-years gained.
Let \(\ell_a\) be the remaining life expectancy for the representative age of stratum \(a\), taken from the US female life table. The age-specific life-year gain per recurrence avoided is scaled from \(c_0\) by relative remaining life expectancy:
\[ c_a = c_0 \frac{\ell_a}{\sum_a W_a \ell_a}. \]
Finally, life-years saved in age stratum \(a\) are:
\[ Y_a = c_a \sum_e P_{a,e}. \]
This life-year translation is directional: it is designed to illustrate how age-specific recurrence prevention changes expected population impact, not to replace a disease-specific survival model.
The local meta-regression estimates an older-versus-younger age contrast of 0.2073 on the log-hazard-ratio scale, where >=45 years has the higher log-HR and therefore the smaller recurrence benefit. The deterministic base case uses the point estimate and communicates the uncertainty separately: standard error 0.0984, model p-value 0.0352, and permutation-test p-value 0.0604.
published_total_recurrences_no_testing <-
scalar_input("published_mbc_no_testing") +
scalar_input("published_non_mbc_no_testing")
published_total_recurrences_prevented <-
scalar_input("published_mbc_prevented") +
scalar_input("published_non_mbc_prevented")
base_recurrence_hr <- 1 - (
published_total_recurrences_prevented /
published_total_recurrences_no_testing
)
age_distribution <- model_inputs |>
filter(category == "age_distribution") |>
transmute(
subtype,
age_group,
age_share = as.numeric(value)
)
age_weights <- subtype_inputs |>
select(subtype, patients) |>
inner_join(age_distribution, by = "subtype") |>
group_by(age_group) |>
summarise(
patients = sum(patients * age_share),
cohort_share = patients / sum(subtype_inputs$patients),
.groups = "drop"
) |>
arrange(factor(age_group, levels = c("<45", ">=45")))
delta_log_hr <- scalar_input("age_contrast_log_hr")
weight_young <- age_weights$cohort_share[age_weights$age_group == "<45"]
weight_older <- age_weights$cohort_share[age_weights$age_group == ">=45"]
base_log_hr <- log(base_recurrence_hr)
age_effects <- age_weights |>
mutate(
log_hazard_ratio = if_else(
age_group == "<45",
base_log_hr - weight_older * delta_log_hr,
base_log_hr + weight_young * delta_log_hr
),
hazard_ratio = exp(log_hazard_ratio),
recurrence_reduction = 1 - hazard_ratio
)
age_calibration_check <- age_effects |>
summarise(
weighted_log_hr = sum(cohort_share * log_hazard_ratio),
base_log_hr = base_log_hr,
difference = weighted_log_hr - base_log_hr,
observed_delta = log_hazard_ratio[age_group == ">=45"] -
log_hazard_ratio[age_group == "<45"]
)
kable(
age_effects |>
transmute(
`Age group` = age_group,
`Cohort share` = fmt_pct(cohort_share),
`Log-HR` = fmt_decimal(log_hazard_ratio, 3),
HR = fmt_decimal(hazard_ratio, 3),
`Relative recurrence reduction` = fmt_pct(recurrence_reduction)
),
align = c("l", "r", "r", "r", "r")
)| Age group | Cohort share | Log-HR | HR | Relative recurrence reduction |
|---|---|---|---|---|
| <45 | 50.0% | -0.356 | 0.700 | 30.0% |
| >=45 | 50.0% | -0.149 | 0.861 | 13.9% |
Calibration check: the weighted log-HR is -0.252817, the base log-HR is -0.252817, and the age contrast is 0.2073.
The age-stratified extension uses subtype-specific untreated recurrence rates from the published model, then applies the age-specific olaparib recurrence HRs above. This keeps subtype-specific baseline recurrence behavior while making treatment benefit age-stratified.
baseline_recurrence_rates <- subtype_inputs |>
select(subtype, patients, no_mbc_rate, no_non_mbc_rate) |>
pivot_longer(
cols = c(no_mbc_rate, no_non_mbc_rate),
names_to = "endpoint",
values_to = "baseline_rate"
) |>
mutate(
endpoint = recode(
endpoint,
no_mbc_rate = "mBC recurrences",
no_non_mbc_rate = "non-mBC recurrences"
)
)
age_recurrence_cells <- baseline_recurrence_rates |>
inner_join(age_distribution, by = "subtype") |>
inner_join(
age_effects |> select(age_group, hazard_ratio, recurrence_reduction),
by = "age_group"
) |>
mutate(
baseline_recurrences = patients * age_share * baseline_rate,
olaparib_recurrences = baseline_recurrences * hazard_ratio,
recurrences_prevented = baseline_recurrences - olaparib_recurrences
)
age_recurrence_summary <- age_recurrence_cells |>
group_by(age_group, endpoint) |>
summarise(
baseline_recurrences = sum(baseline_recurrences),
olaparib_recurrences = sum(olaparib_recurrences),
recurrences_prevented = sum(recurrences_prevented),
.groups = "drop"
)
age_output_wide <- age_recurrence_summary |>
select(age_group, endpoint, recurrences_prevented) |>
pivot_wider(names_from = endpoint, values_from = recurrences_prevented)
kable(
age_recurrence_summary |>
transmute(
`Age group` = age_group,
Endpoint = endpoint,
`No testing recurrences` = fmt_count(baseline_recurrences),
`Testing and olaparib recurrences` = fmt_count(olaparib_recurrences),
`Recurrences avoided` = fmt_count(recurrences_prevented)
),
align = c("l", "l", "r", "r", "r")
)| Age group | Endpoint | No testing recurrences | Testing and olaparib recurrences | Recurrences avoided |
|---|---|---|---|---|
| <45 | mBC recurrences | 610 | 427 | 183 |
| <45 | non-mBC recurrences | 152 | 106 | 46 |
| >=45 | mBC recurrences | 610 | 525 | 85 |
| >=45 | non-mBC recurrences | 152 | 131 | 21 |
ggplot(
age_recurrence_summary,
aes(x = age_group, y = recurrences_prevented, fill = endpoint)
) +
geom_col(position = "dodge", width = 0.7) +
scale_y_continuous(labels = comma) +
scale_fill_manual(values = c("#1b7f79", "#be6a15")) +
labs(
x = "Age group",
y = "Recurrences avoided",
fill = "Endpoint",
title = "Exploratory age-stratified recurrence impact"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "bottom",
panel.grid.minor = element_blank()
)The published base case reports 8,677 life-years gained for 340 total recurrences avoided, or 25.5 life-years per recurrence avoided. This aggregate reconstruction scales that relationship by age-specific remaining life expectancy from the female US life table.
representative_ages <- model_inputs |>
filter(category == "representative_age") |>
transmute(
age_group,
representative_age = as.integer(round(as.numeric(value))),
source_note,
assumption_note
)
life_expectancy_by_age <- representative_ages |>
left_join(
life_table |> select(age, life_expectancy_years),
by = c("representative_age" = "age")
)
reference_life_expectancy <- age_effects |>
select(age_group, cohort_share) |>
left_join(life_expectancy_by_age, by = "age_group") |>
summarise(
value = sum(cohort_share * life_expectancy_years)
) |>
pull(value)
base_life_years_per_recurrence <-
scalar_input("published_life_years_gained") /
published_total_recurrences_prevented
life_year_translation <- age_effects |>
select(age_group, cohort_share) |>
left_join(life_expectancy_by_age, by = "age_group") |>
mutate(
life_years_per_recurrence =
base_life_years_per_recurrence *
life_expectancy_years /
reference_life_expectancy
)
life_years_by_age <- age_recurrence_summary |>
group_by(age_group) |>
summarise(
total_recurrences_prevented = sum(recurrences_prevented),
.groups = "drop"
) |>
left_join(life_year_translation, by = "age_group") |>
mutate(
life_years_saved = total_recurrences_prevented * life_years_per_recurrence
)
kable(
life_year_translation |>
transmute(
`Age group` = age_group,
`Representative age` = representative_age,
`Female life expectancy` = fmt_years(life_expectancy_years),
`Life-years per recurrence avoided` = fmt_years(life_years_per_recurrence)
),
align = c("l", "r", "r", "r")
)| Age group | Representative age | Female life expectancy | Life-years per recurrence avoided |
|---|---|---|---|
| <45 | 42 | 40.8 | 30.2 |
| >=45 | 56 | 28.2 | 20.8 |
main_outputs <- age_output_wide |>
left_join(
life_years_by_age |> select(age_group, life_years_saved),
by = "age_group"
)
main_outputs <- bind_rows(
main_outputs,
main_outputs |>
summarise(
age_group = "Total",
`mBC recurrences` = sum(`mBC recurrences`),
`non-mBC recurrences` = sum(`non-mBC recurrences`),
life_years_saved = sum(life_years_saved)
)
)
kable(
main_outputs |>
transmute(
`Age group` = age_group,
`mBC recurrences avoided` = fmt_count(`mBC recurrences`),
`non-mBC recurrences avoided` = fmt_count(`non-mBC recurrences`),
`Life-years saved` = fmt_count(life_years_saved)
),
align = c("l", "r", "r", "r")
)| Age group | mBC recurrences avoided | non-mBC recurrences avoided | Life-years saved |
|---|---|---|---|
| <45 | 183 | 46 | 6,899 |
| >=45 | 85 | 21 | 2,201 |
| Total | 267 | 67 | 9,100 |
subtype_age_detail <- age_recurrence_cells |>
group_by(subtype, age_group, endpoint) |>
summarise(recurrences_prevented = sum(recurrences_prevented), .groups = "drop") |>
pivot_wider(names_from = endpoint, values_from = recurrences_prevented)
kable(
subtype_age_detail |>
transmute(
Subtype = subtype,
`Age group` = age_group,
`mBC recurrences avoided` = fmt_count(`mBC recurrences`),
`non-mBC recurrences avoided` = fmt_count(`non-mBC recurrences`)
),
align = c("l", "l", "r", "r")
)| Subtype | Age group | mBC recurrences avoided | non-mBC recurrences avoided |
|---|---|---|---|
| HR-positive | <45 | 88 | 22 |
| HR-positive | >=45 | 41 | 10 |
| TNBC | <45 | 95 | 24 |
| TNBC | >=45 | 44 | 11 |
The age-stratified treatment-effect estimates are used here to improve population-impact estimation, not to define an age-based treatment rule. In this first-pass analysis, olaparib eligibility and treatment uptake are unchanged: eligible patients in both age groups remain candidates for olaparib, the >=45 years stratum still has a positive predicted recurrence reduction, and no better alternative treatment strategy for >=45 years is modeled.
Two distinct age-related mechanisms affect projected life-years saved. First, if recurrence is avoided earlier in life, more remaining life-years can be preserved even when the treatment effect is identical at every age. Second, if treatment benefit is heterogeneous by age, the model avoids a different number of recurrences in each age group before life expectancy is even applied. The counterfactual below separates those mechanisms.
lt45_impact_share <- main_outputs |>
filter(age_group == "<45") |>
transmute(
recurrence_share =
(`mBC recurrences` + `non-mBC recurrences`) /
sum(main_outputs$`mBC recurrences`[main_outputs$age_group != "Total"] +
main_outputs$`non-mBC recurrences`[main_outputs$age_group != "Total"]),
life_year_share =
life_years_saved /
sum(main_outputs$life_years_saved[main_outputs$age_group != "Total"])
)
communication_table <- tibble(
Question = c(
"Does this change who should receive olaparib?",
"Does this change predicted recurrences avoided?",
"Does this change predicted life-years saved?",
"Does this help when applying the model to another population?",
"Does this change how uncertainty should be communicated?",
"Does this show value in collecting subgroup evidence?"
),
Answer = c(
"No. The analysis is not an age-based rule for withholding treatment from eligible patients in the >=45 years stratum.",
"Yes. The same all-age treatment benefit is redistributed across age strata according to the calibrated treatment-effect contrast.",
"Yes. Life-years depend on both recurrence reductions and remaining life expectancy, so age-stratified effects can materially change the life-year estimate.",
"Yes. A population with a different age mix may not have the same all-age population impact, even under the same treatment strategy.",
"Yes. The report can separate the published-model benchmark from the uncertain age-stratified extension.",
"Yes. Subgroup evidence can improve population-impact estimates even when it does not justify a different treatment rule."
)
)
kable(communication_table, align = c("l", "l"))| Question | Answer |
|---|---|
| Does this change who should receive olaparib? | No. The analysis is not an age-based rule for withholding treatment from eligible patients in the >=45 years stratum. |
| Does this change predicted recurrences avoided? | Yes. The same all-age treatment benefit is redistributed across age strata according to the calibrated treatment-effect contrast. |
| Does this change predicted life-years saved? | Yes. Life-years depend on both recurrence reductions and remaining life expectancy, so age-stratified effects can materially change the life-year estimate. |
| Does this help when applying the model to another population? | Yes. A population with a different age mix may not have the same all-age population impact, even under the same treatment strategy. |
| Does this change how uncertainty should be communicated? | Yes. The report can separate the published-model benchmark from the uncertain age-stratified extension. |
| Does this show value in collecting subgroup evidence? | Yes. Subgroup evidence can improve population-impact estimates even when it does not justify a different treatment rule. |
homogeneous_recurrence_summary <- age_recurrence_cells |>
group_by(age_group, endpoint) |>
summarise(
baseline_recurrences = sum(baseline_recurrences),
.groups = "drop"
) |>
mutate(
recurrences_prevented = baseline_recurrences * (1 - base_recurrence_hr)
)
homogeneous_life_years_by_age <- homogeneous_recurrence_summary |>
group_by(age_group) |>
summarise(
homogeneous_recurrences_prevented = sum(recurrences_prevented),
.groups = "drop"
) |>
left_join(
life_year_translation |> select(age_group, life_years_per_recurrence),
by = "age_group"
) |>
mutate(
homogeneous_life_years_saved =
homogeneous_recurrences_prevented * life_years_per_recurrence
)
hte_decomposition <- life_years_by_age |>
select(
age_group,
hte_recurrences_prevented = total_recurrences_prevented,
hte_life_years_saved = life_years_saved
) |>
left_join(homogeneous_life_years_by_age, by = "age_group") |>
mutate(
recurrence_increment_from_hte =
hte_recurrences_prevented - homogeneous_recurrences_prevented,
life_year_increment_from_hte =
hte_life_years_saved - homogeneous_life_years_saved
)
hte_decomposition_display <- hte_decomposition |>
bind_rows(
hte_decomposition |>
summarise(
age_group = "Total",
hte_recurrences_prevented = sum(hte_recurrences_prevented),
homogeneous_recurrences_prevented =
sum(homogeneous_recurrences_prevented),
recurrence_increment_from_hte =
sum(recurrence_increment_from_hte),
hte_life_years_saved = sum(hte_life_years_saved),
homogeneous_life_years_saved =
sum(homogeneous_life_years_saved),
life_year_increment_from_hte =
sum(life_year_increment_from_hte)
)
)
lt45_homogeneous_life_year_share <- hte_decomposition |>
filter(age_group == "<45") |>
summarise(
share = homogeneous_life_years_saved /
sum(hte_decomposition$homogeneous_life_years_saved)
) |>
pull(share)
lt45_life_year_share_increment_from_hte <-
lt45_impact_share$life_year_share - lt45_homogeneous_life_year_share
lt45_recurrence_increment_from_hte <- hte_decomposition |>
filter(age_group == "<45") |>
pull(recurrence_increment_from_hte)
total_life_year_increment_from_hte <- hte_decomposition |>
summarise(total = sum(life_year_increment_from_hte)) |>
pull(total)
kable(
hte_decomposition_display |>
transmute(
`Age group` = age_group,
`Recurrences avoided with homogeneous effect` =
fmt_count(homogeneous_recurrences_prevented),
`Recurrences avoided with age-specific effect` =
fmt_count(hte_recurrences_prevented),
`Recurrence difference from HTE` =
fmt_count(recurrence_increment_from_hte),
`Life-years saved with homogeneous effect` =
fmt_count(homogeneous_life_years_saved),
`Life-years saved with age-specific effect` =
fmt_count(hte_life_years_saved),
`Life-year difference from HTE` =
fmt_count(life_year_increment_from_hte)
),
align = c("l", "r", "r", "r", "r", "r", "r")
)| Age group | Recurrences avoided with homogeneous effect | Recurrences avoided with age-specific effect | Recurrence difference from HTE | Life-years saved with homogeneous effect | Life-years saved with age-specific effect | Life-year difference from HTE |
|---|---|---|---|---|---|---|
| <45 | 170 | 228 | 58 | 5,139 | 6,899 | 1,759 |
| >=45 | 170 | 106 | -65 | 3,548 | 2,201 | -1,347 |
| Total | 340 | 334 | -6 | 8,688 | 9,100 | 412 |
In this run, <45 years represents 50.0% of the modeled eligible population. With age-specific life expectancy but a homogeneous treatment effect, that group accounts for 59.2% of life-years saved. With age-specific treatment effects, it accounts for 68.4% of avoided recurrences and 75.8% of life-years saved. That is an additional 16.7 percentage points shift toward <45 years attributable to treatment-effect heterogeneity, over and above the fact that younger patients have more life-years left. In counts, the age-specific treatment effect moves about 58 additional avoided recurrences into <45 years and adds 412 life-years in this aggregate approximation.
The base-case age contrast is a single point estimate from an underpowered, marginally significant meta-regression (see Age-Modifier Calibration). This section recomputes the age-stratified recurrence and life-year pipeline at the lower and upper bounds of the age contrast’s 95% confidence interval, and at a null (no-heterogeneity) benchmark, to show how much of the reported <45 years shift depends on the exact point estimate used in the base case.
run_age_scenario <- function(delta_log_hr) {
log_h_lt45 <- base_log_hr - weight_older * delta_log_hr
log_h_ge45 <- base_log_hr + weight_young * delta_log_hr
scenario_age_effects <- age_weights |>
mutate(
log_hazard_ratio = if_else(age_group == "<45", log_h_lt45, log_h_ge45),
hazard_ratio = exp(log_hazard_ratio)
)
scenario_cells <- baseline_recurrence_rates |>
inner_join(age_distribution, by = "subtype", relationship = "many-to-many") |>
inner_join(
scenario_age_effects |> select(age_group, hazard_ratio),
by = "age_group"
) |>
mutate(
baseline_recurrences = patients * age_share * baseline_rate,
recurrences_prevented = baseline_recurrences * (1 - hazard_ratio)
)
scenario_recurrence_summary <- scenario_cells |>
group_by(age_group) |>
summarise(recurrences_prevented = sum(recurrences_prevented), .groups = "drop")
scenario_reference_le <- scenario_age_effects |>
select(age_group, cohort_share) |>
left_join(life_expectancy_by_age, by = "age_group") |>
summarise(value = sum(cohort_share * life_expectancy_years)) |>
pull(value)
scenario_life_years <- scenario_age_effects |>
select(age_group, cohort_share) |>
left_join(life_expectancy_by_age, by = "age_group") |>
mutate(
life_years_per_recurrence =
base_life_years_per_recurrence * life_expectancy_years / scenario_reference_le
) |>
left_join(scenario_recurrence_summary, by = "age_group") |>
mutate(life_years_saved = recurrences_prevented * life_years_per_recurrence)
tibble(
lt45_hazard_ratio =
scenario_age_effects$hazard_ratio[scenario_age_effects$age_group == "<45"],
ge45_hazard_ratio =
scenario_age_effects$hazard_ratio[scenario_age_effects$age_group == ">=45"],
lt45_recurrence_share =
scenario_recurrence_summary$recurrences_prevented[
scenario_recurrence_summary$age_group == "<45"
] / sum(scenario_recurrence_summary$recurrences_prevented),
lt45_life_year_share =
scenario_life_years$life_years_saved[scenario_life_years$age_group == "<45"] /
sum(scenario_life_years$life_years_saved),
total_life_years_saved = sum(scenario_life_years$life_years_saved)
)
}
sensitivity_scenarios <- tibble(
scenario = c(
"No heterogeneity (life expectancy only)",
"Age contrast: lower 95% CI bound",
"Age contrast: point estimate (base case)",
"Age contrast: upper 95% CI bound"
),
delta_log_hr = c(
0,
scalar_input("age_contrast_ci_low"),
scalar_input("age_contrast_log_hr"),
scalar_input("age_contrast_ci_high")
)
)
sensitivity_scenarios <- bind_cols(
sensitivity_scenarios,
bind_rows(lapply(sensitivity_scenarios$delta_log_hr, run_age_scenario))
) |>
mutate(
life_year_share_increment =
lt45_life_year_share -
lt45_life_year_share[scenario == "No heterogeneity (life expectancy only)"]
)
kable(
sensitivity_scenarios |>
transmute(
Scenario = scenario,
`Log-HR contrast` = fmt_decimal(delta_log_hr, 4),
`HR <45` = fmt_decimal(lt45_hazard_ratio, 3),
`HR >=45` = fmt_decimal(ge45_hazard_ratio, 3),
`<45 recurrence share` = fmt_pct(lt45_recurrence_share),
`<45 life-year share` = fmt_pct(lt45_life_year_share),
`Shift vs. no heterogeneity` = fmt_pp(life_year_share_increment)
),
align = c("l", "r", "r", "r", "r", "r", "r")
)| Scenario | Log-HR contrast | HR <45 | HR >=45 | <45 recurrence share | <45 life-year share | Shift vs. no heterogeneity |
|---|---|---|---|---|---|---|
| No heterogeneity (life expectancy only) | 0.0000 | 0.777 | 0.777 | 50.0% | 59.2% | 0.0 percentage points |
| Age contrast: lower 95% CI bound | 0.0144 | 0.771 | 0.782 | 51.3% | 60.4% | 1.2 percentage points |
| Age contrast: point estimate (base case) | 0.2073 | 0.700 | 0.861 | 68.4% | 75.8% | 16.7 percentage points |
| Age contrast: upper 95% CI bound | 0.4002 | 0.636 | 0.949 | 87.6% | 91.1% | 32.0 percentage points |
At the base-case point estimate, treatment-effect heterogeneity shifts an additional 16.7 percentage points of life-years saved toward <45 years, on top of the life-expectancy-only baseline. At the lower bound of the age contrast’s 95% confidence interval, that additional shift falls to 1.2 percentage points — close to zero — while at the upper bound it grows to 32.0 percentage points. Because the age contrast comes from a meta-regression of 21 observations drawn from only 9 trials (model p = 0.0352; permutation p = 0.0604), the treatment-effect-heterogeneity component of the <45 years life-year shift should be read as this wide range rather than as the fixed point-estimate value reported above.
The published Kasle et al. model is not age-stratified: it uses a single representative age (42, the OlympiA trial average) for the whole eligible cohort’s lifetime horizon, so there is no <45/>=45 population split in the original paper to reproduce. The 10%/90% split used in an earlier version of this exploratory extension was this project’s own first-pass proxy (general-population ACS statistics), not a published input. The Published Model Benchmark section above is unaffected by any age-distribution assumption and remains the directly comparable-to-Kasle output.
This section instead compares the exploratory age-stratified extension’s own outputs under that earlier general-population proxy and the current gBRCA-matched proxy, to make clear how much of the extension’s conclusions follow from this replaceable assumption rather than from the age-contrast calibration itself.
run_proxy_scenario <- function(lt45_share, representative_age_ge45_years) {
alt_age_distribution <- tibble(
subtype = rep(c("HR-positive", "TNBC"), each = 2),
age_group = rep(c("<45", ">=45"), times = 2),
age_share = rep(c(lt45_share, 1 - lt45_share), times = 2)
)
alt_age_weights <- subtype_inputs |>
select(subtype, patients) |>
inner_join(alt_age_distribution, by = "subtype") |>
group_by(age_group) |>
summarise(
patients = sum(patients * age_share),
cohort_share = patients / sum(subtype_inputs$patients),
.groups = "drop"
)
alt_weight_young <- alt_age_weights$cohort_share[alt_age_weights$age_group == "<45"]
alt_weight_older <- alt_age_weights$cohort_share[alt_age_weights$age_group == ">=45"]
alt_age_effects <- alt_age_weights |>
mutate(
log_hazard_ratio = if_else(
age_group == "<45",
base_log_hr - alt_weight_older * delta_log_hr,
base_log_hr + alt_weight_young * delta_log_hr
),
hazard_ratio = exp(log_hazard_ratio)
)
alt_cells <- baseline_recurrence_rates |>
inner_join(alt_age_distribution, by = "subtype", relationship = "many-to-many") |>
inner_join(
alt_age_effects |> select(age_group, hazard_ratio),
by = "age_group"
) |>
mutate(
baseline_recurrences = patients * age_share * baseline_rate,
recurrences_prevented = baseline_recurrences * (1 - hazard_ratio)
)
alt_recurrence_summary <- alt_cells |>
group_by(age_group) |>
summarise(recurrences_prevented = sum(recurrences_prevented), .groups = "drop")
alt_life_expectancy_by_age <- tibble(
age_group = c("<45", ">=45"),
representative_age = c(
as.integer(round(scalar_input("representative_age_lt45"))),
as.integer(round(representative_age_ge45_years))
)
) |>
left_join(life_table |> select(age, life_expectancy_years), by = c("representative_age" = "age"))
alt_reference_le <- alt_age_effects |>
select(age_group, cohort_share) |>
left_join(alt_life_expectancy_by_age, by = "age_group") |>
summarise(value = sum(cohort_share * life_expectancy_years)) |>
pull(value)
alt_life_years <- alt_age_effects |>
select(age_group, cohort_share) |>
left_join(alt_life_expectancy_by_age, by = "age_group") |>
mutate(
life_years_per_recurrence =
base_life_years_per_recurrence * life_expectancy_years / alt_reference_le
) |>
left_join(alt_recurrence_summary, by = "age_group") |>
mutate(life_years_saved = recurrences_prevented * life_years_per_recurrence)
tibble(
lt45_population_share = alt_weight_young,
lt45_recurrence_share =
alt_recurrence_summary$recurrences_prevented[alt_recurrence_summary$age_group == "<45"] /
sum(alt_recurrence_summary$recurrences_prevented),
lt45_life_year_share =
alt_life_years$life_years_saved[alt_life_years$age_group == "<45"] /
sum(alt_life_years$life_years_saved),
total_life_years_saved = sum(alt_life_years$life_years_saved)
)
}
distribution_scenarios <- tibble(
scenario = c(
"Previous version: ACS general-population proxy (10% / 90%, representative age 63)",
"Current base case: gBRCA-matched proxy (Morganti et al. 2024; 50% / 50%, representative age 56)"
),
lt45_share = c(0.10, 0.50),
representative_age_ge45_years = c(63, 56)
)
distribution_scenarios <- bind_cols(
distribution_scenarios,
bind_rows(
Map(
run_proxy_scenario,
distribution_scenarios$lt45_share,
distribution_scenarios$representative_age_ge45_years
)
)
)
kable(
distribution_scenarios |>
transmute(
Scenario = scenario,
`<45 population share` = fmt_pct(lt45_population_share),
`<45 recurrence share` = fmt_pct(lt45_recurrence_share),
`<45 life-year share` = fmt_pct(lt45_life_year_share),
`Total life-years saved` = fmt_count(total_life_years_saved)
),
align = c("l", "r", "r", "r", "r")
)| Scenario | <45 population share | <45 recurrence share | <45 life-year share | Total life-years saved |
|---|---|---|---|---|
| Previous version: ACS general-population proxy (10% / 90%, representative age 63) | 10.0% | 16.0% | 25.9% | 9,030 |
| Current base case: gBRCA-matched proxy (Morganti et al. 2024; 50% / 50%, representative age 56) | 50.0% | 68.4% | 75.8% | 9,100 |
Under the previous ACS-based proxy, <45 years accounted for 25.9% of life-years saved; under the current gBRCA-matched proxy, that rises to 75.8%. Both figures use the same base-case age contrast (0.2073); the difference between them is attributable entirely to the age-distribution and representative-age assumptions, not to the treatment-effect calibration. Because the age-distribution proxy is now matched on gBRCA1/2 status rather than drawn from the general breast-cancer population, the current base case is judged more representative of this model’s population — but readers who want the prior version’s assumptions for their own comparison can reconstruct it from this table.
Under the first-pass age-distribution proxy, the calibrated age-specific recurrence HR is lower for <45 years than for >=45 years, so more benefit concentrates in the younger stratum than its population share alone would imply. The total age-stratified recurrence outputs remain close to the published aggregate benchmark because the age contrast is centered around the published all-age recurrence effect on the log-HR scale.
The life-year output is more exploratory than the recurrence output. It combines the published aggregate life-year-per-recurrence relationship with female general-population life expectancy, so it should be treated as a directional translation rather than a disease-specific survival projection. The main interpretive claim is therefore about estimating expected population impact more accurately and transparently, not changing olaparib treatment recommendations by age.
<45/>=45 split or subtype-specific age moments, so 50%/50% is a round approximation rather than a directly reported figure. It is also not restricted to the olaparib-eligible subgroup specifically, which had a younger median age (39) in the same source.representative_age_ge45 is now derived (56) from the same Morganti et al. 2024 mean/SD used for the age-distribution proxy, via a normal-distribution approximation, rather than a directly reported figure. representative_age_lt45 (42) is kept from the OlympiA trial average because it is directly reported rather than derived; applying the same normal-approximation method used for representative_age_ge45 to the <45 stratum would instead give about 34.5, so the two representative ages currently rest on different methods and a single consistent source would be an improvement.<45 years life-year shift depends on this estimate’s confidence interval.1 - HR as an aggregate approximation.