---
title: "From what workers are owed to what humans are"
subtitle: "Across two encyclicals by two popes named Leo, 135 years apart, the vocabulary shifts from labor, wages, and property to dignity, personhood, and artificial intelligence."
description: "A diverging bar chart comparing the distinctive vocabulary of two papal encyclicals 135 years apart — Rerum Novarum (1891) and Magnifica Humanitas (2026) — showing the language of concern shift from labor and property to dignity and artificial intelligence. Distinctiveness is measured with weighted log-odds. Built in R with ggplot2 and tidytext."
date: "2026-06-22"
author:
- name: "Steven Ponce"
url: "https://stevenponce.netlify.app"
citation:
url: "https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_25.html"
categories: ["TidyTuesday", "2026"]
tags: [
"TidyTuesday",
"Text Analysis",
"Diverging Bar Chart",
"Keyness",
"Log-Odds",
"Catholic Social Teaching",
"Encyclicals",
"Natural Language Processing",
"tidytext",
"ggplot2",
"Annotation",
"2026"
]
image: "thumbnails/tt_2026_25.png"
format:
html:
toc: true
toc-depth: 5
code-link: true
code-fold: true
code-tools: true
code-summary: "Show code"
editor_options:
chunk_output_type: inline
execute:
freeze: true
cache: true
error: false
message: false
warning: false
eval: true
---
{#fig-1}
### [**Steps to Create this Graphic**]{.mark}
#### [1. Load Packages & Setup]{.smallcaps}
```{r}
#| label: load
#| warning: false
#| message: false
#| results: "hide"
## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
tidyverse, ggtext, showtext, janitor, ggrepel,
scales, glue, skimr, tidytext
)
})
### |- figure size ----
camcorder::gg_record(
dir = here::here("temp_plots"),
device = "png",
width = 9,
height = 8,
units = "in",
dpi = 320
)
# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```
#### [2. Read in the Data]{.smallcaps}
```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false
tt <- tidytuesdayR::tt_load(2026, week = 25)
encyclicals <- tt$encyclicals |> clean_names()
# papal_encyclicals <- tt$papal_encyclicals |> clean_names()
# scripture_references <- tt$scripture_references |> clean_names()
rm(tt)
```
#### [3. Examine the Data]{.smallcaps}
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(encyclicals)
# glimpse(papal_encyclicals)
# glimpse(scripture_references)
```
#### [4. Tidy Data]{.smallcaps}
```{r}
#| label: tidy
#| warning: false
### |- tokenize ----
tokens <- encyclicals |>
select(encyclical, paragraph, text) |>
unnest_tokens(word, text) |>
filter(str_detect(word, "^[a-z]+$")) |>
anti_join(stop_words, by = "word")
### |- weighted log-odds (signed distinctiveness) ----
word_counts <- tokens |>
count(encyclical, word, name = "n") |>
filter(n >= 3) |>
pivot_wider(names_from = encyclical, values_from = n, values_fill = 0) |>
rename(mh = `Magnifica Humanitas`, rn = `Rerum Novarum`)
n_mh <- sum(word_counts$mh)
n_rn <- sum(word_counts$rn)
vocab <- nrow(word_counts)
keyness <- word_counts |>
mutate(
a_mh = mh + 1, # + uninformative prior
a_rn = rn + 1,
log_odds = log(a_mh / (n_mh + vocab - a_mh)) -
log(a_rn / (n_rn + vocab - a_rn)),
se = sqrt(1 / a_mh + 1 / a_rn),
z = log_odds / se # weighted log-odds (signed)
)
### |- word selection: thesis vocabulary, log-odds ranked ----
# Pure log-odds buries the AI signal under high-count CST register
# (social, economic, justice, power...). So we force-include each side's
# vocabulary of CONCERN and suppress the institutional register -- same logic
# as the shared-spine exclusion, just extended. Every shown word is still
# verified distinctive (correct sign of z); sizing stays log-odds.
# words common to both / structural -- never shown ("human" returns as anchor)
shared_spine <- c(
"human", "god", "church", "society", "life",
"poor", "common", "world", "people", "time"
)
# modern-CST institutional register -- distinctive to MH but off-thesis
register_terms <- c(
"social", "economic", "responsibility", "development",
"doctrine", "political", "forms", "justice", "power",
"humanity"
)
excluded <- c(shared_spine, register_terms)
# force-included concern vocabularies (verified by sign of z below)
rn_focus <- c(
"labor", "property", "rights", "classes", "rich", "wages",
"employer", "workmen", "associations", "private", "law"
)
mh_focus <- c(
"dignity", "person", "ai", "digital", "artificial",
"intelligence", "technology", "technological", "data",
"algorithms", "machines"
)
n_each <- 11
# force-include only words that genuinely lean the intended way, then
# top up to n_each from the remaining distinctive (non-register) words.
pick_side <- function(focus, sign_dir) {
forced <- keyness |>
filter(word %in% focus, sign(z) == sign_dir)
fill <- keyness |>
filter(sign(z) == sign_dir, !word %in% excluded, !word %in% forced$word) |>
slice_max(abs(z), n = n_each, with_ties = FALSE)
bind_rows(forced, fill) |>
distinct(word, .keep_all = TRUE) |>
slice_head(n = n_each)
}
plot_words <- bind_rows(
pick_side(mh_focus, 1L),
pick_side(rn_focus, -1L)
) |>
mutate(side = if_else(z > 0, "Magnifica Humanitas", "Rerum Novarum")) |>
arrange(z) |>
mutate(word = fct_inorder(word))
### |- shared anchor ----
anchor <- keyness |>
filter(word == "human") |>
mutate(
mh_per1k = mh / n_mh * 1000,
rn_per1k = rn / n_rn * 1000
)
```
#### [5. Visualization Parameters]{.smallcaps}
```{r}
#| label: params
#| include: true
#| warning: false
### |- plot aesthetics ----
clrs <- get_theme_colors(
palette = list(
mh = "#722F37",
rn = "#7A7068",
ink = "#2C2825"
)
)
col_mh <- clrs$palette$mh
col_rn <- clrs$palette$rn
col_ink <- clrs$palette$ink
### |- titles and caption ----
title_text <- str_glue("From what workers are owed to what humans are")
subtitle_text <- str_glue(
"Across two encyclicals by two popes named Leo, 135 years apart, the ",
"vocabulary shifts from **labor, wages, and property** to ",
"**dignity, personhood, and artificial intelligence**."
)
method_note <- str_glue(
"Each encyclical's distinctive vocabulary of concern (shared and ",
"general-register terms removed);<br>bar length = weighted log-odds ",
"(Monroe et al.). Words appear at least 3 times."
)
caption_text <- str_glue(
"{create_social_caption(tt_year = 2026, tt_week = 25, ",
"source_text = 'Vatican.va (Holy See)')}<br>{method_note}"
)
### |- fonts ----
setup_fonts()
fonts <- get_font_families()
### |- plot theme ----
base_theme <- create_base_theme(clrs)
weekly_theme <- extend_weekly_theme(
base_theme,
theme(
plot.title = element_markdown(
face = "bold", family = fonts$title_1, size = rel(1.85),
color = col_ink, margin = margin(b = 6)
),
plot.subtitle = element_textbox_simple(
family = fonts$subtitle, size = rel(0.8), color = col_ink,
width = unit(1, "npc"), lineheight = 1.25, margin = margin(b = 30)
),
plot.caption = element_markdown(
family = fonts$caption, size = rel(0.55), color = col_rn,
hjust = 0, lineheight = 1.3, margin = margin(t = 20, b = 10)
),
axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
axis.title.y = element_blank(),
panel.grid = element_blank(),
axis.ticks = element_blank()
)
)
theme_set(weekly_theme)
### |- annotation positions ----
x_max <- max(abs(plot_words$z)) * 1.05
y_header <- n_each * 2 + 1.4
y_anchor <- -1.3
```
#### [6. Plot]{.smallcaps}
```{r}
#| label: plot
#| warning: false
### |- plot ----
p <- plot_words |>
ggplot(aes(x = z, y = word, fill = side)) +
# Geoms
geom_col(width = 0.72) +
geom_text(
data = \(d) filter(d, z > 0),
aes(label = word), hjust = -0.08,
family = fonts$text, size = 3.5, color = col_ink
) +
geom_text(
data = \(d) filter(d, z < 0),
aes(label = word), hjust = 1.08,
family = fonts$text, size = 3.5, color = col_ink
) +
geom_vline(xintercept = 0, color = col_ink, linewidth = 0.4) +
# Annotations
annotate(
"richtext",
x = -x_max, y = y_header, hjust = 0, vjust = 0,
label.color = NA, fill = NA, family = fonts$text, size = 3.4,
color = col_rn,
label = "**Rerum Novarum** \u00b7 1891 \u00b7 Leo XIII<br>
<span style='font-size:8pt'>The Industrial Revolution</span>"
) +
annotate(
"richtext",
x = x_max, y = y_header, hjust = 1, vjust = 0,
label.color = NA, fill = NA, family = fonts$text, size = 3.4,
color = col_mh,
label = "**Magnifica Humanitas** \u00b7 2026 \u00b7 Leo XIV<br>
<span style='font-size:8pt'>The AI Revolution</span>"
) +
annotate(
"richtext",
x = 0, y = y_anchor, hjust = 0.5, vjust = 0.5,
label.color = NA, fill = NA, family = fonts$text, size = 3.1,
color = col_rn, lineheight = 1.3,
label = "**The constant is the human** -- both encyclicals lean on that one word.<br>
What changes is the *threat* to it."
) +
# Scales
scale_fill_manual(
values = c("Magnifica Humanitas" = col_mh, "Rerum Novarum" = col_rn),
guide = "none"
) +
scale_x_continuous(
limits = c(-x_max, x_max),
expand = expansion(mult = c(0.22, 0.22))
) +
scale_y_discrete(expand = expansion(add = c(3, 2.2))) +
coord_cartesian(clip = "off") +
# Labs
labs(
title = title_text,
subtitle = subtitle_text,
caption = caption_text,
x = NULL,
y = NULL
)
```
#### [7. Save]{.smallcaps}
```{r}
#| label: save
#| warning: false
### |- plot image ----
save_plot(
plot = p,
type = "tidytuesday",
year = 2026,
week = 25,
width = 9,
height = 8,
)
```
#### [8. Session Info]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for Session Info
```{r, echo = FALSE}
#| eval: true
#| warning: false
sessionInfo()
```
:::
#### [9. GitHub Repository]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for GitHub Repo
The complete code for this analysis is available in [`tt_2026_25.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2026/tt_2026_25.qmd).
For the full repository, [click here](https://github.com/poncest/personal-website/).
:::
#### [10. References]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for References
1. **Data Source:**
- TidyTuesday 2026 Week 245cals: Industrial Revolution vs. AI Revolution](https://github.com/rfordatascience/tidytuesday/blob/main/data/2026/2026-06-23/readme.md)
:::
#### [11. Custom Functions Documentation]{.smallcaps}
::: {.callout-note collapse="true"}
##### 📦 Custom Helper Functions
This analysis uses custom functions from my personal module library for efficiency and consistency across projects.
**Functions Used:**
- **`fonts.R`**: `setup_fonts()`, `get_font_families()` - Font management with showtext
- **`social_icons.R`**: `create_social_caption()` - Generates formatted social media captions
- **`image_utils.R`**: `save_plot()` - Consistent plot saving with naming conventions
- **`base_theme.R`**: `create_base_theme()`, `extend_weekly_theme()`, `get_theme_colors()` - Custom ggplot2 themes
**Why custom functions?**\
These utilities standardize theming, fonts, and output across all my data visualizations. The core analysis (data tidying and visualization logic) uses only standard tidyverse packages.
**Source Code:**\
View all custom functions → [GitHub: R/utils](https://github.com/poncest/personal-website/tree/master/R)
:::