Breast Cancer Molecular Subtypes & IHC Distribution — Kenyan Sites

Author

Stellamaries Syombua

Published

May 7, 2026

1 Load Data

2 Filter to Kenyan Sites

Code
#  Kenyan Study sites

kenyan_sites <- c("Kenya-MERU", "Kenya-KTRH", "Kenya-NTRH")

#  Breast cancer DSQ (Kenyan only)
breast_kenya <- breast_cancer_dsq_data %>%
  filter(`Study Center` %in% kenyan_sites)

cat("Kenyan breast cancer participants (DSQ):", n_distinct(breast_kenya$`Participant Code`), "\n")
Kenyan breast cancer participants (DSQ): 167 
Code
breast_kenya %>%
  summarise(n = n_distinct(`Participant Code`), .by = `Study Center`)
  Study Center  n
1   Kenya-MERU 90
2   Kenya-KTRH 30
3   Kenya-NTRH 47

3 IHC Data Preparation

Code
#  Merge IHC markers with registration to get Study Center


ihc_kenya <- Breast_IHC_data %>%
  left_join(
    taca_registration %>%
      dplyr::select(`Participant Code`, `Study Center`, `Study Country`),
    by = "Participant Code"
  ) %>%
  filter(`Study Center` %in% kenyan_sites)

cat("Kenyan IHC records:", nrow(ihc_kenya), "\n")
Kenyan IHC records: 144 
Code
#  Harmonise IHC result values to Positive / Negative


harmonise_ihc <- function(x) {
  x_clean <- str_trim(tolower(as.character(x)))
  case_when(
    x_clean %in% c("positive", "pos", "yes", "1", "2", "3",
                   "2+", "3+", "1+", "reactive", "detected")    ~ "Positive",
    x_clean %in% c("negative", "neg", "no", "0", "0+",
                   "non-reactive", "not detected")               ~ "Negative",
    x_clean %in% c("equivocal", "borderline", "2+ equivocal")   ~ "Equivocal",
    TRUE                                                         ~ NA_character_
  )
}

ihc_kenya <- ihc_kenya %>%
  mutate(
    ER_status  = harmonise_ihc(ER),
    PR_status  = harmonise_ihc(PR),
    HER2_status = harmonise_ihc(HER2)
  )

# Quick check of harmonised values
ihc_kenya %>%
  summarise(
    ER_n   = sum(!is.na(ER_status)),
    PR_n   = sum(!is.na(PR_status)),
    HER2_n = sum(!is.na(HER2_status)),
    .by = `Study Center`
  )
# A tibble: 3 × 4
  `Study Center`  ER_n  PR_n HER2_n
  <chr>          <int> <int>  <int>
1 Kenya-MERU        79    79     80
2 Kenya-KTRH        15    15     15
3 Kenya-NTRH        45    43     43

4 Molecular Subtype Classification

Code
#  Surrogate molecular subtypes

ihc_kenya <- ihc_kenya %>%
  mutate(
    HR_positive = ER_status == "Positive" | PR_status == "Positive",
    Subtype = case_when(
      # Need all three markers to classify
      is.na(ER_status) | is.na(PR_status) | is.na(HER2_status) ~ NA_character_,
      # HER2-enriched equivocal — treat as indeterminate
      HER2_status == "Equivocal"                                ~ "Indeterminate",
      # Luminal A : HR+, HER2-
       HR_positive & HER2_status == "Negative"                  ~ "Luminal A",
      # Luminal B : HR+, HER2+
       HR_positive & HER2_status == "Positive"                  ~ "Luminal B",
      # HER2-enriched : HR-, HER2+
      !HR_positive & HER2_status == "Positive"                  ~ "HER2-enriched",
      # TNBC : HR-, HER2-
      !HR_positive & HER2_status == "Negative"                  ~ "Triple Negative",
      TRUE                                                       ~ NA_character_
    ),
    Subtype = factor(
      Subtype,
      levels = c("Luminal A", "Luminal B", "HER2-enriched", "Triple Negative", "Indeterminate")
    )
  )

