HIV self-testing positivity rate and linkage to confirmatory testing and care: a telephone survey in Côte d’Ivoire, Mali and Senegal

Author

Arsène Kouassi Kra et al

Published

September 8, 2023

Code
# Loading packages
library(tidyverse)
library(gtsummary)
library(labelled)
library(scales)
library(ggrepel)
library(RColorBrewer)
library(utils)
library(patchwork)

# Data import
data <- readr::read_csv("data.csv", show_col_types = FALSE)

Table 1. Self-reported test result and the reported number of lines among participants of phase 1, and positivity rates according to different hypotheses

Code
# Creating "test_result_number_lines_reported" variable
data <- data |>
  mutate(
    test_result_number_lines_reported =
      case_when(
        (HIVST_reported_lines == "2 lines" & HIVST_reported_result == "reactive") ~ "2 lines / reactive",
        (HIVST_reported_lines == "2 lines" & HIVST_reported_result == "non reactive") ~ "2 lines / non-reactive",
        (HIVST_reported_lines == "2 lines" & (HIVST_reported_result == "DK" | HIVST_reported_result == "R")) ~ "2 lines / DK-R",
        (HIVST_reported_lines == "1 line" & HIVST_reported_result == "reactive") ~ "1 line / reactive",
        (HIVST_reported_lines == "1 line" & HIVST_reported_result == "non reactive") ~ "1 line / non-reactive",
        (HIVST_reported_lines == "1 line" & (HIVST_reported_result == "DK" | HIVST_reported_result == "R")) ~ "1 line / DK-R", (HIVST_reported_lines == "0 line" & HIVST_reported_result == "non reactive") ~ "0 line / non-reactive", (HIVST_reported_lines == "0 line" & HIVST_reported_result == "reactive") ~ "0 line / reactive", (HIVST_reported_lines == "0 line" & (HIVST_reported_result == "DK" | HIVST_reported_result == "R")) ~ "0 line / DK-R", ((HIVST_reported_lines == "0 line" | HIVST_reported_lines == "1 line") & HIVST_reported_result == "invalid") ~ "0-1 line / invalid",
        (HIVST_reported_lines == "2 lines" & HIVST_reported_result == "invalid") ~ "2 lines / invalid",
        ((HIVST_reported_lines == "DK" | HIVST_reported_lines == "R") & HIVST_reported_result == "invalid") ~ "DK-R / invalid",
        ((HIVST_reported_lines == "DK" | HIVST_reported_lines == "R") & HIVST_reported_result == "reactive") ~ "DK-R / reactive",
        ((HIVST_reported_lines == "DK" | HIVST_reported_lines == "R") & HIVST_reported_result == "non reactive") ~ "DK-R / non-reactive",
        ((HIVST_reported_lines == "DK" | HIVST_reported_lines == "R") & (HIVST_reported_result == "DK" | HIVST_reported_result == "R")) ~ "DK-R / DK-R"
      )
  ) |>
  set_variable_labels(test_result_number_lines_reported = "Reported number of lines / self-interpreted HIVST result")

# Reorganizing the labels of the variable "test_result_number_lines_reported"
data$test_result_number_lines_reported <- data$test_result_number_lines_reported |>
  fct_relevel(
    "2 lines / reactive",
    "1 line / non-reactive",
    "0-1 line / invalid",
    "1 line / reactive",
    "2 lines / non-reactive", "0 line / non-reactive",
    "0 line / DK-R", "1 line / DK-R", "2 lines / DK-R", "DK-R / reactive",
    "DK-R / DK-R", "DK-R / non-reactive"
  )

# Generating Table 1
tbl1 <-
  data |>
  tbl_summary(
    include = test_result_number_lines_reported,
    label = test_result_number_lines_reported ~ ""
  ) |>
  modify_header(label = "**Phase 1 participants**") |>
  as_gt() |>
  gt::tab_row_group(
    label = gt::md("*Partial reponse (P)*"),
    rows = 8:13
  ) |>
  gt::tab_row_group(
    label = gt::md("*Inconsistant reponse (I)*"),
    rows = 5:7
  ) |>
  gt::tab_row_group(
    label = gt::md("*Consistant reponse (C)*"),
    rows = 2:4
  )

tbl1
Phase 1 participants N = 2,6151
Consistant reponse (C)
    2 lines / reactive 50 (1.9%)
    1 line / non-reactive 2,292 (88%)
    0-1 line / invalid 4 (0.2%)
Inconsistant reponse (I)
    1 line / reactive 10 (0.4%)
    2 lines / non-reactive 35 (1.3%)
    0 line / non-reactive 3 (0.1%)
