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

April 24, 2024

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

# 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"
  )

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
# Creating "key population profiles" variable
data <- data |>
  mutate(
    key_population_profile = 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"
      )
  ) |> 
  set_variable_labels(key_population_profile = "Key population profile")


# Recoding variable "key population profiles"

data <- data |>
  mutate(
    key_population_profile = key_population_profile |>
      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"
      )
  ) |>
  set_variable_labels(key_population_profile = "key population profiles")

dh1 <- data |>
  group_by(key_population_profile, country) |>
  summarise(
    complete = "Based on self-interpreted test results",
    n = sum(HIVST_reported_result %in% c("reactive")),
    N = sum(!HIVST_reported_result %in% c("DK", "R")),
    Lowest = "Based on self-interpreted test results",
    n1 = sum(HIVST_reported_result %in% c("reactive")),
    N1 = n(),
    Highest = "Based on self-interpreted test results",
    n2 = sum(HIVST_reported_result %in% c("reactive", "DK", "R")),
    N2 = n()
  )

dh2 <- data |>
  group_by(key_population_profile, country) |>
  summarise(
    complete = "Based on the reported number of lines",
    n = sum(HIVST_reported_lines %in% c("2 lines")),
    N = sum(!HIVST_reported_result %in% c("DK", "R")),
    Lowest = "Based on the reported number of lines",
    n1 = sum(HIVST_reported_lines %in% c("2 lines")),
    N1 = n(),
    Highest = "Based on the reported number of lines",
    n2 = sum(HIVST_reported_lines %in% c("2 lines", "DK", "R")),
    N2 = n()
  )

dh0 <- bind_rows(dh1, dh2) |>
  mutate(
    complete = factor(complete, levels = c("Based on self-interpreted test results", "Based on the reported number of lines")),
    positivity_rate = n / N,
    label = scales::percent(positivity_rate, suffix = "", accuracy = .1),
    lowest = factor(Lowest, levels = c("reported result", "reported lines")),
    lowest_possible_rate = n1 / N1,
    label1 = scales::percent(lowest_possible_rate, suffix = "", accuracy = .1),
    Highest = factor(Highest, levels = c("reported result", "reported lines")),
    highest_possible_rate = n2 / N2,
    label2 = scales::percent(highest_possible_rate, suffix = "", accuracy = .1)
  ) |>
  mutate(
    ylabel = highest_possible_rate + .01
  )

# Dropping cells with denominator below 25
to_drop <- dh0$N < 25
to_drop <- dh0$N1 < 25
to_drop <- dh0$N2 < 25
dh0$positivity_rate[to_drop] <- NA
dh0$lowest_possible_rate[to_drop] <- NA
dh0$highest_possible_rate[to_drop] <- NA
dh0$label[to_drop] <- ""
dh0$label[to_drop] <- "*"
dh0$ylabel[to_drop] <- 0

fig3 <-
  dh0 |>
  ggplot() +
  aes(
    x = key_population_profile,
    y = positivity_rate,
    ymin = lowest_possible_rate,
    ymax = highest_possible_rate,
    color = complete,
    shape = complete,
  ) +
  geom_errorbar(
    position = position_dodge(width = 0.9),
    color = "#555555",
    width = .2
  ) +
  geom_point(
    stat = "identity",
    position = position_dodge(width = 0.9),
    size = 2.5
  ) +
  geom_text(
    mapping = aes(label = label, y = NULL),
    y = -0.01,
    position = position_dodge(width = 0.9),
    color = "black",
    size = 3,
    vjust = 1,
    face = "bold"
  ) +
  facet_grid(cols = vars(country)) +
  labs(title = "", x = "", y = "") +
  ggtitle("") +
  scale_y_continuous(labels = scales::percent, limits = c(-0.01, .25)) +
  scale_colour_manual(values = c("#DD3D2D", "#1B7837")) +
  theme_classic() +
  theme(
    panel.grid.major.y = element_line(colour = "#DDDDDD", linetype = "dotted"),
    legend.title = element_blank(),
    legend.position = "bottom"
  )
fig3

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
data <- data |>
  set_variable_labels(
    country = "Country",
    age_group = "Age group",
    marital_status = "Marital status",
    educational_level = "Educational level",
    first_time_tester = "First time tester"
  )


# 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)

var_label(phase2$test_result_number_lines_reported) <- "Reported number of lines /self-interpreted result"
var_label(phase2$all) <- "Overall"

# 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**"
  )
) %>%
  bold_labels()
Characteristic Completed phase 2 Linked to confirmatory testing Confirmed HIV positive Initiated ART
n n (%) 95% CI1 n (%) 95% CI1 n (%) 95% CI1
Overall






    ALL 78 34 (44%) 33%, 55% 19 (56%) 38%, 72% 18 (95%) 72%, 100%
