3. L-G-B-T Disaggregation

Within-Group Heterogeneity Among LGBTQ+ Candidates

Show code
source(here::here("code", "00_setup.R"))
library(ggridges)

df <- readRDS(paths$analysis_full_rds)
df <- df %>%
  mutate(ideology_category = factor(ideology_category, levels = ideology_levels))

# Working subsets
lgbtq <- df %>% filter(lgbtq_candidate)
n_lgbtq <- nrow(lgbtq)

# Categories used in comparative analyses (excluding "Other LGBTQ+", N=12, too small for reliable estimates)
analysis_categories <- c("Gay", "Lesbian", "Bisexual+", "Trans", "Asexual")
main_categories <- c("Gay", "Lesbian", "Bisexual+", "Trans")

1 Overview

The previous chapter treated LGBTQ+ candidates as a single group. That is a useful starting point, but it conceals profound differences. A gay white man running for city council in São Paulo on a center-left ticket inhabits a very different political reality than a trans Black woman running in a small Northeastern municipality for a left-wing party.

This chapter disaggregates the LGBTQ+ umbrella into its constituent identity categories and asks: How do Gay, Lesbian, Bisexual+, and Trans candidates differ from one another — in demographics, political positioning, party affiliation, and electoral success?

The analysis focuses on the 3,134 LGBTQ+ candidates identified in the dataset.

Results by Position Type

As in Chapter 2, disaggregated analyses are presented separately for city councilors (proportional representation) and mayors/vice-mayors (plurality/majority) using tabbed panels. Because this chapter further splits by identity category, cell sizes become small for executive positions — the Mayors & Vice-Mayors tab carries a small-N warning, and some visualizations are simplified accordingly.

2 Identity Categories

2.1 Category Construction

The lgbt_category variable classifies each LGBTQ+ candidate into one of six categories: Gay, Lesbian, Bisexual+ (including bisexual and pansexual), Trans (transgender and travesti), Asexual, and Other LGBTQ+ (candidates identified as LGBTQ+ but without a more specific classification).

Classification Rule: Trans Takes Priority

A key coding decision: trans identity is prioritized over sexual orientation. A candidate who identifies as both transgender and lesbian is classified as “Trans,” not “Lesbian.” This reflects the sociological logic that gender identity is typically the more salient axis of political visibility and discrimination. The alternative coding (prioritizing sexual orientation) would obscure the experiences of trans candidates who also hold non-heterosexual orientations — which is the majority of trans candidates.

2.2 Distribution of Identity Categories

The lgbt_category variable classifies each LGBTQ+ candidate based on a combination of TSE self-reported sexual orientation/gender identity and VOTE LGBT records. Trans identity is prioritized over sexual orientation (see note above). The table below reports the count and cumulative share of each category.

Show code
render_identity <- function(data, tab_name) {
  lgbtq_pos <- data %>% filter(lgbtq_candidate)

  # --- Count table ---
  cat("### Count Table\n\n")

  identity_counts <- lgbtq_pos %>%
    count(lgbt_category, sort = TRUE) %>%
    mutate(
      pct = n / sum(n),
      pct_fmt = format_pct(pct),
      cum_pct = format_pct(cumsum(pct))
    )

  identity_counts %>%
    select(Category = lgbt_category, N = n, `%` = pct_fmt, `Cumulative %` = cum_pct) %>%
    cat_kable(align = c("l", "r", "r", "r"))

  # --- Composition chart ---
  cat("### Composition Chart\n\n")

  p_waffle <- lgbtq_pos %>%
    count(lgbt_category) %>%
    mutate(
      pct = n / sum(n),
      label = paste0(lgbt_category, "\n", format_n(n), " (", format_pct(pct), ")")
    ) %>%
    arrange(desc(n)) %>%
    mutate(lgbt_category = fct_reorder(lgbt_category, n)) %>%
    ggplot(aes(x = "", y = pct, fill = lgbt_category)) +
    geom_col(width = 1, alpha = 0.9, color = "white", linewidth = 0.5) +
    geom_text(
      aes(label = label),
      position = position_stack(vjust = 0.5),
      size = 3.5, color = "white", fontface = "bold"
    ) +
    coord_flip() +
    scale_fill_manual(values = pal_identity, guide = "none") +
    labs(
      x = NULL, y = NULL,
      title = paste0("Composition of LGBTQ+ Candidate Pool by Identity (", tab_name, ")"),
      subtitle = "Proportional stacked bar showing relative size of each identity group"
    ) +
    theme(
      axis.text = element_blank(),
      axis.ticks = element_blank(),
      panel.grid = element_blank()
    )
  cat_plot(p_waffle, paste0("03-identity-waffle-", pos_suffix(tab_name)))

  # --- Category counts ---
  cat("### Category Counts\n\n")

  p_bar <- lgbtq_pos %>%
    count(lgbt_category) %>%
    mutate(
      pct = n / sum(n),
      label = paste0(format_n(n), " (", format_pct(pct), ")")
    ) %>%
    ggplot(aes(x = reorder(lgbt_category, n), y = n, fill = lgbt_category)) +
    geom_col(alpha = 0.9, show.legend = FALSE, width = 0.6) +
    geom_text(aes(label = label), hjust = -0.1, size = 4) +
    coord_flip() +
    scale_y_continuous(expand = expansion(mult = c(0, 0.25))) +
    scale_fill_manual(values = pal_identity) +
    labs(
      x = NULL, y = "Number of Candidates",
      title = paste0("LGBTQ+ Candidates by Identity Category (", tab_name, ")"),
      subtitle = "Absolute counts with percentage of total LGBTQ+ pool"
    )
  cat_plot(p_bar, paste0("03-identity-bar-", pos_suffix(tab_name)))
}