Partial reponse (P)
    0 line / DK-R 1 (<0.1%)
    1 line / DK-R 117 (4.5%)
    2 lines / DK-R 29 (1.1%)
    DK-R / reactive 2 (<0.1%)
    DK-R / DK-R 28 (1.1%)
    DK-R / non-reactive 44 (1.7%)
1 n (%)

Figure 3. Positivity rates based on self-interpreted HIVST results or the reported number of visible lines, by distribution channel, gender and country, among participants of the first survey phase in Côte d’Ivoire, Mali, and Senegal (2021).

Code
## Fig 3.a BASED ON SELF-REPORTED RESULTS

# Creating "delivery_channel_by_sex" variable
data <- data |>
  mutate(
    delivery_channel_by_sex = interaction(sex, delivery_channel_grouped) |>
      fct_recode() |>
      fct_relevel(
        "man.MSM-based channels", "woman.MSM-based channels", "man.FSW-based channels",
        "woman.FSW-based channels", "man.Other delivery channels", "woman.Other delivery channels"
      )
  )

# Recoding variable "delivery_channel_by_sex"
d <- data |>
  mutate(
    delivery_channel_by_sex = delivery_channel_by_sex |>
      fct_recode(
        "man\nMSM-\nbased\nchannels" = "man.MSM-based channels",
        "woman\nMSM-\nbased\nchannels" = "woman.MSM-based channels",
        "man\nFSW-\n based\nchannels" = "man.FSW-based channels",
        "woman\nFSW-\nbased\nchannels" = "woman.FSW-based channels",
        "man\nother\nchannels" = "man.Other delivery channels",
        "woman\nother\nchannels" = "woman.Other delivery channels"
      )
  )

# Computing numerators and denominators for the 3 hypothesis
dh1 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "Low",
    n = sum(HIVST_reported_result %in% c("reactive")),
    N = n()
  )

dh2 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "Central",
    n = sum(HIVST_reported_result %in% c("reactive")),
    N = sum(!HIVST_reported_result %in% c("DK", "R"))
  )

dh3 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "High",
    n = sum(HIVST_reported_result %in% c("reactive", "DK", "R")),
    N = n()
  )

# Computing positivity rates based on self interpreted results
d_self_interpreted <- bind_rows(dh1, dh2, dh3) |>
  mutate(
    hypothesis = factor(hypothesis, levels = c("Low", "Central", "High")),
    positivity_rate = n / N,
    label = scales::percent(positivity_rate, suffix = "", accuracy = .1)
  ) |>
  rowwise() |>
  mutate(
    ci_low = prop.test(n, N)$conf.int[1],
    ci_high = prop.test(n, N)$conf.int[2]
  ) |>
  mutate(
    ylabel = ci_high + .01
  )

# Dropping cells with denominator below 25
to_drop <- d_self_interpreted$N < 25
d_self_interpreted$positivity_rate[to_drop] <- NA
d_self_interpreted$ci_low[to_drop] <- NA
d_self_interpreted$ci_high[to_drop] <- NA
d_self_interpreted$label[to_drop] <- ""
d_self_interpreted$label[to_drop & d_self_interpreted$hypothesis == "Central"] <- "*"
d_self_interpreted$ylabel[to_drop] <- 0

# Generating Figure 3.a.
g_self_interpreted <-
  d_self_interpreted |>
  ggplot() +
  aes(
    x = delivery_channel_by_sex,
    y = positivity_rate,
    ymin = ci_low,
    ymax = ci_high,
    color = hypothesis,
    shape = hypothesis,
  ) +
  geom_errorbar(
    position = position_dodge(width = 0.9),
    color = "#555555",
    width = .2
  ) +
  geom_point(
    stat = "identity",
    position = position_dodge(width = 0.9),
    size = 2
  ) +
  geom_text(
    mapping = aes(label = label, y = ylabel),
    position = position_dodge(width = 0.9),
    color = "black",
    size = 2.5,
    vjust = 0,
    face = "bold"
  ) +
  facet_grid(cols = vars(country)) +
  labs(title = "", x = "", y = "") +
  ggtitle("a. Positivity rates based on self-interpreted HIVST result") +
  scale_y_continuous(labels = scales::percent, limits = c(0, .5)) +
  scale_colour_manual(values = c("#ACD39E", "#5AAE61", "#1B7837")) +
  theme_classic() +
  theme(
    panel.grid.major.y = element_line(colour = "#DDDDDD", linetype = "dotted"),
    legend.title = element_blank(),
    legend.position = "bottom"
  )

## Fig 3.b BASED ON THE NUMBER OF LINES

# Computing numerators and denominators for the 3 hypothesis
dh1 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "Low",
    n = sum(HIVST_reported_lines %in% c("2 lines")),
    N = n()
  )