cat("Subtype classification summary:\n")
Subtype classification summary:
Code
print(table(ihc_kenya$Subtype, useNA = "ifany"))

      Luminal A       Luminal B   HER2-enriched Triple Negative   Indeterminate 
             66              19              12              26              11 
           <NA> 
             10 

5 Results

5.1 Overall Molecular Subtype Distribution

Code
#  Table — overall subtype counts & percentages

subtype_overall <- ihc_kenya %>%
  filter(!is.na(Subtype)) %>%
  count(Subtype, name = "n") %>%
  mutate(
    Percentage = round(100 * n / sum(n), 1),
    `n (%)`    = paste0(n, " (", Percentage, "%)")
  )

subtype_overall %>%
  dplyr::select(Subtype, n, `n (%)`) %>%
  gt() %>%
  tab_header(
    title    = "Molecular Subtype Distribution",
    subtitle = "Kenyan sites combined"
  ) %>%
  cols_label(
    Subtype = "Molecular Subtype",
    n       = "Count",
    `n (%)`  = "n (%)"
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  grand_summary_rows(
    columns = n,
    fns     = list(Total = ~sum(.)),
    fmt     = ~ fmt_integer(.)
  ) %>%
  opt_stylize(style = 6, color = "blue")
Molecular Subtype Distribution
Kenyan sites combined
Molecular Subtype Count n (%)
Luminal A 66 66 (49.3%)
Luminal B 19 19 (14.2%)
HER2-enriched 12 12 (9%)
Triple Negative 26 26 (19.4%)
Indeterminate 11 11 (8.2%)
Total 134
Code
#  Plot — overall subtype bar chart

subtype_colours <- c(
  "Luminal A"       = "#2196F3",
  "Luminal B"       = "#4CAF50",
  "HER2-enriched"   = "#FF9800",
  "Triple Negative"  = "#F44336",
  "Indeterminate"    = "#9E9E9E"
)

p_overall <- subtype_overall %>%
  ggplot(aes(x = fct_reorder(Subtype, n), y = n, fill = Subtype)) +
  geom_col(width = 0.65, show.legend = FALSE) +
  geom_text(aes(label = paste0(n, "\n(", Percentage, "%)")),
            hjust = -0.1, size = 3.5, lineheight = 0.9) +
  scale_fill_manual(values = subtype_colours) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
  coord_flip() +
  labs(
    title    = "Overall Molecular Subtype Distribution",
    subtitle = paste0("n = ", sum(subtype_overall$n), " classified patients"),
    x        = NULL,
    y        = "Number of Patients"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title    = element_text(face = "bold"),
    plot.subtitle = element_text(colour = "grey40"),
    panel.grid = element_blank(),
    panel.grid.major = element_blank()
  )

p_overall

Overall molecular subtype distribution across all Kenyan sites

5.2 Subtype Distribution by Kenyan Site

Code
#  Cross-tabulation: Subtype × Site

subtype_site <- ihc_kenya %>%
  filter(!is.na(Subtype)) %>%
  count(`Study Center`, Subtype) %>%
  group_by(`Study Center`) %>%
  mutate(
    site_total = sum(n),
    Pct        = round(100 * n / site_total, 1),
    `n (%)`    = paste0(n, " (", Pct, "%)")
  ) %>%
  ungroup()

subtype_site %>%
  dplyr::select(`Study Center`, Subtype, n, `n (%)`) %>%
  gt(groupname_col = "Study Center") %>%
  tab_header(
    title    = "Molecular Subtype Distribution by Kenyan Site"
  ) %>%
  cols_label(
    Subtype  = "Molecular Subtype",
    n        = "Count",
    `n (%)` = "n (%)"
  ) %>%
  tab_style(
    style     = cell_text(weight = "bold"),
    locations = cells_row_groups()
  ) %>%
  tab_style(
    style     = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  opt_stylize(style = 6, color = "blue")
Molecular Subtype Distribution by Kenyan Site
Molecular Subtype Count n (%)
Kenya-KTRH
Luminal A 6 6 (40%)
Luminal B 3 3 (20%)
HER2-enriched 2 2 (13.3%)
Triple Negative 2 2 (13.3%)
Indeterminate 2 2 (13.3%)
Kenya-MERU
Luminal A 42 42 (54.5%)
Luminal B 9 9 (11.7%)
HER2-enriched 5 5 (6.5%)
Triple Negative 16 16 (20.8%)
Indeterminate 5 5 (6.5%)
Kenya-NTRH
Luminal A 18 18 (42.9%)
Luminal B 7 7 (16.7%)
HER2-enriched 5 5 (11.9%)
Triple Negative 8 8 (19%)
Indeterminate 4 4 (9.5%)
Code
#  Stacked bar — proportion per site

p_site_stacked <- subtype_site %>%
  ggplot(aes(x = `Study Center`, y = Pct, fill = Subtype)) +
  geom_col(position = "stack", width = 0.6) +
  geom_text(
    aes(label = ifelse(Pct >= 5, paste0(Pct, "%"), "")),
    position = position_stack(vjust = 0.5),
    size = 3.2, colour = "white", fontface = "bold"
  ) +
  scale_fill_manual(values = subtype_colours, name = "Molecular Subtype") +
  scale_y_continuous(labels = label_percent(scale = 1)) +
  labs(
    title    = "Molecular Subtype by Study Site",
    x        = NULL,
    y        = "Percentage (%)"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title       = element_text(face = "bold"),
    plot.subtitle    = element_text(colour = "grey40"),
    legend.position  = "bottom",
    legend.title     = element_text(face = "bold"),
    panel.grid = element_blank(),
    panel.grid.major = element_blank()
  ) +
  guides(fill = guide_legend(nrow = 2))

p_site_stacked

Molecular subtype distribution stratified by Kenyan study site
Code
# counts per site
p_site_facet <- subtype_site %>%
  ggplot(aes(x = Subtype, y = n, fill = Subtype)) +
  geom_col(show.legend = FALSE, width = 0.65) +
  geom_text(aes(label = n), vjust = -0.5, size = 3) +
  scale_fill_manual(values = subtype_colours) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.18))) +
  facet_wrap(~`Study Center`, scales = "free_y") +
  labs(
    title   = "Molecular Subtype Counts",
    x       = NULL,
    y       = "Number of Patients"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title  = element_text(face = "bold"),
    axis.text.x = element_text(angle = 30, hjust = 1),
    panel.grid  = element_blank(),
    panel.grid.major = element_blank(),
    strip.text  = element_text(face = "bold", size = 11)
  )

