Mapping Sentiment and Controversy in Croatian Catholic Digital Media
Author
DigiKat Project
Published
January 27, 2026
1 Introduction
This document presents Map 3 of the DigiKat project, analyzing the emotional structure of Croatian Catholic digital media. The core question driving this analysis is What emotional register characterizes Croatian Catholic digital communication, and where does controversy emerge?
1.1 Methodological Overview
Sentiment analysis classifies text into positive, neutral, or negative categories based on the emotional tone of the language used. This automated classification (AUTO_SENTIMENT in the data) provides a high level view of emotional valence across the corpus.
Facebook reactions offer a more granular view of audience emotional response. Unlike simple likes, reactions (LOVE, WOW, HAHA, SAD, ANGRY) capture distinct emotional states, enabling us to construct emotional fingerprints for different actors and content types.
Key metrics in this analysis:
Metric
Definition
Interpretation
Sentiment ratio
Positive posts / Negative posts
Higher values indicate more positive tone
LOVE share
LOVE reactions / Total emotional reactions
Measures affection/appreciation response
ANGRY share
ANGRY reactions / Total emotional reactions
Measures anger/outrage response
Controversy Index
(ANGRY / Interactions) x log(Interactions)
Combines anger intensity with reach
Interpretation guidance:
High LOVE share with low ANGRY share indicates devotional, well received content
High ANGRY share may indicate controversial topics or provocative framing
Controversy Index accounts for both intensity (ANGRY ratio) and visibility (interaction volume)
Differences between automated sentiment and audience reactions reveal gaps between intent and reception
Show code
dta <-readRDS("C:/Users/lsikic/Luka C/HKS/Projekti/Digitalni Kat/SHKM/DigiKat/data/merged_comprehensive.rds") %>%filter(SOURCE_TYPE !="tiktok", !is.na(SOURCE_TYPE)) %>%filter(DATE >=as.Date("2021-01-01") & DATE <=as.Date("2025-12-31")) %>%filter(year >=2021& year <=2025)setDT(dta)# Check data loadedcat("Data loaded:", nrow(dta), "rows\n")
Data loaded: 608879 rows
Show code
if (nrow(dta) ==0) stop("No data after filtering! Check your date filters.")n_posts <-nrow(dta)n_sources <-uniqueN(dta$FROM)date_range <-paste(min(dta$DATE), "to", max(dta$DATE))
Sentiment analysis classifies each post as positive, neutral, or negative based on linguistic features. This section examines how sentiment varies across the corpus, platforms, and actor types.
Reading the charts:
Overall distribution shows the baseline emotional tone of the corpus
Platform comparison reveals how different media afford different emotional registers
Actor comparison identifies which communicators tend toward more positive or negative framing
Positivity ratio (positive/negative) summarizes relative emotional valence
Different platforms may encourage different emotional registers due to their affordances, audience expectations, and content formats.
Show code
sentiment_platform <- dta[!is.na(AUTO_SENTIMENT), .(Count = .N), by = .(SOURCE_TYPE, AUTO_SENTIMENT)]sentiment_platform[, Total :=sum(Count), by = SOURCE_TYPE]sentiment_platform[, Percentage := Count / Total *100]ggplot(sentiment_platform, aes(x =reorder(SOURCE_TYPE, Total), y = Percentage, fill = AUTO_SENTIMENT)) +geom_col(position ="stack", width =0.7) +geom_text(aes(label =sprintf("%.1f%%", Percentage)), position =position_stack(vjust =0.5), size =3, color ="white") +coord_flip() +scale_fill_manual(values = sentiment_colors) +labs(title ="Sentiment Distribution by Platform",subtitle ="Share of positive, neutral, and negative content per platform",x =NULL,y ="Percentage of Posts",fill ="Sentiment" ) +theme(legend.position ="top")
2.2 Sentiment by Actor Type
Actor types may have characteristic emotional registers reflecting their institutional roles, communication strategies, and target audiences.
Show code
sentiment_actor <- dta[!is.na(AUTO_SENTIMENT), .(Count = .N), by = .(ACTOR_TYPE, AUTO_SENTIMENT)]sentiment_actor[, Total :=sum(Count), by = ACTOR_TYPE]sentiment_actor[, Percentage := Count / Total *100]actor_order <- sentiment_actor[AUTO_SENTIMENT =="positive"][order(-Percentage)]$ACTOR_TYPEggplot(sentiment_actor, aes(x =factor(ACTOR_TYPE, levels =rev(actor_order)), y = Percentage, fill = AUTO_SENTIMENT)) +geom_col(position ="stack", width =0.7) +coord_flip() +scale_fill_manual(values = sentiment_colors) +labs(title ="Sentiment Distribution by Actor Type",subtitle ="Ordered by share of positive content",x =NULL,y ="Percentage of Posts",fill ="Sentiment" ) +theme(legend.position ="top")
2.3 Sentiment Summary Table
The Positivity Ratio divides positive posts by negative posts. Values above 1 indicate more positive than negative content; higher values indicate a more positive overall tone.
Does emotional tone affect audience engagement? This analysis examines whether positive, neutral, or negative content generates different levels of interaction.
The overall distribution of emotional reactions reveals the dominant emotional register of Croatian Catholic Facebook content.
Show code
if (nrow(fb_data) >0) { emotion_totals <-data.table(Emotion =c("LOVE", "WOW", "HAHA", "SAD", "ANGRY"),Total =c(sum(fb_data$LOVE_COUNT, na.rm =TRUE),sum(fb_data$WOW_COUNT, na.rm =TRUE),sum(fb_data$HAHA_COUNT, na.rm =TRUE),sum(fb_data$SAD_COUNT, na.rm =TRUE),sum(fb_data$ANGRY_COUNT, na.rm =TRUE) ) ) emotion_totals[, Percentage := Total /sum(Total) *100]ggplot(emotion_totals, aes(x =reorder(Emotion, -Total), y = Total, fill = Emotion)) +geom_col(width =0.7) +geom_text(aes(label =sprintf("%s\n(%.1f%%)", format(Total, big.mark =","), Percentage)), vjust =-0.2, size =3.5) +scale_fill_manual(values = emotion_colors) +scale_y_continuous(labels = comma, expand =expansion(mult =c(0, 0.15))) +labs(title ="Overall Emotional Reaction Distribution",subtitle ="Total Facebook reactions by type across all Catholic content",x =NULL,y ="Total Reactions" ) +theme(legend.position ="none")} else {cat("No Facebook reaction data available.\n")}
3.2 Emotional Fingerprints by Actor Type
Each actor type has a characteristic mix of emotional reactions. These fingerprints reveal how audiences emotionally engage with different types of Catholic communicators.
Show code
if (nrow(fb_data) >0&&sum(fb_data$Total_Reactions >0, na.rm =TRUE) >0) { actor_emotions <- fb_data[Total_Reactions >0, .(LOVE =mean(LOVE_SHARE, na.rm =TRUE) *100,WOW =mean(WOW_SHARE, na.rm =TRUE) *100,HAHA =mean(HAHA_SHARE, na.rm =TRUE) *100,SAD =mean(SAD_SHARE, na.rm =TRUE) *100,ANGRY =mean(ANGRY_SHARE, na.rm =TRUE) *100,Posts = .N ), by = ACTOR_TYPE] actor_emotions_long <- actor_emotions %>%select(-Posts) %>%pivot_longer(cols =-ACTOR_TYPE, names_to ="Emotion", values_to ="Share")ggplot(actor_emotions_long, aes(x = Emotion, y = Share, fill = Emotion)) +geom_col(width =0.7) +facet_wrap(~ACTOR_TYPE, ncol =3, scales ="free_y") +scale_fill_manual(values = emotion_colors) +labs(title ="Emotional Fingerprints by Actor Type",subtitle ="Mean share of each reaction type among emotional reactions",x =NULL,y ="Share of Emotional Reactions (%)" ) +theme(legend.position ="none",axis.text.x =element_text(angle =45, hjust =1) )} else {cat("Insufficient Facebook reaction data for actor analysis.\n")}
3.3 Comparative Emotional Profiles
The heatmap provides a comparative view of emotional profiles across all actor types, making it easy to identify which actors receive similar or different emotional responses.
Show code
if (exists("actor_emotions") &&nrow(actor_emotions) >0) { actor_emotions_matrix <- actor_emotions %>%select(ACTOR_TYPE, LOVE, WOW, HAHA, SAD, ANGRY) %>%pivot_longer(cols =-ACTOR_TYPE, names_to ="Emotion", values_to ="Share")ggplot(actor_emotions_matrix, aes(x = Emotion, y = ACTOR_TYPE, fill = Share)) +geom_tile(color ="white", linewidth =0.5) +geom_text(aes(label =sprintf("%.1f%%", Share)), size =3) +scale_fill_viridis(option ="plasma", direction =-1) +labs(title ="Emotional Profile Heatmap by Actor Type",subtitle ="Mean share of each reaction type",x =NULL,y =NULL,fill ="Share %" ) +theme(axis.text.x =element_text(angle =0, hjust =0.5),panel.grid =element_blank() )}
3.4 LOVE Ratio Analysis
LOVE reactions indicate deep appreciation and emotional connection. High LOVE share suggests content that resonates strongly with audience values and emotions.
Show code
if (nrow(fb_data) >0&&sum(fb_data$Total_Reactions >0, na.rm =TRUE) >0) { love_by_actor <- fb_data[Total_Reactions >0, .(Mean_Love_Share =mean(LOVE_SHARE, na.rm =TRUE) *100,Median_Love_Share =median(LOVE_SHARE, na.rm =TRUE) *100,SD_Love_Share =sd(LOVE_SHARE, na.rm =TRUE) *100,Posts = .N ), by = ACTOR_TYPE][order(-Mean_Love_Share)]ggplot(love_by_actor, aes(x =reorder(ACTOR_TYPE, Mean_Love_Share), y = Mean_Love_Share, fill = ACTOR_TYPE)) +geom_col(width =0.7) +geom_errorbar(aes(ymin = Mean_Love_Share - SD_Love_Share/sqrt(Posts),ymax = Mean_Love_Share + SD_Love_Share/sqrt(Posts)),width =0.2) +geom_text(aes(label =sprintf("%.1f%%", Mean_Love_Share)), hjust =-0.1, size =3.5) +coord_flip() +scale_fill_manual(values = actor_colors) +scale_y_continuous(expand =expansion(mult =c(0, 0.15))) +labs(title ="LOVE Reaction Share by Actor Type",subtitle ="Mean percentage of LOVE among all emotional reactions (with standard error)",x =NULL,y ="LOVE Share (%)" ) +theme(legend.position ="none")}
3.5 ANGRY Ratio Analysis
ANGRY reactions signal disagreement, outrage, or frustration. High ANGRY share may indicate controversial content, provocative framing, or topics that elicit strong negative responses.
Show code
if (nrow(fb_data) >0&&sum(fb_data$Total_Reactions >0, na.rm =TRUE) >0) { angry_by_actor <- fb_data[Total_Reactions >0, .(Mean_Angry_Share =mean(ANGRY_SHARE, na.rm =TRUE) *100,Median_Angry_Share =median(ANGRY_SHARE, na.rm =TRUE) *100,SD_Angry_Share =sd(ANGRY_SHARE, na.rm =TRUE) *100,Posts = .N ), by = ACTOR_TYPE][order(-Mean_Angry_Share)]ggplot(angry_by_actor, aes(x =reorder(ACTOR_TYPE, Mean_Angry_Share), y = Mean_Angry_Share, fill = ACTOR_TYPE)) +geom_col(width =0.7) +geom_errorbar(aes(ymin =pmax(0, Mean_Angry_Share - SD_Angry_Share/sqrt(Posts)),ymax = Mean_Angry_Share + SD_Angry_Share/sqrt(Posts)),width =0.2) +geom_text(aes(label =sprintf("%.2f%%", Mean_Angry_Share)), hjust =-0.1, size =3.5) +coord_flip() +scale_fill_manual(values = actor_colors) +scale_y_continuous(expand =expansion(mult =c(0, 0.2))) +labs(title ="ANGRY Reaction Share by Actor Type",subtitle ="Mean percentage of ANGRY among all emotional reactions (with standard error)",x =NULL,y ="ANGRY Share (%)" ) +theme(legend.position ="none")}
Controversy is operationalized through the Controversy Index, which combines the intensity of angry reactions with the visibility of the content.
Formula:Controversy Index = (ANGRY_COUNT / INTERACTIONS) × log(INTERACTIONS + 1)
This formula ensures that: 1. Content with higher ANGRY ratios scores higher 2. Content with more total interactions (greater reach) scores higher 3. The logarithmic transformation prevents extremely viral posts from dominating
Posts above the 95th percentile threshold are flagged as controversial.
This section cross-tabulates automated sentiment classification with actual emotional reactions to validate whether sentiment labels align with audience responses.
Expected patterns: - Positive sentiment posts should receive more LOVE reactions - Negative sentiment posts should receive more ANGRY and SAD reactions - Neutral sentiment posts should show balanced emotional profiles
Deviations from these patterns may indicate limitations in the sentiment classifier or interesting disconnects between content tone and audience reception.
The Positive Ratio divides LOVE by (ANGRY + SAD) to measure whether sentiment classifications align with actual emotional responses. Higher ratios for positive sentiment validate the classifier.
Emotional patterns may vary over time due to seasonal factors (liturgical calendar), events (elections, scandals), or longer term trends in the Catholic digital space.
Show code
if (nrow(fb_data) >0&&sum(fb_data$Total_Reactions >0, na.rm =TRUE) >0) { emotion_monthly <- fb_data[Total_Reactions >0, .(LOVE =mean(LOVE_SHARE, na.rm =TRUE) *100,ANGRY =mean(ANGRY_SHARE, na.rm =TRUE) *100,SAD =mean(SAD_SHARE, na.rm =TRUE) *100,Posts = .N ), by = .(Year =year(DATE), Month =month(DATE))] emotion_monthly[, Date :=as.Date(paste(Year, Month, "01", sep ="-"))] emotion_monthly_long <- emotion_monthly %>%select(Date, LOVE, ANGRY, SAD) %>%pivot_longer(cols =-Date, names_to ="Emotion", values_to ="Share")ggplot(emotion_monthly_long[!is.na(Date)], aes(x = Date, y = Share, color = Emotion)) +geom_line(linewidth =1) +geom_smooth(method ="loess", se =FALSE, linetype ="dashed", linewidth =0.7) +scale_color_manual(values =c("LOVE"="#ec4899", "ANGRY"="#ef4444", "SAD"="#3b82f6")) +scale_x_date(date_labels ="%Y-%m", date_breaks ="6 months") +labs(title ="Key Emotional Reactions Over Time",subtitle ="Monthly mean share of LOVE, ANGRY, and SAD reactions",x =NULL,y ="Share of Emotional Reactions (%)",color ="Reaction" ) +theme(axis.text.x =element_text(angle =45, hjust =1),legend.position ="top" )}
6.1 Day of Week Emotional Patterns
Do posting patterns by day of week affect emotional responses? Sunday posts (liturgical content) may receive different reactions than weekday posts.
Show code
if (nrow(fb_data) >0&&sum(fb_data$Total_Reactions >0, na.rm =TRUE) >0) { fb_data[, DOW := lubridate::wday(DATE, label =TRUE, abbr =FALSE)] emotion_dow <- fb_data[Total_Reactions >0&!is.na(DOW), .(LOVE =mean(LOVE_SHARE, na.rm =TRUE) *100,ANGRY =mean(ANGRY_SHARE, na.rm =TRUE) *100,Posts = .N ), by = DOW] emotion_dow_long <- emotion_dow %>%select(DOW, LOVE, ANGRY) %>%pivot_longer(cols =-DOW, names_to ="Emotion", values_to ="Share")ggplot(emotion_dow_long, aes(x = DOW, y = Share, fill = Emotion)) +geom_col(position ="dodge", width =0.7) +scale_fill_manual(values =c("LOVE"="#ec4899", "ANGRY"="#ef4444")) +labs(title ="LOVE and ANGRY Reactions by Day of Week",subtitle ="Mean share of reactions by posting day",x =NULL,y ="Share of Emotional Reactions (%)",fill ="Reaction" ) +theme(legend.position ="top")}