Reported number of lines /self-interpreted result






    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 S1a. Factors associated (logistic regression) with positivity rate based on the reported number of visible lines among participants of the first survey phase in Côte d’Ivoire, Mali, and Senegal (2021)

Code
data$HIVST_reported_lines_status <- 
  data$HIVST_reported_lines %>%
  fct_recode(
    NULL = "0 line",
    "non reactive" = "1 line",
    "reactive" = "2 lines",
    NULL = "DK",
    NULL = "R"
  )

modele_reported_lines <- glm(
  HIVST_reported_lines_status ~ key_population_profile + country + age_group + marital_status + educational_level + first_time_tester,
  data = data,
  family = binomial
)

modele_reported_lines %>%
  tbl_regression(intercept = TRUE, exponentiate = TRUE) %>%
  add_global_p() %>%
  add_q(method = "bonferroni") %>%
  bold_labels()
Characteristic OR1 95% CI1 p-value q-value2
(Intercept) 0.04 0.01, 0.10 <0.001 <0.001
key population profiles

0.5 >0.9
    man MSM- based channels

    woman MSM- based channels 1.14 0.38, 2.73

    man FSW- based channels 0.90 0.54, 1.49

    woman FSW- based channels 0.69 0.40, 1.17

    man other channels 0.46 0.13, 1.29

    woman other channels 0.40 0.06, 1.49

Country

0.3 >0.9
    Côte d'Ivoire

    Mali 1.22 0.80, 1.88

    Senegal 1.79 0.84, 3.59

Age group

0.079 0.6
    24 years or less

    25-34 years 1.56 1.02, 2.42

    35 years or more 1.78 0.92, 3.34

Marital status

0.2 >0.9
    divorced / separated / widowed

    living with partner / married 0.48 0.21, 1.22

    single 0.64 0.28, 1.60

Educational level

0.2 >0.9
    higher

    none / primary 1.49 0.78, 2.86

    secondary 1.56 0.95, 2.64

First time tester

0.083 0.6
    no

    yes 1.44 0.95, 2.16

1 OR = Odds Ratio, CI = Confidence Interval
2 Bonferroni correction for multiple testing

Table S1b. Factors associated (logistic regression) with positivity rate based on self-reported HIVST, among participants of the first survey phase in Côte d’Ivoire, Mali, and Senegal (2021).

Code
data$HIVST_reported_result_status <- 
  data$HIVST_reported_result %>%
  fct_recode(
    NULL = "DK",
    NULL = "invalid",
    NULL = "R"
  )

modele_reported_result <- glm(
  HIVST_reported_result_status ~ key_population_profile + country + age_group + marital_status + educational_level + first_time_tester,
  data = data,
  family = binomial
)

modele_reported_result %>%
  tbl_regression(intercept = TRUE, exponentiate = TRUE) %>%
  add_global_p() %>%
  add_q(method = "bonferroni") %>%
  bold_labels()
Characteristic OR1 95% CI1 p-value q-value2
(Intercept) 0.01 0.00, 0.06 <0.001 <0.001
key population profiles

0.13 >0.9
    man MSM- based channels

    woman MSM- based channels 0.28 0.02, 1.35

    man FSW- based channels 0.42 0.19, 0.86

    woman FSW- based channels 0.55 0.28, 1.05

    man other channels 0.25 0.01, 1.40

    woman other channels 0.42 0.02, 2.42

Country

0.2 >0.9
    Côte d'Ivoire

    Mali 1.48 0.85, 2.62

    Senegal 0.69 0.15, 2.31

Age group

0.3 >0.9
    24 years or less

    25-34 years 1.57 0.89, 2.79

    35 years or more 1.35 0.48, 3.39

Marital status

0.5 >0.9
    divorced / separated / widowed

    living with partner / married 0.53 0.18, 1.98

    single 0.48 0.16, 1.79

Educational level

0.002 0.014
    higher

    none / primary 4.00 1.44, 12.9

    secondary 4.12 1.76, 12.1

First time tester

0.10 0.7
    no

    yes 1.58 0.91, 2.78

1 OR = Odds Ratio, CI = Confidence Interval
2 Bonferroni correction for multiple testing

Table S2a. Positivity rates based on self-interpreted HIVST result, by key population profiles and country, among phase 1 participants

Code
# Creation of a table of the lowest possible positivity rates


tblS2a_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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 table of positivity rates based on complete responses

tblS2a_central <- data |>
  filter(HIVST_reported_result != "DK", HIVST_reported_result != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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 table of the highest possible positivity rates

tblS2a_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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(
    "Lowest possible rate: positivity rate based on self reported test result and DK-R are considered not reactive",
    "Complete responses: positivity rate based on self reported test result and DK-R are excluded",
    "Highest possible rate: 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
Lowest possible rate: 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)
Complete responses: 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)
Highest possible rate: 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 S2b. Positivity rates based on the reported number of lines, by key population profiles and country, among phase 1 participants