p_site_facet

Molecular subtype counts per site — faceted view

5.3 IHC Marker Distribution by Kenyan Site

Code
#  Reshape IHC data to long format for per-marker plots
ihc_long <- ihc_kenya %>%
  dplyr::select(`Participant Code`, `Study Center`, ER_status, PR_status, HER2_status) %>%
  pivot_longer(
    cols      = c(ER_status, PR_status, HER2_status),
    names_to  = "Marker",
    values_to = "Result"
  ) %>%
  mutate(
    Marker = recode(Marker,
      "ER_status"   = "ER",
      "PR_status"   = "PR",
      "HER2_status" = "HER2"
    ),
    Marker = factor(Marker, levels = c("ER", "PR", "HER2")),
    Result = factor(Result, levels = c("Positive", "Negative", "Equivocal"))
  ) %>%
  filter(!is.na(Result))
Code
#  Table — IHC marker results by site
ihc_marker_summary <- ihc_long %>%
  count(`Study Center`, Marker, Result) %>%
  group_by(`Study Center`, Marker) %>%
  mutate(
    marker_total = sum(n),
    Pct          = round(100 * n / marker_total, 1),
    `n (%)`      = paste0(n, " (", Pct, "%)")
  ) %>%
  ungroup()

ihc_marker_summary %>%
  dplyr::select(`Study Center`, Marker, Result, n, `n (%)`) %>%
  arrange(`Study Center`, Marker, Result) %>%
  gt(groupname_col = "Study Center") %>%
  tab_header(
    title    = "IHC Marker Distribution by Study Site"
  ) %>%
  cols_label(
    Marker   = "Marker",
    Result   = "Result",
    n        = "Count",
    `n (%)` = "n (%)"
  ) %>%
  tab_style(
    style     = cell_text(weight = "bold"),
    locations = cells_row_groups()
  ) %>%
  tab_style(
    style     = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_fill(color = "#E8F5E9"),
    locations = cells_body(
      columns = Result,
      rows    = Result == "Positive"
    )
  ) %>%
  tab_style(
    style = cell_fill(color = "#FFEBEE"),
    locations = cells_body(
      columns = Result,
      rows    = Result == "Negative"
    )
  ) %>%
  opt_stylize(style = 6, color = "blue")