render_position_tabset(render_identity, df)

2.2.1 Count Table

Category N % Cumulative %
Gay 1034 34.0% 34.0%
Lesbian 670 22.0% 56.0%
Trans 610 20.0% 76.0%
Bisexual+ 538 17.7% 93.7%
Asexual 188 6.2% 99.9%
Other LGBTQ+ 3 0.1% 100.0%

2.2.2 Composition Chart

2.2.3 Category Counts

2.2.4 Count Table

Category N % Cumulative %
Gay 43 47.3% 47.3%
Bisexual+ 26 28.6% 75.8%
Lesbian 11 12.1% 87.9%
Asexual 7 7.7% 95.6%
Trans 4 4.4% 100.0%

2.2.5 Composition Chart

2.2.6 Category Counts

Note

This tab pools city councilors (proportional representation) and mayors/vice-mayors (plurality). Position-specific results in the other tabs may be more informative.

2.2.7 Count Table

Category N % Cumulative %
Gay 1077 34.4% 34.4%
Lesbian 681 21.7% 56.1%
Trans 614 19.6% 75.7%
Bisexual+ 564 18.0% 93.7%
Asexual 195 6.2% 99.9%
Other LGBTQ+ 3 0.1% 100.0%

2.2.8 Composition Chart

2.2.9 Category Counts

Analytical Sample

The “Other LGBTQ+” category contains only 3 candidates — too few for reliable statistical comparisons. All subsequent analyses in this chapter exclude this category and focus on the five substantive identity groups: Gay, Lesbian, Bisexual+, Trans, and Asexual. The descriptive count tables above include all candidates for completeness.

3 Demographic Profiles

3.1 Multi-Column Comparison Table

The table below compares the identity groups on core demographics: mean age, gender composition (% female), racial breakdown (% Nonwhite, White, Black, Brown), and educational attainment (% College+). All variables are defined as in Chapter 1. Smaller categories (Asexual, Other LGBTQ+) are included for completeness but should be interpreted with caution given their smaller sample sizes.

