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 subsetslgbtq <- 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)
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)
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) 2else3cat(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) 8else15 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)
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)
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.
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:
Compositional diversity: The LGBTQ+ candidate pool comprises several distinct identity categories, each with a different share of the total pool.
Demographic heterogeneity: Age, gender composition, racial makeup, and educational attainment vary across identity groups, reflecting distinct structural positions within Brazilian society.
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.”
Party sorting: Different identities concentrate in different parties, suggesting that the party system channels LGBTQ+ representation unevenly.
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.
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?
Source Code
---title: "3. L-G-B-T Disaggregation"subtitle: "Within-Group Heterogeneity Among LGBTQ+ Candidates"---```{r setup}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 subsetslgbtq <- 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")```# OverviewThe 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 `r format_n(n_lgbtq)` LGBTQ+ candidates identified in the dataset.::: {.callout-note}## Results by Position TypeAs 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.:::# Identity Categories {#sec-categories}## Category ConstructionThe `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).::: {.callout-note}## Classification Rule: Trans Takes PriorityA 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.:::## Distribution of Identity CategoriesThe `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.```{r identity-tabset}#| results: asisrender_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)```::: {.callout-note}## Analytical SampleThe "Other LGBTQ+" category contains only `r format_n(sum(lgbtq$lgbt_category == "Other LGBTQ+"))` 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.:::# Demographic Profiles {#sec-demographics}## Multi-Column Comparison TableThe 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.```{r demographics-tabset}#| results: asisrender_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)```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.# Political Profiles {#sec-political}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.```{r political-tabset}#| results: asisrender_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)```# Electoral Outcomes {#sec-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.```{r outcomes-tabset}#| results: asisrender_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)```# Disclosure Patterns {#sec-disclosure}## 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.::: {.callout-note}## Pooled Across PositionsDisclosure 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.:::```{r tbl-disclosure-by-identity}#| label: tbl-disclosure-by-identity#| tbl-cap: "Identity Category by Disclosure Source"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 tabledisclosure_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)))``````{r fig-disclosure-by-identity}#| label: fig-disclosure-by-identity#| fig-cap: "Disclosure Source by Identity Category (Stacked Bars)"#| fig-height: 6lgbtq %>% 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")```# SummaryThis 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?