dh2 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "Central",
    n = sum(HIVST_reported_lines %in% c("2 lines")),
    N = sum(!HIVST_reported_result %in% c("DK", "R"))
  )

dh3 <- d |>
  group_by(delivery_channel_by_sex, country) |>
  summarise(
    hypothesis = "High",
    n = sum(HIVST_reported_lines %in% c("2 lines", "DK", "R")),
    N = n()
  )

# Computing positivity rates based on the number of reported lines
d_reported_lines <- bind_rows(dh1, dh2, dh3) |>
  mutate(
    hypothesis = factor(hypothesis, levels = c("Low", "Central", "High")),
    positivity_rate = n / N,
    label = scales::percent(positivity_rate, suffix = "", accuracy = .1)
  ) |>
  rowwise() |>
  mutate(
    ci_low = prop.test(n, N)$conf.int[1],
    ci_high = prop.test(n, N)$conf.int[2]
  ) |>
  mutate(
    ylabel = ci_high + .01
  )

# Dropping cells with denominator below 25
to_drop <- d_reported_lines$N < 25
d_reported_lines$positivity_rate[to_drop] <- NA
d_reported_lines$ci_low[to_drop] <- NA
d_reported_lines$ci_high[to_drop] <- NA
d_reported_lines$label[to_drop] <- ""
d_reported_lines$label[to_drop & d_reported_lines$hypothesis == "Central"] <- "*"
d_reported_lines$ylabel[to_drop] <- 0

# Generating Figure 3.b.
g_reported_lines <-
  d_reported_lines |>
  ggplot() +
  aes(
    x = delivery_channel_by_sex,
    y = positivity_rate,
    ymin = ci_low,
    ymax = ci_high,
    color = hypothesis,
    shape = hypothesis,
  ) +
  geom_errorbar(
    position = position_dodge(width = 0.9),
    color = "#555555",
    width = .2
  ) +
  geom_point(
    stat = "identity",
    position = position_dodge(width = 0.9),
    size = 2
  ) +
  geom_text(
    mapping = aes(label = label, y = ylabel),
    position = position_dodge(width = 0.9),
    color = "black",
    size = 2.5,
    vjust = 0,
    face = "bold"
  ) +
  facet_grid(cols = vars(country)) +
  labs(title = "", x = "", y = "") +
  ggtitle("b. Positivity rates based on the reported number of lines") +
  scale_y_continuous(labels = scales::percent, limits = c(0, .5)) +
  scale_colour_manual(values = c("#F67E4B", "#DD3D2D", "#A50026")) +
  theme_classic() +
  theme(
    panel.grid.major.y = element_line(colour = "#DDDDDD", linetype = "dotted"),
    legend.title = element_blank(),
    legend.position = "bottom"
  )

# Combining fig 3.a and fig 3.b
wrap_plots(
  g_self_interpreted,
  g_reported_lines,
  nrow = 2
) &
  theme(
    axis.text = element_text(size = 8.5),
    legend.text = element_text(size = 9),
    panel.spacing = unit(0.5, "lines"),
    strip.text = element_text(size = 9, face = "bold"),
    plot.title = element_text(size = 10, face = "bold")
  )

Figure 4: Elements for the flow chart of the participant selection process for Phase 2 of the survey

Code
n_eligible_for_phase_2 <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines")) |>
  nrow()

n_eligible_agreed_recontacted <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes")) |>
  nrow()

n_eligible_refusal_recontacted <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "no")) |>
  nrow()

n_unreachable_time_phase2 <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 == "unavaible" | is.na(status_phase2))) |>
  nrow()

n_successfully_recontacted <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 != "unavaible" & !is.na(status_phase2))) |>
  nrow()

n_refused_partcipate <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 != "unavaible" & !is.na(status_phase2)) & status_phase2 == "refusal") |>
  nrow()

n_accepted_partcipate <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 != "unavaible" & !is.na(status_phase2)) & status_phase2 != "refusal") |>
  nrow()

n_disconneted <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 != "unavaible" & !is.na(status_phase2)) & (status_phase2 != "refusal" & status_phase2 == "disconnected")) |>
  nrow()

n_dropped <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & (status_phase2 != "unavaible" & !is.na(status_phase2)) & status_phase2 != "refusal" & status_phase2 == "dropped out before the end") |>
  nrow()