Show code
render_demographics_03 <- function(data, tab_name) {
  lgbtq_pos <- data %>% filter(lgbtq_candidate)

  # --- Summary table ---
  cat("### Demographic Summary\n\n")

  demo_by_id <- lgbtq_pos %>%
    filter(lgbt_category %in% analysis_categories) %>%
    group_by(lgbt_category) %>%
    summarise(
      N = n(),
      `Mean Age` = round(mean(age, na.rm = TRUE), 1),
      `SD Age` = round(sd(age, na.rm = TRUE), 1),
      `% Female` = format_pct(mean(female, na.rm = TRUE)),
      `% Nonwhite` = format_pct(mean(nonwhite, na.rm = TRUE)),
      `% White` = format_pct(mean(race_simple == "White", na.rm = TRUE)),
      `% Black` = format_pct(mean(race_simple == "Black", na.rm = TRUE)),
      `% Brown` = format_pct(mean(race_simple == "Brown", na.rm = TRUE)),
      `% College+` = format_pct(mean(education_simple == "College+", na.rm = TRUE)),
      .groups = "drop"
    ) %>%
    rename(Category = lgbt_category)

  demo_by_id %>%
    mutate(N = format_n(N)) %>%
    cat_kable(align = c("l", "r", "r", "r", "r", "r", "r", "r", "r", "r"))

  # --- Age boxplot ---
  cat("### Age Distribution\n\n")

  p_age <- lgbtq_pos %>%
    filter(lgbt_category %in% main_categories, !is.na(age)) %>%
    mutate(lgbt_category = factor(lgbt_category, levels = main_categories)) %>%
    ggplot(aes(x = lgbt_category, y = age, fill = lgbt_category)) +
    geom_boxplot(alpha = 0.7, outlier.alpha = 0.3, outlier.size = 1) +
    geom_jitter(alpha = 0.08, width = 0.2, size = 0.8) +
    stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "black") +
    scale_fill_manual(values = pal_identity, guide = "none") +
    labs(
      x = NULL, y = "Age (years)",
      title = paste0("Age Distribution by LGBTQ+ Identity (", tab_name, ")"),
      subtitle = "Box plots with individual points (jittered). Black diamond = mean.",
      caption = "Showing Gay, Lesbian, Bisexual+, and Trans categories."
    )
  cat_plot(p_age, paste0("03-age-boxplot-", pos_suffix(tab_name)))

  # --- Race ---
  cat("### Race by Identity\n\n")

  p_race <- lgbtq_pos %>%
    filter(lgbt_category %in% main_categories, !is.na(race_simple)) %>%
    mutate(lgbt_category = factor(lgbt_category, levels = main_categories)) %>%
    count(lgbt_category, race_simple) %>%
    group_by(lgbt_category) %>%
    mutate(pct = n / sum(n)) %>%
    ungroup() %>%
    ggplot(aes(x = race_simple, y = pct, fill = race_simple)) +
    geom_col(alpha = 0.9, width = 0.7) +
    geom_text(aes(label = format_pct(pct)), vjust = -0.5, size = 3) +
    facet_wrap(~lgbt_category, nrow = 1) +
    scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.15))) +
    scale_fill_manual(values = pal_race, name = "Race") +
    labs(
      x = NULL, y = "Proportion",
      title = paste0("Racial Composition by Identity (", tab_name, ")"),
      subtitle = "Grouped bar charts within each identity group"
    ) +
    theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 9))
  cat_plot(p_race, paste0("03-race-by-identity-", pos_suffix(tab_name)), height = 6)

  # --- Education ---
  cat("### Education by Identity\n\n")

  p_edu <- lgbtq_pos %>%
    filter(lgbt_category %in% main_categories, !is.na(education_simple)) %>%
    mutate(
      lgbt_category = factor(lgbt_category, levels = main_categories),
      education_simple = factor(education_simple, levels = c("Less than HS", "High School", "College+"))
    ) %>%
    count(lgbt_category, education_simple) %>%
    group_by(lgbt_category) %>%
    mutate(pct = n / sum(n)) %>%
    ungroup() %>%
    ggplot(aes(x = education_simple, y = pct, fill = lgbt_category)) +
    geom_col(alpha = 0.9, width = 0.6, show.legend = FALSE) +
    geom_text(aes(label = format_pct(pct)), vjust = -0.5, size = 3) +
    facet_wrap(~lgbt_category, nrow = 1) +
    scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.15))) +
    scale_fill_manual(values = pal_identity) +
    labs(
      x = NULL, y = "Proportion",
      title = paste0("Education Levels by Identity (", tab_name, ")"),
      subtitle = "Comparing educational attainment across identity groups"
    ) +
    theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 9))
  cat_plot(p_edu, paste0("03-education-by-identity-", pos_suffix(tab_name)), height = 6)

  cat("::: {.callout-note}\n")
  cat("## Trans Educational Attainment\n")
  cat("Brazil's trans population faces well-documented barriers to educational attainment, ",
      "including school exclusion, bullying, and economic marginalization. Any differences ",
      "in college completion rates between trans and other LGBTQ+ candidates should be ",
      "interpreted in this structural context rather than as reflecting individual capacity.\n")
  cat(":::\n\n")
}

render_position_tabset(render_demographics_03, df)

3.1.1 Demographic Summary

Category N Mean Age SD Age % Female % Nonwhite % White % Black % Brown % College+
Gay 1,034 36.8 9.7 2.5% 59.4% 40.6% 24.0% 33.8% 56.4%
Lesbian 670 40.5 10.2 99.9% 61.0% 39.0% 22.8% 36.7% 44.9%
Bisexual+ 538 34.8 9.5 62.8% 61.5% 38.5% 28.4% 31.0% 65.1%
Trans 610 39.6 10.8 76.9% 65.9% 34.1% 21.8% 41.6% 31.1%
Asexual 188 47.2 12.1 46.3% 63.3% 36.7% 14.4% 46.3% 27.7%

3.1.2 Age Distribution

3.1.3 Race by Identity

3.1.4 Education by Identity

Trans Educational Attainment

Brazil’s trans population faces well-documented barriers to educational attainment, including school exclusion, bullying, and economic marginalization. Any differences in college completion rates between trans and other LGBTQ+ candidates should be interpreted in this structural context rather than as reflecting individual capacity.

3.1.5 Demographic Summary

Category N Mean Age SD Age % Female % Nonwhite % White % Black % Brown % College+
Gay 43 39.0 10.0 0.0% 32.6% 67.4% 16.3% 16.3% 88.4%
Lesbian 11 46.4 10.9 100.0% 54.5% 45.5% 18.2% 36.4% 63.6%
Bisexual+ 26 36.3 11.0 61.5% 53.8% 46.2% 30.8% 19.2% 76.9%
Trans 4 41.8 9.2 100.0% 50.0% 50.0% 25.0% 25.0% 75.0%
Asexual 7 50.0 4.6 42.9% 42.9% 57.1% 0.0% 42.9% 57.1%

3.1.6 Age Distribution

3.1.7 Race by Identity

3.1.8 Education by Identity