IHC Marker Distribution by Study Site
Marker Result Count n (%)
Kenya-KTRH
ER Positive 10 10 (66.7%)
ER Negative 5 5 (33.3%)
PR Positive 9 9 (60%)
PR Negative 6 6 (40%)
HER2 Positive 5 5 (33.3%)
HER2 Negative 8 8 (53.3%)
HER2 Equivocal 2 2 (13.3%)
Kenya-MERU
ER Positive 54 54 (68.4%)
ER Negative 25 25 (31.6%)
PR Positive 49 49 (62%)
PR Negative 30 30 (38%)
HER2 Positive 14 14 (17.5%)
HER2 Negative 61 61 (76.2%)
HER2 Equivocal 5 5 (6.2%)
Kenya-NTRH
ER Positive 32 32 (71.1%)
ER Negative 13 13 (28.9%)
PR Positive 29 29 (67.4%)
PR Negative 14 14 (32.6%)
HER2 Positive 13 13 (30.2%)
HER2 Negative 26 26 (60.5%)
HER2 Equivocal 4 4 (9.3%)
Code
# positivity rate per marker per site

ihc_pos_rate <- ihc_marker_summary %>%
  filter(Result == "Positive") %>%
  dplyr::select(`Study Center`, Marker, n, Pct, marker_total)

marker_colours <- c("ER" = "#1565C0", "PR" = "#2E7D32", "HER2" = "#C62828")

p_marker_pos <- ihc_pos_rate %>%
  ggplot(aes(x = `Study Center`, y = Pct, fill = Marker)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.65) +
  geom_errorbar(
    aes(
      ymin = Pct - 1.96 * sqrt(Pct * (100 - Pct) / marker_total),
      ymax = Pct + 1.96 * sqrt(Pct * (100 - Pct) / marker_total)
    ),
    position = position_dodge(width = 0.7),
    width = 0.25, colour = "grey30"
  ) +
  geom_text(
    aes(label = paste0(Pct, "%")),
    position = position_dodge(width = 0.7),
    vjust = -0.7, size = 3
  ) +
  scale_fill_manual(values = marker_colours, name = "IHC Marker") +
  scale_y_continuous(
    limits = c(0, 105),
    labels = label_percent(scale = 1)
  ) +
  labs(
    title    = "IHC Marker Positivity Rates by Site",
    x        = NULL,
    y        = "Positivity Rate (%)"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title       = element_text(face = "bold"),
    plot.subtitle    = element_text(colour = "grey40"),
    legend.position  = "top",
    legend.title     = element_text(face = "bold"),
    panel.grid  = element_blank()
  )

p_marker_pos

IHC marker positivity rates by Kenyan study site
Code
# full result distribution per marker

result_colours <- c(
  "Positive"   = "#1B5E20",
  "Negative"   = "#B71C1C",
  "Equivocal"  = "#F57F17"
)

p_marker_full <- ihc_marker_summary %>%
  ggplot(aes(x = `Study Center`, y = Pct, fill = Result)) +
  geom_col(position = "stack", width = 0.6) +
  geom_text(
    aes(label = ifelse(Pct >= 5, paste0(Pct, "%"), "")),
    position = position_stack(vjust = 0.5),
    size = 3, colour = "white", fontface = "bold"
  ) +
  scale_fill_manual(values = result_colours, name = "IHC Result") +
  scale_y_continuous(labels = label_percent(scale = 1)) +
  facet_wrap(~Marker, ncol = 1) +
  labs(
    title    = "IHC Marker Result Distribution by Kenyan Site",
    x        = NULL,
    y        = "Percentage (%)",
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title       = element_text(face = "bold"),
    plot.subtitle    = element_text(colour = "grey40"),
    legend.position  = "top",
    legend.title     = element_text(face = "bold"),
    strip.text       = element_text(face = "bold", size = 12),
    panel.grid  = element_blank()
  )