Code
# Creation of a table of the lowest possible positivity rates

tblS2b_low <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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 table of positivity rates based on complete responses

tblS2b_central <- data |>
  filter(HIVST_reported_lines != "DK", HIVST_reported_lines != "R") |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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 table of the highest possible positivity rates

tblS2b_high <- data |>
  to_factor() |>
  tbl_custom_summary(
    include = c(country),
    by = key_population_profile,
    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(
    "Lowest possible rate: positivity rate based on self reported number of lines and DK-R are considered not 2 lines",
    "Complete responses: positivity rate based on self reported number of lines and DK-R are excluded",
    "Highest possible rate: 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
Lowest possible rate: 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)
Complete responses: 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)
Highest possible rate: 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 S3a. Positivity rates based on self-interpreted HIVST result, by age group and country, among participants of the first survey phase in Côte d’Ivoire, Mali, and Senegal (2021)

Code
# Creation of a table of the lowest possible positivity rates

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 table of positivity rates based on complete responses

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 table of the highest possible positivity rates

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(
    "Lowest possible rate: positivity rate based on self reported test result and DK-R are considered not reactive",
    "Complete responses: positivity rate based on self reported test result and DK-R are excluded",
    "Highest possible rate: positivity rate based on self reported test result and DK-R are considered reactive"
  )
)

tblS3a |>
  modify_header(label ~ "")
Overall, N = 2,6151 24 years or less, N = 1,1641 25-34 years, N = 1,0631 35 years or more, N = 3881
Lowest possible rate: 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)
Complete responses: 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)
Highest possible rate: 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 S3b. Positivity rates based on the reported number of visible lines, by age group and country, among participants of the first survey phase in Côte d’Ivoire, Mali, and Senegal (2021)

Code
# Creation of a table of the lowest possible positivity rates

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 table of positivity rates based on complete responses

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 table of the highest possible positivity rates

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(
    "Lowest possible rate: positivity rate based on self reported number of lines and DK-R are considered not 2 lines",
    "Complete responses: positivity rate based on self reported number of lines and DK-R are excluded",
    "Highest possible rate: positivity rate based on self reported number of lines and DK-R are considered 2 lines"
  )
)


tblS3b |>
  modify_header(label ~ "")
Overall, N = 2,6151 24 years or less, N = 1,1641 25-34 years, N = 1,0631 35 years or more, N = 3881
Lowest possible rate: 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)
Complete responses: 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)
Highest possible rate: 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)

Table S4a. Eligibility and participation in phase 2 survey by sociodemographic characteristics, distribution channel, and HIV testing history (bivariable comparison ).

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",
    key_population_profile = "Sex and distribution channel",
    age_group = "Age group",
    marital_status = "Marital status",
    educational_level = "Educational level",
    first_time_tester = "Firt-time tester"
  )

# Computing Table S4.a
data |>
  tbl_summary(
    include =
      c(
        country, key_population_profile, age_group,
        marital_status, educational_level, first_time_tester
      ),
    by = final_status_phas,
    type = list(c(first_time_tester) ~ "categorical")
  ) |>
  add_overall(TRUE) |>
  add_p(
    test = list(
      key_population_profile ~ "chisq.test",
      country ~ "chisq.test",
      marital_status ~ "chisq.test"
    )
  ) |>
  bold_labels()
Characteristic completed phase 2 questionnaire, N = 781 eligible for phase 2 but did not complete the questionnaire, N = 481 phase 1 participants not eligible for phase 2, N = 2,4891 Overall, N = 2,6151 p-value2
Country



>0.9
    Côte d'Ivoire 39 (50%) 23 (48%) 1,328 (53%) 1,390 (53%)
    Mali 31 (40%) 20 (42%) 933 (37%) 984 (38%)
    Senegal 8 (10%) 5 (10%) 228 (9.2%) 241 (9.2%)
Sex and distribution channel