Trans Educational Attainment

Brazil’s trans population faces well-documented barriers to educational attainment, including school exclusion, bullying, and economic marginalization. Any differences in college completion rates between trans and other LGBTQ+ candidates should be interpreted in this structural context rather than as reflecting individual capacity.

Note

This tab pools city councilors (proportional representation) and mayors/vice-mayors (plurality). Position-specific results in the other tabs may be more informative.

3.1.9 Demographic Summary

Category N Mean Age SD Age % Female % Nonwhite % White % Black % Brown % College+
Gay 1,077 36.9 9.7 2.4% 58.3% 41.7% 23.7% 33.1% 57.7%
Lesbian 681 40.6 10.2 99.9% 60.9% 39.1% 22.8% 36.7% 45.2%
Bisexual+ 564 34.9 9.5 62.8% 61.2% 38.8% 28.5% 30.5% 65.6%
Trans 614 39.6 10.8 77.0% 65.8% 34.2% 21.8% 41.5% 31.4%
Asexual 195 47.3 11.9 46.2% 62.6% 37.4% 13.8% 46.2% 28.7%

3.1.10 Age Distribution

3.1.11 Race by Identity

3.1.12 Education by Identity

Trans Educational Attainment

Brazil’s trans population faces well-documented barriers to educational attainment, including school exclusion, bullying, and economic marginalization. Any differences in college completion rates between trans and other LGBTQ+ candidates should be interpreted in this structural context rather than as reflecting individual capacity.

Several dimensions warrant attention:

  • Gender composition differs across categories by definition (Gay candidates are male, Lesbian candidates are female) and by social structure (the gender distribution among Trans and Bisexual+ candidates reflects the composition of each group’s candidate pool).
  • Age varies across identities, potentially reflecting different generational patterns of openness and political entry.
  • Education differences across groups may reflect structural inequalities — particularly for trans candidates, who face documented barriers to formal education in Brazil.

4 Political Profiles

Party ideology scores are drawn from Bolognesi et al.’s expert survey (0–10 left-right scale; Left < 4.0, Center 4.0–7.1, Right > 7.1). The ideological positioning of LGBTQ+ candidates may vary by identity group.