p_marker_full

Full IHC result distribution (Positive / Negative / Equivocal) faceted by marker

6 Summary Statistics

Code
# overall characteristics table

ihc_kenya %>%
  filter(!is.na(Subtype)) %>%
  dplyr::select(`Study Center`, Subtype, ER_status, PR_status, HER2_status) %>%
  tbl_summary(
    by        = `Study Center`,
    label     = list(
      Subtype     ~ "Molecular Subtype",
      ER_status   ~ "ER Status",
      PR_status   ~ "PR Status",
      HER2_status ~ "HER2 Status"
    ),
    statistic = all_categorical() ~ "{n} ({p}%)",
    missing   = "no"
  ) %>%
  add_overall(last = FALSE) %>%
  bold_labels() %>%
  modify_header(label = "**Characteristic**") %>%
  modify_caption("**Table 1. IHC and Subtype Summary — Kenyan Sites**")
Table 1. IHC and Subtype Summary — Kenyan Sites
Characteristic Overall
N = 1341
Kenya-KTRH
N = 151
Kenya-MERU
N = 771
Kenya-NTRH
N = 421
Molecular Subtype



    Luminal A 66 (49%) 6 (40%) 42 (55%) 18 (43%)
    Luminal B 19 (14%) 3 (20%) 9 (12%) 7 (17%)
    HER2-enriched 12 (9.0%) 2 (13%) 5 (6.5%) 5 (12%)
    Triple Negative 26 (19%) 2 (13%) 16 (21%) 8 (19%)
    Indeterminate 11 (8.2%) 2 (13%) 5 (6.5%) 4 (9.5%)
ER Status



    Negative 43 (32%) 5 (33%) 25 (32%) 13 (31%)
    Positive 91 (68%) 10 (67%) 52 (68%) 29 (69%)
PR Status



    Negative 50 (37%) 6 (40%) 30 (39%) 14 (33%)
    Positive 84 (63%) 9 (60%) 47 (61%) 28 (67%)
HER2 Status



    Equivocal 11 (8.2%) 2 (13%) 5 (6.5%) 4 (9.5%)
    Negative 92 (69%) 8 (53%) 58 (75%) 26 (62%)
    Positive 31 (23%) 5 (33%) 14 (18%) 12 (29%)
1 n (%)

7 Data Completeness

Code
#  Report missing IHC data per site
ihc_kenya %>%
  group_by(`Study Center`) %>%
  summarise(
    Total_patients  = n(),
    ER_available    = sum(!is.na(ER_status)),
    PR_available    = sum(!is.na(PR_status)),
    HER2_available  = sum(!is.na(HER2_status)),
    All_3_available = sum(!is.na(ER_status) & !is.na(PR_status) & !is.na(HER2_status)),
    Subtype_classified = sum(!is.na(Subtype)),
    .groups = "drop"
  ) %>%
  mutate(
    Completeness_pct = round(100 * Subtype_classified / Total_patients, 1)
  ) %>%
  gt() %>%
  tab_header(
    title    = "IHC Data Completeness by Site",
    subtitle = "Number of patients with available marker data"
  ) %>%
  cols_label(
    `Study Center`      = "Site",
    Total_patients      = "Total Enrolled",
    ER_available        = "ER Available",
    PR_available        = "PR Available",
    HER2_available      = "HER2 Available",
    All_3_available     = "All 3 Available",
    Subtype_classified  = "Subtype Classified",
    Completeness_pct    = "Completeness (%)"
  ) %>%
  data_color(
    columns  = Completeness_pct,
    palette  = "Blues"
  ) %>%
  opt_stylize(style = 6, color = "blue")
IHC Data Completeness by Site
Number of patients with available marker data
Site Total Enrolled ER Available PR Available HER2 Available All 3 Available Subtype Classified Completeness (%)
Kenya-KTRH 17 15 15 15 15 15 88.2
Kenya-MERU 81 79 79 80 77 77 95.1
Kenya-NTRH 46 45 43 43 42 42 91.3