0.3
    man MSM- based channels 35 (45%) 18 (38%) 944 (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 (21%) 588 (24%) 620 (24%)
    woman FSW- based channels 14 (18%) 16 (33%) 655 (26%) 685 (26%)
    man other channels 1 (1.3%) 3 (6.3%) 133 (5.3%) 137 (5.2%)
    woman other channels 1 (1.3%) 1 (2.1%) 71 (2.9%) 73 (2.8%)
Age group



0.5
    24 years or less 27 (35%) 21 (44%) 1,116 (45%) 1,164 (45%)
    25-34 years 38 (49%) 20 (42%) 1,005 (40%) 1,063 (41%)
    35 years or more 13 (17%) 7 (15%) 368 (15%) 388 (15%)
Marital status



0.3
    single 54 (69%) 32 (67%) 1,675 (67%) 1,761 (67%)
    divorced / separated / widowed 6 (7.7%) 2 (4.2%) 89 (3.6%) 97 (3.7%)
    living with partner / married 18 (23%) 14 (29%) 725 (29%) 757 (29%)
Educational level



0.079
    none / primary 13 (17%) 13 (27%) 477 (19%) 503 (19%)
    secondary 50 (64%) 29 (60%) 1,353 (54%) 1,432 (55%)
    higher 15 (19%) 6 (13%) 659 (26%) 680 (26%)
Firt-time tester



0.2
    no 40 (51%) 25 (52%) 1,472 (59%) 1,537 (59%)
    yes 38 (49%) 23 (48%) 1,017 (41%) 1,078 (41%)
1 n (%)
2 Pearson’s Chi-squared test

Table S4b. Eligibility and participation in phase 2 survey by sociodemographic characteristics, distribution channel, and HIV testing history (multivariable multinomial regression model).

Details of the multinomial model

Code
# Calculating the multinomial model

reg <- nnet::multinom(
  final_status_phas ~ country + key_population_profile + age_group + marital_status +
    educational_level + first_time_tester,
  data = data
)
# weights:  48 (30 variable)
initial  value 2872.871135 
iter  10 value 667.083122
iter  20 value 581.210504
iter  30 value 569.730948
iter  40 value 569.541017
final  value 569.540841 
converged
Code
# Model results display

tbl_reg <- reg |>
  tbl_regression(exponentiate = TRUE)

# Displaying the coefficients table in long format

multinom_pivot_wider <- function(x) {
  # check inputs match expectatations
  if (!inherits(x, "tbl_regression") || !inherits(x$model_obj, "multinom")) {
    stop("`x=` must be class 'tbl_regression' summary of a `nnet::multinom()` model.")
  }

  # create tibble of results
  df <- tibble::tibble(outcome_level = unique(x$table_body$groupname_col))
  df$tbl_reg <-
    purrr::map(
      df$outcome_level,
      function(lvl) {
        gtsummary::modify_table_body(
          x,
          ~ dplyr::filter(.x, .data$groupname_col %in% lvl) %>%
            dplyr::ungroup() %>%
            dplyr::select(-.data$groupname_col)
        )
      }
    )



  tbl_merge(df$tbl_reg, tab_spanner = paste0("**", df$outcome_level, "**"))
}

# Computing Table S4.b

tbl_reg |>
  multinom_pivot_wider() |>
  bold_labels()
Characteristic eligible for phase 2 but did not complete the questionnaire phase 1 participants not eligible for phase 2
OR1 95% CI1 p-value OR1 95% CI1 p-value
Country





    Côte d'Ivoire

    Mali 0.97 0.43, 2.20 >0.9 0.92 0.55, 1.54 0.8
    Senegal 0.55 0.13, 2.28 0.4 0.55 0.22, 1.33 0.2
Sex and distribution channel





    man MSM- based channels

    woman MSM- based channels 0.00 0.00, 0.00 <0.001 0.64 0.24, 1.72 0.4
    man FSW- based channels 0.95 0.35, 2.53 >0.9 1.09 0.62, 1.95 0.8
    woman FSW- based channels 2.34 0.85, 6.42 0.10 2.21 1.10, 4.44 0.025
    man other channels 9.32 0.77, 113 0.080 7.99 0.99, 64.2 0.051
    woman other channels 3.20 0.16, 63.5 0.4 4.79 0.58, 39.9 0.15
Age group





    24 years or less

    25-34 years 0.55 0.24, 1.27 0.2 0.51 0.30, 0.87 0.013
    35 years or more 0.43 0.13, 1.50 0.2 0.44 0.20, 0.96 0.038
Marital status





    single

    divorced / separated / widowed 0.51 0.08, 3.16 0.5 0.49 0.18, 1.35 0.2
    living with partner / married 1.33 0.55, 3.20 0.5 1.32 0.74, 2.35 0.3
Educational level





    none / primary

    secondary 0.62 0.24, 1.63 0.3 0.75 0.38, 1.45 0.4
    higher 0.45 0.12, 1.67 0.2 1.25 0.55, 2.80 0.6
Firt-time tester





    no

    yes 0.79 0.36, 1.72 0.6 0.66 0.41, 1.08 0.10
1 OR = Odds Ratio, CI = Confidence Interval

Global p-values used in Table S4

Code
reg |> car::Anova()
Analysis of Deviance Table (Type II tests)

Response: final_status_phas
                       LR Chisq Df Pr(>Chisq)  
country                  1.6352  4    0.80245  
key_population_profile  17.5542 10    0.06297 .
age_group                7.5856  4    0.10799  
marital_status           3.3810  4    0.49620  
educational_level        7.9290  4    0.09421 .
first_time_tester        2.9531  2    0.22842  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1