Show code
render_political_03 <- function(data, tab_name) {
  lgbtq_pos <- data %>% filter(lgbtq_candidate)
  simplified <- use_simplified(data)

  # --- Ideology distribution table ---
  cat("### Ideology Distribution\n\n")

  ideo_tab <- lgbtq_pos %>%
    filter(!is.na(ideology_category), lgbt_category %in% analysis_categories) %>%
    count(lgbt_category, ideology_category) %>%
    group_by(lgbt_category) %>%
    mutate(pct = format_pct(n / sum(n))) %>%
    ungroup() %>%
    select(-n) %>%
    pivot_wider(names_from = ideology_category, values_from = pct, values_fill = "0.0%") %>%
    rename(Category = lgbt_category)
  cat_kable(ideo_tab, align = c("l", "r", "r", "r"))

  # --- Ideology score summary ---
  cat("### Ideology Scores\n\n")

  score_tab <- lgbtq_pos %>%
    filter(!is.na(ideology_score), lgbt_category %in% analysis_categories) %>%
    group_by(lgbt_category) %>%
    summarise(
      N = n(),
      Mean = round(mean(ideology_score), 2),
      SD = round(sd(ideology_score), 2),
      Median = round(median(ideology_score), 2),
      .groups = "drop"
    ) %>%
    rename(Category = lgbt_category)
  cat_kable(score_tab, align = c("l", "r", "r", "r", "r"))

  # --- Ideology plot ---
  cat("### Ideology Distribution Plot\n\n")

  if (simplified) {
    # Small N: grouped bar chart by ideology category
    p_ideo <- lgbtq_pos %>%
      filter(!is.na(ideology_category), lgbt_category %in% main_categories) %>%
      mutate(lgbt_category = factor(lgbt_category, levels = main_categories)) %>%
      count(lgbt_category, ideology_category) %>%
      group_by(lgbt_category) %>%
      mutate(pct = n / sum(n)) %>%
      ungroup() %>%
      ggplot(aes(x = ideology_category, y = pct, fill = ideology_category)) +
      geom_col(alpha = 0.9, width = 0.6) +
      geom_text(aes(label = format_pct(pct)), vjust = -0.5, size = 3) +
      facet_wrap(~lgbt_category, nrow = 1) +
      scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.15))) +
      scale_fill_manual(values = pal_ideology, guide = "none") +
      labs(
        x = NULL, y = "Proportion",
        title = paste0("Ideology by Identity (", tab_name, ")"),
        subtitle = "Bar chart of ideology categories within each identity group",
        caption = "Based on Bolognesi et al. expert survey scores."
      )
  } else {
    # Large N: ridgeline density
    p_ideo <- lgbtq_pos %>%
      filter(!is.na(ideology_score), lgbt_category %in% main_categories) %>%
      mutate(lgbt_category = factor(lgbt_category, levels = rev(main_categories))) %>%
      ggplot(aes(x = ideology_score, y = lgbt_category, fill = lgbt_category)) +
      geom_density_ridges(
        alpha = 0.7, scale = 1.2, rel_min_height = 0.01,
        quantile_lines = TRUE, quantiles = 2
      ) +
      geom_vline(xintercept = c(4, 7.1), linetype = "dashed", color = "gray40", linewidth = 0.4) +
      annotate("text", x = 2, y = Inf, label = "Left", vjust = 2,
               color = "#E74C3C", fontface = "bold", size = 3.5) +
      annotate("text", x = 5.5, y = Inf, label = "Center", vjust = 2,
               color = "#95A5A6", fontface = "bold", size = 3.5) +
      annotate("text", x = 8.5, y = Inf, label = "Right", vjust = 2,
               color = "#3498DB", fontface = "bold", size = 3.5) +
      scale_fill_manual(values = pal_identity, guide = "none") +
      scale_x_continuous(limits = c(0, 10)) +
      labs(
        x = "Ideology Score (0 = Far Left, 10 = Far Right)", y = NULL,
        title = paste0("Ideology Distribution by Identity (", tab_name, ")"),
        subtitle = "Ridgeline density. Vertical line within each ridge = median. Dashed lines = ideology thresholds.",
        caption = "Based on Bolognesi et al. expert survey scores."
      )
  }
  cat_plot(p_ideo, paste0("03-ideology-", pos_suffix(tab_name)), height = 7)

  # --- Top parties ---
  n_top <- if (simplified) 2 else 3
  cat(paste0("### Top ", n_top, " Parties by Identity\n\n"))

  top_parties <- lgbtq_pos %>%
    filter(lgbt_category %in% analysis_categories) %>%
    count(lgbt_category, party_abbrev, sort = TRUE) %>%
    group_by(lgbt_category) %>%
    mutate(rank = row_number(), pct = format_pct(n / sum(n))) %>%
    filter(rank <= n_top) %>%
    ungroup() %>%
    select(Category = lgbt_category, Rank = rank, Party = party_abbrev,
           N = n, `% within Category` = pct)
  cat_kable(top_parties, align = c("l", "r", "l", "r", "r"))

  # --- Party-identity heatmap ---
  cat("### Party x Identity Heatmap\n\n")

  n_parties <- if (simplified) 8 else 15
  top_lgbtq_parties <- lgbtq_pos %>%
    count(party_abbrev, sort = TRUE) %>%
    head(n_parties) %>%
    pull(party_abbrev)

  heatmap_data <- lgbtq_pos %>%
    filter(party_abbrev %in% top_lgbtq_parties, lgbt_category %in% main_categories) %>%
    count(party_abbrev, lgbt_category) %>%
    group_by(lgbt_category) %>%
    mutate(pct = n / sum(n)) %>%
    ungroup()

  p_heat <- heatmap_data %>%
    ggplot(aes(
      x = factor(lgbt_category, levels = main_categories),
      y = reorder(party_abbrev, n, FUN = sum),
      fill = pct
    )) +
    geom_tile(color = "white", linewidth = 0.5) +
    geom_text(aes(label = n), size = 3.5, color = "white", fontface = "bold") +
    scale_fill_gradient(
      low = "#EBF5FB", high = "#2C3E50",
      labels = percent, name = "% of Identity\nGroup"
    ) +
    labs(
      x = "Identity Category", y = "Party",
      title = paste0("LGBTQ+ Candidates Across Parties by Identity (", tab_name, ")"),
      subtitle = "Cell values = counts. Color intensity = proportion of identity group.",
      caption = paste0("Top ", n_parties, " parties by total LGBTQ+ candidates.")
    ) +
    theme(
      panel.grid = element_blank(),
      axis.text.x = element_text(face = "bold")
    )
  cat_plot(p_heat, paste0("03-party-heatmap-", pos_suffix(tab_name)), height = 8)

  cat("::: {.callout-note}\n")
  cat("## Party Preferences Differ by Identity\n")
  cat("The heatmap reveals the extent to which different identity groups concentrate ",
      "in different parties. If party preferences vary across identities, this suggests ",
      "that different groups face distinct opportunity structures in the party system. ",
      "Parties that are receptive to some LGBTQ+ identities may not be equally receptive to all.\n")
  cat(":::\n\n")
}

render_position_tabset(render_political_03, df)

4.0.1 Ideology Distribution

Category Left Center Right
Gay 38.8% 30.5% 30.8%
Lesbian 36.0% 35.1% 29.0%
Bisexual+ 59.3% 20.4% 20.3%
Trans 33.4% 39.5% 27.0%
Asexual 18.1% 36.2% 45.7%

4.0.2 Ideology Scores

Category N Mean SD Median
Gay 1034 5.22 2.36 5.29
Lesbian 670 5.25 2.34 5.80
Bisexual+ 538 4.22 2.48 2.97
Trans 610 5.35 2.29 6.50
Asexual 188 6.30 1.96 7.09