n_completed_quest_phase2 <- data |>
  filter((HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines") & (recontact_phase2 == "yes") & status_phase2 == "questionnaires completed") |>
  nrow()
  • 126 participants who reported a reactive test and/or two lines in phase 1

  • 120 participants who agreed to be recalled several months later

  • 6 participants refusal to be recalled several months later

  • 24 unreachable at the time of phase 2

  • 96 successfully recontacted for phase 2

  • 7 refused to participate

  • 89 accepted to participate in phase 2 survey

  • 1 disconnected and unreachable

  • 10 dropped out before the end

  • 78 completed questionnaire phase 2

Table 2. Linkage to confirmatory testing, proportion being confirmed HIV positive and treatment initiation, by reported number of lines and self-interpreted HIVST result among phase 2 eligible participants who completed their questionnaire

Code
# Selection of individuals eligible for phase 2 who completed their phase 2 questionnaire
phase2 <- data |>
  filter(
    HIVST_reported_result == "reactive" | HIVST_reported_lines == "2 lines",
    recontact_phase2 == "yes",
    status_phase2 == "questionnaires completed"
  ) |>
  mutate(all = "ALL")
phase2$test_result_number_lines_reported <- droplevels(phase2$test_result_number_lines_reported)

# Computing the numbers of individuals who completed phase 2 questionnaire
tbl_completed <-
  phase2 |>
  tbl_summary(
    include = c(all, test_result_number_lines_reported),
    statistic = ~"{n}"
  ) |>
  modify_header(stat_0 = "**n**") |>
  modify_footnote(update = everything() ~ NA)

# Computing the proportion who linked to confirmatory testing
tbl_linked <-
  phase2 |>
  tbl_summary(
    by = confirmatory_test,
    include = c(all, test_result_number_lines_reported),
    percent = "row",
    digits = ~0
  ) |>
  add_ci(style_fun = ~ label_percent(accuracy = 1, suffix = "")) |>
  modify_column_hide(c(stat_1, ci_stat_1)) |>
  modify_header(stat_2 ~ "**n (%)**") |>
  modify_footnote(update = everything() ~ NA)

# Computing the proportion who were confirmed HIV-positive
tbl_confirmed <-
  phase2 |>
  filter(confirmatory_test == "yes") |>
  mutate(confirmed = confirmatory_test_result == "positive") |>
  tbl_summary(
    by = confirmed,
    include = c(all, test_result_number_lines_reported),
    percent = "row",
    digits = ~0
  ) |>
  add_ci(style_fun = ~ label_percent(accuracy = 1, suffix = "")) |>
  modify_column_hide(c(stat_1, ci_stat_1)) |>
  modify_header(stat_2 ~ "**n (%)**") |>
  modify_footnote(update = everything() ~ NA)

# Computing the proportion who initiated ART
tbl_initiated <-
  phase2 |>
  filter(confirmatory_test_result == "positive") |>
  mutate(initiated = consulted_health_prof == "yes") |>
  mutate(test_result_number_lines_reported = fct_drop(test_result_number_lines_reported)) |>
  tbl_summary(
    by = initiated,
    include = c(all, test_result_number_lines_reported),
    percent = "row",
    digits = ~0
  ) |>
  add_ci(style_fun = ~ label_percent(accuracy = 1, suffix = "")) |>
  modify_column_hide(c(stat_1, ci_stat_1)) |>
  modify_header(stat_2 ~ "**n (%)**") |>
  modify_footnote(update = everything() ~ NA)

# Merging tables
tbl_merge(
  list(tbl_completed, tbl_linked, tbl_confirmed, tbl_initiated),
  tab_spanner = c(
    "**Completed phase 2**",
    "**Linked to confirmatory testing**",
    "**Confirmed HIV positive**",
    "**Initiated ART**"
  )
)
Characteristic Completed phase 2 Linked to confirmatory testing Confirmed HIV positive Initiated ART
n n (%) 95% CI1 n (%) 95% CI1 n (%) 95% CI1
all
    ALL 78 34 (44%) 33%, 55% 19 (56%) 38%, 72% 18 (95%) 72%, 100%
test_result_number_lines_reported
    2 lines / reactive 27 15 (56%) 36%, 74% 12 (80%) 51%, 95% 12 (100%) 70%, 100%
    1 line / reactive 7 1 (14%) 1%, 58% 0 (0%) 0%, 95%
    2 lines / non-reactive 25 9 (36%) 19%, 57% 3 (33%) 9%, 69% 3 (100%) 31%, 100%
    2 lines / DK-R 18 8 (44%) 22%, 69% 4 (50%) 22%, 78% 3 (75%) 22%, 99%
    DK-R / reactive 1 1 (100%) 5%, 100% 0 (0%) 0%, 95%
1 CI = Confidence Interval

Table S1. Eligibility and participation in phase 2 survey by sociodemographic characteristics, distribution channel, HIV testing history, the reported number of lines and the self-interpreted HIVST result

Code
# Reordoring value labels
data <- data |>
  mutate(
    marital_status = marital_status |>
      fct_relevel(
        "single", "divorced / separated / widowed", "living with partner / married"
      ),
    educational_level = educational_level |>
      fct_relevel(
        "none / primary", "secondary", "higher"
      )
  )


# Improving variable labels
data <- data |>
  set_variable_labels(
    country = "Country",
    delivery_channel_by_sex = "Sex and distribution channel",
    age_group = "Age group",
    marital_status = "Marital status",
    educational_level = "Educational level",
    first_time_tester = "Firt-time tester",
    test_result_number_lines_reported = "Reported number of lines & self-interpreted HIVST result"
  )

# Computing Table S1.
data |>
  tbl_summary(
    include =
      c(
        country, delivery_channel_by_sex, age_group,
        marital_status, educational_level, first_time_tester, test_result_number_lines_reported
      ),
    by = final_status_phas,
    type = list(c(first_time_tester) ~ "categorical")
  ) |>
  add_overall(TRUE) |>
  add_p(
    test = list(
      test_result_number_lines_reported ~ "chisq.test",
      delivery_channel_by_sex ~ "chisq.test"
    )
  )
Characteristic completed phase 2 questionnaire, N = 781 eligible for phase 2 but did not complete the questionnaire, N = 421 phase 1 participants not eligible for phase 2, N = 2,4951 Overall, N = 2,6151 p-value2
Country 0.9
    Côte d'Ivoire 39 (50%) 20 (48%) 1,331 (53%) 1,390 (53%)
    Mali 31 (40%) 18 (43%) 935 (37%) 984 (38%)
    Senegal 8 (10%) 4 (9.5%) 229 (9.2%) 241 (9.2%)
Sex and distribution channel 0.3
    man.MSM-based channels 35 (45%) 14 (33%) 948 (38%) 997 (38%)
    woman.MSM-based channels 5 (6.4%) 0 (0%) 98 (3.9%) 103 (3.9%)
    man.FSW-based channels 22 (28%) 10 (24%) 588 (24%) 620 (24%)
    woman.FSW-based channels 14 (18%) 15 (36%) 656 (26%) 685 (26%)
    man.Other delivery channels 1 (1.3%) 2 (4.8%) 134 (5.4%) 137 (5.2%)
    woman.Other delivery channels 1 (1.3%) 1 (2.4%) 71 (2.8%) 73 (2.8%)
Age group 0.5
    24 years or less 27 (35%) 20 (48%) 1,117 (45%) 1,164 (45%)
    25-34 years 38 (49%) 16 (38%) 1,009 (40%) 1,063 (41%)
    35 years or more 13 (17%) 6 (14%) 369 (15%) 388 (15%)
Marital status 0.3
    single 54 (69%) 28 (67%) 1,679 (67%) 1,761 (67%)
    divorced / separated / widowed 6 (7.7%) 2 (4.8%) 89 (3.6%) 97 (3.7%)
    living with partner / married 18 (23%) 12 (29%) 727 (29%) 757 (29%)
Educational level 0.057
    none / primary 13 (17%) 10 (24%) 480 (19%) 503 (19%)
    secondary 50 (64%) 28 (67%) 1,354 (54%) 1,432 (55%)
    higher 15 (19%) 4 (9.5%) 661 (26%) 680 (26%)
Firt-time tester 0.3
    no 40 (51%) 22 (52%) 1,475 (59%) 1,537 (59%)
    yes 38 (49%) 20 (48%) 1,020 (41%) 1,078 (41%)
Reported number of lines & self-interpreted HIVST result <0.001
    2 lines / reactive 27 (35%) 20 (48%) 3 (0.1%) 50 (1.9%)
    1 line / non-reactive 0 (0%) 0 (0%) 2,292 (92%) 2,292 (88%)
    0-1 line / invalid 0 (0%) 0 (0%) 4 (0.2%) 4 (0.2%)
    1 line / reactive 7 (9.0%) 3 (7.1%) 0 (0%) 10 (0.4%)
    2 lines / non-reactive 25 (32%) 9 (21%) 1 (<0.1%) 35 (1.3%)
    0 line / non-reactive 0 (0%) 0 (0%) 3 (0.1%) 3 (0.1%)
    0 line / DK-R 0 (0%) 0 (0%) 1 (<0.1%) 1 (<0.1%)
    1 line / DK-R 0 (0%) 0 (0%) 117 (4.7%) 117 (4.5%)
    2 lines / DK-R 18 (23%) 9 (21%) 2 (<0.1%) 29 (1.1%)
    DK-R / reactive 1 (1.3%) 1 (2.4%) 0 (0%) 2 (<0.1%)
    DK-R / DK-R 0 (0%) 0 (0%) 28 (1.1%) 28 (1.1%)
    DK-R / non-reactive 0 (0%) 0 (0%) 44 (1.8%) 44 (1.7%)
1 n (%)
2 Fisher’s exact test; Pearson’s Chi-squared test

Table S2.a: Positivity rates based on self-interpreted HIVST result, by distribution channel, gender and country, among phase 1 participants

Code
# Creation of a positivity rate table according to the low hypothesis
tblS2a_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the central hypothesis
tblS2a_central <- data |>
  filter(HIVST_reported_result != "DK", HIVST_reported_result != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the high hypothesis
tblS2a_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive", "DK", "R")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Merging the different tables
tblS2a <- tbl_stack(list(tblS2a_low, tblS2a_central, tblS2a_high),
  group_header = c(
    "Low: positivity rate based on self reported test result and DK-R are considered not reactive",
    "Central: positivity rate based on self reported test result and DK-R are excluded",
    "High: positivity rate based on self reported test result and DK-R are considered reactive"
  )
)

tblS2a |>
  modify_header(label ~ "**Variable**") |>
  modify_spanning_header(
    c("stat_1", "stat_2") ~ "**MSM-based channels**",
    c("stat_3", "stat_4") ~ "**FSW-based channels**",
    c("stat_5", "stat_6") ~ "**Others delivery channels**",
    stat_0 ~ "**Total**"
  ) |>
  modify_header(
    stat_1 ~ "**Man**", stat_2 ~ "**Woman**",
    stat_3 ~ "**Man**", stat_4 ~ "**Woman**",
    stat_5 ~ "**Man**", stat_6 ~ "**Woman**",
    stat_0 ~ " "
  ) |>
  modify_footnote(update = everything() ~ NA)
Variable Total MSM-based channels FSW-based channels Others delivery channels
Man Woman Man Woman Man Woman
Low: positivity rate based on self reported test result and DK-R are considered not reactive
Overall 2.4% (62/2,615) 3.2% (32/997) 1.0% (1/103) 1.6% (10/620) 2.5% (17/685) 0.7% (1/137) 1.4% (1/73)
Country
    Côte d'Ivoire 1.8% (25/1,390) 2.5% (16/650) 1.4% (1/73) 1.5% (5/339) 1.2% (3/245) 0% (0/60) 0% (0/23)
    Mali 3.5% (34/984) 4.6% (14/306) 0% (0/29) 1.9% (5/269) 3.9% (14/360) 9.1% (1/11) 0% (0/9)
    Senegal 1.2% (3/241) 4.9% (2/41) 0% (0/1) 0% (0/12) 0% (0/80) 0% (0/66) 2.4% (1/41)
Central: positivity rate based on self reported test result and DK-R are excluded
Overall 2.5% (62/2,440) 3.4% (32/931) 1.0% (1/101) 1.7% (10/579) 2.7% (17/631) 0.8% (1/130) 1.5% (1/68)
Country
    Côte d'Ivoire 2.0% (25/1,279) 2.7% (16/597) 1.4% (1/71) 1.6% (5/311) 1.4% (3/221) 0% (0/58) 0% (0/21)
    Mali 3.6% (34/952) 4.7% (14/301) 0% (0/29) 1.9% (5/257) 4.1% (14/345) 9.1% (1/11) 0% (0/9)
    Senegal 1.4% (3/209) 6.1% (2/33) 0% (0/1) 0% (0/11) 0% (0/65) 0% (0/61) 2.6% (1/38)
High: positivity rate based on self reported test result and DK-R are considered reactive
Overall 9.1% (237/2,615) 9.8% (98/997) 2.9% (3/103) 8.2% (51/620) 10% (71/685) 5.8% (8/137) 8.2% (6/73)
Country
    Côte d'Ivoire 9.8% (136/1,390) 11% (69/650) 4.1% (3/73) 9.7% (33/339) 11% (27/245) 3.3% (2/60) 8.7% (2/23)
    Mali 6.7% (66/984) 6.2% (19/306) 0% (0/29) 6.3% (17/269) 8.1% (29/360) 9.1% (1/11) 0% (0/9)
    Senegal 15% (35/241) 24% (10/41) 0% (0/1) 8.3% (1/12) 19% (15/80) 7.6% (5/66) 9.8% (4/41)

Table S2.b: Positivity rates based on the reported number of lines, by distribution channel, gender and country, among phase 1 participants

Code
# Creation of a positivity rate table according to the low hypothesis
tblS2b_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the central hypothesis
tblS2b_central <- data |>
  filter(HIVST_reported_lines != "DK", HIVST_reported_lines != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the high hypothesis
tblS2b_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = delivery_channel_by_sex,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines", "DK", "R")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)


# Merging of the different tables
tblS2b <- tbl_stack(list(tblS2b_low, tblS2b_central, tblS2b_high),
  group_header = c(
    "Low: positivity rate based on self reported number of lines and DK-R are considered not 2 lines",
    "Central: positivity rate based on self reported number of lines and DK-R are excluded",
    "High: positivity rate based on self reported number of lines and DK-R are considered 2 lines"
  )
)

tblS2b |>
  modify_header(label ~ "**Variable**") |>
  modify_spanning_header(
    c("stat_1", "stat_2") ~ "**MSM-based channels**",
    c("stat_3", "stat_4") ~ "**FSW-based channels**",
    c("stat_5", "stat_6") ~ "**Others delivery channels**",
    stat_0 ~ "**Total**"
  ) |>
  modify_header(
    stat_1 ~ "**Man**", stat_2 ~ "**Woman**",
    stat_3 ~ "**Man**", stat_4 ~ "**Woman**",
    stat_5 ~ "**Man**", stat_6 ~ "**Woman**",
    stat_0 ~ " "
  ) |>
  modify_footnote(update = everything() ~ NA)
Variable Total MSM-based channels FSW-based channels Others delivery channels
Man Woman Man Woman Man Woman
Low: positivity rate based on self reported number of lines and DK-R are considered not 2 lines
Overall 4.4% (114/2,615) 4.7% (47/997) 4.9% (5/103) 4.5% (28/620) 4.1% (28/685) 2.9% (4/137) 2.7% (2/73)
Country
    Côte d'Ivoire 3.8% (53/1,390) 4.2% (27/650) 5.5% (4/73) 4.7% (16/339) 2.0% (5/245) 0% (0/60) 4.3% (1/23)
    Mali 4.9% (48/984) 4.9% (15/306) 3.4% (1/29) 4.5% (12/269) 5.3% (19/360) 9.1% (1/11) 0% (0/9)
    Senegal 5.4% (13/241) 12% (5/41) 0% (0/1) 0% (0/12) 5.0% (4/80) 4.5% (3/66) 2.4% (1/41)
Central: positivity rate based on self reported number of lines and DK-R are excluded
Overall 4.5% (114/2,541) 4.8% (47/977) 4.9% (5/103) 4.6% (28/605) 4.2% (28/660) 3.1% (4/128) 2.9% (2/68)
Country
    Côte d'Ivoire 3.9% (53/1,368) 4.2% (27/641) 5.5% (4/73) 4.8% (16/331) 2.1% (5/241) 0% (0/60) 4.5% (1/22)
    Mali 5.0% (48/955) 5.0% (15/298) 3.4% (1/29) 4.5% (12/264) 5.5% (19/344) 9.1% (1/11) 0% (0/9)
    Senegal 6.0% (13/218) 13% (5/38) 0% (0/1) 0% (0/10) 5.3% (4/75) 5.3% (3/57) 2.7% (1/37)
High: positivity rate based on self reported number of lines and DK-R are considered 2 lines
Overall 7.2% (188/2,615) 6.7% (67/997) 4.9% (5/103) 6.9% (43/620) 7.7% (53/685) 9.5% (13/137) 9.6% (7/73)
Country
    Côte d'Ivoire 5.4% (75/1,390) 5.5% (36/650) 5.5% (4/73) 7.1% (24/339) 3.7% (9/245) 0% (0/60) 8.7% (2/23)
    Mali 7.8% (77/984) 7.5% (23/306) 3.4% (1/29) 6.3% (17/269) 9.7% (35/360) 9.1% (1/11) 0% (0/9)
    Senegal 15% (36/241) 20% (8/41) 0% (0/1) 17% (2/12) 11% (9/80) 18% (12/66) 12% (5/41)

Table S3.a: Positivity rates based on self-interpreted HIVST result, by age group and country, among phase 1 participants

Code
# Creation of a positivity rate table according to the low hypothesis
tblS3a_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the central hypothesis
tblS3a_central <- data |>
  filter(HIVST_reported_result != "DK", HIVST_reported_result != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the high hypothesis
tblS3a_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_result", value = c("reactive", "DK", "R")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Merging of the different tables
tblS3a <- tbl_stack(list(tblS3a_low, tblS3a_central, tblS3a_high),
  group_header = c(
    "Low: positivity rate based on self reported test result and DK-R are considered not reactive",
    "Central: positivity rate based on self reported test result and DK-R are excluded",
    "High: positivity rate based on self reported test result and DK-R are considered reactive"
  )
)

tblS3a
Characteristic Overall, N = 2,6151 24 years or less, N = 1,1641 25-34 years, N = 1,0631 35 years or more, N = 3881
Low: positivity rate based on self reported test result and DK-R are considered not reactive
Overall 2.4% (62/2,615) 2.2% (26/1,164) 2.7% (29/1,063) 1.8% (7/388)
Country
    Côte d'Ivoire 1.8% (25/1,390) 1.7% (11/645) 2.0% (11/553) 1.6% (3/192)
    Mali 3.5% (34/984) 3.3% (15/455) 3.9% (16/415) 2.6% (3/114)
    Senegal 1.2% (3/241) 0% (0/64) 2.1% (2/95) 1.2% (1/82)
Central: positivity rate based on self reported test result and DK-R are excluded
Overall 2.5% (62/2,440) 2.4% (26/1,099) 2.9% (29/991) 2.0% (7/350)
Country
    Côte d'Ivoire 2.0% (25/1,279) 1.8% (11/604) 2.2% (11/506) 1.8% (3/169)
    Mali 3.6% (34/952) 3.4% (15/439) 4.0% (16/403) 2.7% (3/110)
    Senegal 1.4% (3/209) 0% (0/56) 2.4% (2/82) 1.4% (1/71)
High: positivity rate based on self reported test result and DK-R are considered reactive
Overall 9.1% (237/2,615) 7.8% (91/1,164) 9.5% (101/1,063) 12% (45/388)
Country
    Côte d'Ivoire 9.8% (136/1,390) 8.1% (52/645) 10% (58/553) 14% (26/192)
    Mali 6.7% (66/984) 6.8% (31/455) 6.7% (28/415) 6.1% (7/114)
    Senegal 15% (35/241) 13% (8/64) 16% (15/95) 15% (12/82)
1 prop% (n/N)

Table S3.b: Positivity rates based on the reported number of lines, by age group and country, among phase 1 participants

Code
# Creation of a positivity rate table according to the low hypothesis
tblS3b_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the central hypothesis
tblS3b_central <- data |>
  filter(HIVST_reported_lines != "DK", HIVST_reported_lines != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)

# Creation of a positivity rate table according to the high hypothesis
tblS3b_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = age_group,
    stat_fns = ~ proportion_summary(variable = "HIVST_reported_lines", value = c("2 lines", "DK", "R")),
    statistic = ~"{prop}% ({n}/{N})",
    digits = ~ list(
      function(x) {
        style_percent(x, digits = 0)
      }, 0, 0
    ),
    overall_row = TRUE
  ) |>
  add_overall(last = FALSE)


# Merging of the different tables
tblS3b <- tbl_stack(list(tblS3b_low, tblS3b_central, tblS3b_high),
  group_header = c(
    "Low: positivity rate based on self reported number of lines and DK-R are considered not 2 lines",
    "Central: positivity rate based on self reported number of lines and DK-R are excluded",
    "High: positivity rate based on self reported number of lines and DK-R are considered 2 lines"
  )
)

tblS3b
Characteristic Overall, N = 2,6151 24 years or less, N = 1,1641 25-34 years, N = 1,0631 35 years or more, N = 3881
Low: positivity rate based on self reported number of lines and DK-R are considered not 2 lines
Overall 4.4% (114/2,615) 3.7% (43/1,164) 4.9% (52/1,063) 4.9% (19/388)
Country
    Côte d'Ivoire 3.8% (53/1,390) 3.1% (20/645) 4.5% (25/553) 4.2% (8/192)
    Mali 4.9% (48/984) 4.8% (22/455) 4.8% (20/415) 5.3% (6/114)
    Senegal 5.4% (13/241) 1.6% (1/64) 7.4% (7/95) 6.1% (5/82)
Central: positivity rate based on self reported number of lines and DK-R are excluded
Overall 4.5% (114/2,541) 3.8% (43/1,138) 5.0% (52/1,032) 5.1% (19/371)
Country
    Côte d'Ivoire 3.9% (53/1,368) 3.1% (20/637) 4.6% (25/546) 4.3% (8/185)
    Mali 5.0% (48/955) 4.9% (22/447) 5.0% (20/401) 5.6% (6/107)
    Senegal 6.0% (13/218) 1.9% (1/54) 8.2% (7/85) 6.3% (5/79)
High: positivity rate based on self reported number of lines and DK-R are considered 2 lines
Overall 7.2% (188/2,615) 5.9% (69/1,164) 7.8% (83/1,063) 9.3% (36/388)
Country
    Côte d'Ivoire 5.4% (75/1,390) 4.3% (28/645) 5.8% (32/553) 7.8% (15/192)
    Mali 7.8% (77/984) 6.6% (30/455) 8.2% (34/415) 11% (13/114)
    Senegal 15% (36/241) 17% (11/64) 18% (17/95) 9.8% (8/82)
1 prop% (n/N)