4.0.3 Ideology Distribution Plot

4.0.4 Top 3 Parties by Identity

Category Rank Party N % within Category
Gay 1 PT 224 21.7%
Bisexual+ 1 PT 151 28.1%
Lesbian 1 PT 120 17.9%
Bisexual+ 2 PSOL 103 19.1%
Trans 1 PT 95 15.6%
Gay 2 PSOL 88 8.5%
Gay 3 PSB 79 7.6%
Lesbian 2 PSB 64 9.6%
Lesbian 3 PSOL 60 9.0%
Trans 2 PSOL 58 9.5%
Trans 3 PSD 53 8.7%
Bisexual+ 3 PDT 32 5.9%
Asexual 1 PT 22 11.7%
Asexual 2 PRD 18 9.6%
Asexual 3 PL 17 9.0%

4.0.5 Party x Identity Heatmap

Party Preferences Differ by Identity

The heatmap reveals the extent to which different identity groups concentrate in different parties. If party preferences vary across identities, this suggests that different groups face distinct opportunity structures in the party system. Parties that are receptive to some LGBTQ+ identities may not be equally receptive to all.

4.0.6 Ideology Distribution

Category Left Center Right
Gay 51.2% 32.6% 16.3%
Lesbian 81.8% 9.1% 9.1%
Bisexual+ 73.1% 15.4% 11.5%
Trans 100.0% 0.0% 0.0%
Asexual 0.0% 42.9% 57.1%

4.0.7 Ideology Scores

Category N Mean SD Median
Gay 43 4.23 2.62 2.97
Lesbian 11 3.14 2.30 2.97
Bisexual+ 26 3.05 2.44 1.73
Trans 4 2.48 1.21 2.35
Asexual 7 6.99 1.11 7.11

4.0.8 Ideology Distribution Plot

4.0.9 Top 3 Parties by Identity

Category Rank Party N % within Category
Gay 1 PSOL 10 23.3%
Gay 2 PT 9 20.9%
Bisexual+ 1 PSOL 8 30.8%
Lesbian 1 PT 5 45.5%
Bisexual+ 2 UP 5 19.2%
Gay 3 PSD 4 9.3%
Bisexual+ 3 PT 4 15.4%
Lesbian 2 PSOL 2 18.2%
Asexual 1 PRD 2 28.6%
Lesbian 3 PL 1 9.1%
Trans 1 PDT 1 25.0%
Trans 2 PSOL 1 25.0%
Trans 3 PT 1 25.0%
Asexual 2 AGIR 1 14.3%
Asexual 3 MDB 1 14.3%

4.0.10 Party x Identity Heatmap

Party Preferences Differ by Identity

The heatmap reveals the extent to which different identity groups concentrate in different parties. If party preferences vary across identities, this suggests that different groups face distinct opportunity structures in the party system. Parties that are receptive to some LGBTQ+ identities may not be equally receptive to all.

Note

This tab pools city councilors (proportional representation) and mayors/vice-mayors (plurality). Position-specific results in the other tabs may be more informative.

4.0.11 Ideology Distribution

Category Left Center Right
Gay 39.3% 30.5% 30.2%
Lesbian 36.7% 34.7% 28.6%
Bisexual+ 59.9% 20.2% 19.9%
Trans 33.9% 39.3% 26.9%
Asexual 17.4% 36.4% 46.2%

4.0.12 Ideology Scores

Category N Mean SD Median
Gay 1077 5.18 2.38 5.29
Lesbian 681 5.22 2.35 5.29
Bisexual+ 564 4.16 2.49 2.97
Trans 614 5.33 2.29 6.41
Asexual 195 6.33 1.94 7.09

4.0.13 Ideology Distribution Plot

4.0.14 Top 3 Parties by Identity

Category Rank Party N % within Category
Gay 1 PT 233 21.6%
Bisexual+ 1 PT 155 27.5%
Lesbian 1 PT 125 18.4%
Bisexual+ 2 PSOL 111 19.7%
Gay 2 PSOL 98 9.1%
Trans 1 PT 96 15.6%
Gay 3 PSB 82 7.6%
Lesbian 2 PSB 64 9.4%
Lesbian 3 PSOL 62 9.1%
Trans 2 PSOL 59 9.6%
Trans 3 PSD 53 8.6%
Bisexual+ 3 PDT 32 5.7%
Asexual 1 PT 22 11.3%
Asexual 2 PRD 20 10.3%
Asexual 3 SOLIDARIEDADE 18 9.2%

4.0.15 Party x Identity Heatmap

Party Preferences Differ by Identity

The heatmap reveals the extent to which different identity groups concentrate in different parties. If party preferences vary across identities, this suggests that different groups face distinct opportunity structures in the party system. Parties that are receptive to some LGBTQ+ identities may not be equally receptive to all.

5 Electoral Outcomes

Election rate is the proportion of candidates who won their race (elected = 1), including outright winners and those elected by average in proportional races. The panels below report election rates for each identity category, with exact binomial 95% confidence intervals to account for varying sample sizes.

Show code
render_outcomes_03 <- function(data, tab_name) {
  lgbtq_pos <- data %>% filter(lgbtq_candidate)

  # --- Election rate table ---
  cat("### Election Rates\n\n")

  election_by_id <- lgbtq_pos %>%
    filter(!is.na(elected), lgbt_category %in% analysis_categories) %>%
    group_by(lgbt_category) %>%
    summarise(
      N = n(),
      Elected = sum(elected),
      Rate = mean(elected),
      .groups = "drop"
    ) %>%
    rowwise() %>%
    mutate(
      ci = list(binom.test(Elected, N)$conf.int),
      CI_low = ci[1],
      CI_high = ci[2]
    ) %>%
    ungroup() %>%
    select(-ci)

  election_by_id %>%
    mutate(
      `Election Rate` = format_pct(Rate),
      `95% CI` = paste0("[", format_pct(CI_low), ", ", format_pct(CI_high), "]"),
      N = format_n(N),
      Elected = format_n(Elected)
    ) %>%
    select(Category = lgbt_category, N, Elected, `Election Rate`, `95% CI`) %>%
    cat_kable(align = c("l", "r", "r", "r", "r"))

  # --- Forest plot ---
  cat("### Forest Plot\n\n")

  ref_rate <- data %>%
    filter(!lgbtq_candidate, !is.na(elected)) %>%
    summarise(rate = mean(elected)) %>%
    pull(rate)

  p_forest <- election_by_id %>%
    mutate(
      lgbt_category = fct_reorder(lgbt_category, Rate),
      sig = ifelse(CI_low > ref_rate, "Above",
                   ifelse(CI_high < ref_rate, "Below", "Overlaps"))
    ) %>%
    ggplot(aes(x = Rate, y = lgbt_category, color = lgbt_category)) +
    geom_vline(
      xintercept = ref_rate,
      linetype = "dashed", color = "gray40", linewidth = 0.6
    ) +
    annotate("text", x = ref_rate, y = Inf,
             label = paste0("Non-LGBTQ+: ", format_pct(ref_rate)),
             hjust = -0.1, vjust = 1.5, size = 3.5, color = "gray40") +
    geom_errorbarh(
      aes(xmin = CI_low, xmax = CI_high),
      height = 0.25, linewidth = 0.8
    ) +
    geom_point(size = 4) +
    scale_x_continuous(labels = percent) +
    scale_color_manual(values = pal_identity, guide = "none") +
    labs(
      x = "Election Rate", y = NULL,
      title = paste0("Election Rate by Identity (", tab_name, ")"),
      subtitle = "Forest plot with 95% CIs. Dashed line = Non-LGBTQ+ election rate.",
      caption = "Exact binomial confidence intervals."
    )
  cat_plot(p_forest, paste0("03-forest-election-", pos_suffix(tab_name)), height = 7)

  cat("::: {.callout-note}\n")
  cat("## Small Sample Caveat\n")
  n_asexual <- sum(lgbtq_pos$lgbt_category == "Asexual")
  cat(paste0("The Asexual category in this tab contains ", format_n(n_asexual),
             " candidates. While included for completeness, its estimates carry wider ",
             "confidence intervals than the four main categories --- Gay, Lesbian, ",
             "Bisexual+, and Trans --- which provide the most reliable within-group comparisons.\n"))
  cat(":::\n\n")
}

render_position_tabset(render_outcomes_03, df)

5.0.1 Election Rates

Category N Elected Election Rate 95% CI
Gay 973 93 9.6% [7.8%, 11.6%]
Lesbian 625 42 6.7% [4.9%, 9.0%]
Bisexual+ 500 34 6.8% [4.8%, 9.4%]
Trans 556 32 5.8% [4.0%, 8.0%]
Asexual 176 15 8.5% [4.8%, 13.7%]

5.0.2 Forest Plot

Small Sample Caveat

The Asexual category in this tab contains 188 candidates. While included for completeness, its estimates carry wider confidence intervals than the four main categories — Gay, Lesbian, Bisexual+, and Trans — which provide the most reliable within-group comparisons.

5.0.3 Election Rates

Category N Elected Election Rate 95% CI
Gay 42 6 14.3% [5.4%, 28.5%]
Lesbian 9 1 11.1% [0.3%, 48.2%]
Bisexual+ 25 1 4.0% [0.1%, 20.4%]
Trans 4 0 0.0% [0.0%, 60.2%]
Asexual 7 2 28.6% [3.7%, 71.0%]

5.0.4 Forest Plot

Small Sample Caveat

The Asexual category in this tab contains 7 candidates. While included for completeness, its estimates carry wider confidence intervals than the four main categories — Gay, Lesbian, Bisexual+, and Trans — which provide the most reliable within-group comparisons.

Note

This tab pools city councilors (proportional representation) and mayors/vice-mayors (plurality). Position-specific results in the other tabs may be more informative.

5.0.5 Election Rates

Category N Elected Election Rate 95% CI
Gay 1,015 99 9.8% [8.0%, 11.7%]
Lesbian 634 43 6.8% [5.0%, 9.0%]
Bisexual+ 525 35 6.7% [4.7%, 9.2%]
Trans 560 32 5.7% [3.9%, 8.0%]
Asexual 183 17 9.3% [5.5%, 14.5%]

5.0.6 Forest Plot

Small Sample Caveat

The Asexual category in this tab contains 195 candidates. While included for completeness, its estimates carry wider confidence intervals than the four main categories — Gay, Lesbian, Bisexual+, and Trans — which provide the most reliable within-group comparisons.

6 Disclosure Patterns

6.1 How Are Different Identities Identified?

Disclosure source indicates whether a candidate was identified via TSE self-declaration, VOTE LGBT external matching, or both. The relationship between identity and disclosure source is substantively important: gender identity (trans status) may be more publicly visible — and therefore more likely to be captured by VOTE LGBT through public campaigning — whereas sexual orientation may be more private and more often revealed through TSE self-declaration.

Pooled Across Positions

Disclosure patterns are presented pooled across city councilors and mayors/vice-mayors. The disclosure source reflects the identification methodology (TSE self-declaration vs. VOTE LGBT project) rather than a position-specific outcome. The pathway through which candidates are identified as LGBTQ+ is unlikely to vary systematically by the office sought.

Show code
disclosure_tab <- lgbtq %>%
  filter(lgbt_category %in% analysis_categories) %>%
  count(lgbt_category, disclosure_source) %>%
  group_by(lgbt_category) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup()

# Wide format for table
disclosure_tab %>%
  mutate(cell = paste0(n, " (", format_pct(pct), ")")) %>%
  select(lgbt_category, disclosure_source, cell) %>%
  pivot_wider(names_from = disclosure_source, values_from = cell, values_fill = "0 (0.0%)") %>%
  rename(Category = lgbt_category) %>%
  kable(align = c("l", rep("r", ncol(.) - 1)))
Table 1: Identity Category by Disclosure Source
Category TSE VOTE VOTE + TSE
Gay 731 (67.9%) 109 (10.1%) 237 (22.0%)
Lesbian 517 (75.9%) 47 (6.9%) 117 (17.2%)
Bisexual+ 362 (64.2%) 69 (12.2%) 133 (23.6%)
Trans 413 (67.3%) 62 (10.1%) 139 (22.6%)
Asexual 192 (98.5%) 1 (0.5%) 2 (1.0%)
Show code
lgbtq %>%
  filter(lgbt_category %in% analysis_categories) %>%
  count(lgbt_category, disclosure_source) %>%
  group_by(lgbt_category) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup() %>%
  ggplot(aes(x = fct_reorder(lgbt_category, -as.numeric(factor(lgbt_category))),
             y = pct, fill = disclosure_source)) +
  geom_col(alpha = 0.9, width = 0.6) +
  geom_text(
    aes(label = ifelse(pct > 0.05, format_pct(pct), "")),
    position = position_stack(vjust = 0.5), size = 3.5, color = "white", fontface = "bold"
  ) +
  coord_flip() +
  scale_y_continuous(labels = percent) +
  scale_fill_manual(
    values = c("TSE" = "#3498DB", "VOTE" = "#E74C3C", "Both" = "#9B59B6"),
    name = "Source"
  ) +
  labs(
    x = NULL, y = "Proportion",
    title = "How Each Identity Group Was Identified",
    subtitle = "Stacked proportions by disclosure source across identity categories"
  )

save_figure(last_plot(), "03_disclosure_by_identity")
Figure 1: Disclosure Source by Identity Category (Stacked Bars)

7 Summary

This chapter demonstrates that “LGBTQ+ candidates” is not a monolithic category. Key takeaways include:

  1. Compositional diversity: The LGBTQ+ candidate pool comprises several distinct identity categories, each with a different share of the total pool.
  2. Demographic heterogeneity: Age, gender composition, racial makeup, and educational attainment vary across identity groups, reflecting distinct structural positions within Brazilian society.
  3. Ideological variation: The degree to which different identity groups concentrate in left, center, or right parties differs. The ridgeline plots reveal distinct distributional shapes rather than a single “LGBTQ+ ideology.”
  4. Party sorting: Different identities concentrate in different parties, suggesting that the party system channels LGBTQ+ representation unevenly.
  5. Electoral outcomes diverge: Election rates are not uniform across identities. The forest plot and confidence intervals indicate which groups differ from the non-LGBTQ+ baseline.
  6. Disclosure is not random: The pathway through which candidates are identified as LGBTQ+ (TSE self-declaration vs. VOTE LGBT identification) varies by identity, with implications for data coverage and sample composition.

The next chapter examines the geographic dimension: where do LGBTQ+ candidates run, and how does geography interact with the identity patterns documented here?