DigiKat Map 4: Temporal Dynamics

Mapping the Evolution of Croatian Catholic Digital Communication

Author

DigiKat Project

Published

January 27, 2026

1 Introduction

This document presents Map 4 of the DigiKat project, analyzing the temporal dynamics of Croatian Catholic digital media. The core question driving this analysis is How does Croatian Catholic digital communication evolve over time, and how does it respond to events?

Understanding temporal patterns reveals the rhythms of religious communication online, from daily posting cycles to annual liturgical seasons. This analysis examines over 600,000 posts from 2021 to 2024 to uncover when Croatian Catholics communicate, how patterns shift across platforms, and how the digital sphere responds to significant religious and secular events.

1.1 Methodological Overview

Temporal analysis examines how communication patterns change across multiple time scales. This includes macro trends (yearly, monthly), meso patterns (weekly, liturgical seasons), and micro rhythms (daily, hourly).

Key metrics in this analysis:

Metric Definition Interpretation
Rolling average Smoothed trend over N days Removes noise to reveal underlying patterns
Year over Year (YoY) Current vs same period last year Shows growth or decline trajectory
CAGR Compound Annual Growth Rate Standardized multi year growth measure
Effect size (Event period - Baseline) / Baseline Quantifies impact of events or seasons
Sunday Index Sunday activity / Weekday average Measures distinctive Sunday patterns

Interpretation guidance:

  • Rising rolling averages indicate growth; declining suggests contraction
  • Positive YoY change shows expansion; negative indicates decline
  • Effect sizes above 0% show increased activity during events/seasons
  • Sunday Index above 1.0 indicates higher Sunday activity than weekday average
  • Platform correlations reveal synchronized vs independent communication patterns

Liturgical calendar mapping:

The Catholic liturgical year structures religious communication. Key seasons include Advent (preparation for Christmas), Christmas (Dec 25 to Epiphany), Lent (40 days before Easter), Easter (50 days to Pentecost), and Ordinary Time (remainder). Feast days create additional activity spikes.

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)

cat("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.")

dta[, DATE := as.Date(DATE)]
dta[, Year := year(DATE)]
dta[, Month := month(DATE)]
dta[, Week := isoweek(DATE)]
dta[, DOW := lubridate::wday(DATE, label = TRUE, abbr = FALSE)]
dta[, DOW_num := lubridate::wday(DATE)]
dta[, Hour := as.integer(substr(TIME, 1, 2))]

n_posts <- nrow(dta)
n_sources <- uniqueN(dta$FROM)
date_range <- paste(min(dta$DATE, na.rm = TRUE), "to", max(dta$DATE, na.rm = TRUE))
Show code
# =============================================================================
# ACTOR CLASSIFICATION V4 - SYNCHRONIZED WITH MAPS 1, 2, 3
# =============================================================================
# Uses hierarchical priority: manual overrides -> secular exclusions -> 
# domain matching -> pattern matching -> lay influencer detection -> default
# =============================================================================

# MANUAL OVERRIDES: Highest priority explicit classifications
manual_overrides <- list(
  "Institutional Official" = c(
    "hrvatska katolička mreža", "hrvatska katolicka mreza",
    "informativna katolička agencija", "informativna katolicka agencija",
    "hrvatski katolički radio", "hrvatski katolicki radio",
    "hrvatska biskupska konferencija", "ika", "hkr", "hkm", "hbk",
    "tiskovni ured hbk", "radio marija"
  ),
  "Independent Media" = c(
    "laudato tv", "laudatotv", "laudato.tv", "laudato.hr", "laudato",
    "bitno.net", "bitno net", "glas koncila", "glaskoncila",
    "nova eva", "nova-eva", "verbum", "totus tuus", "totus-tuus",
    "katolički tjednik", "katolicki tjednik", "kršćanska sadašnjost",
    "krscanska sadasnjost", "mir i dobro", "svjetlo riječi",
    "novizivot.net", "novi zivot", "novi život"
  ),
  "Charismatic Communities" = c(
    "božja pobjeda", "bozja pobjeda", "bozjapobjeda",
    "muževni budite", "muzevni budite", "muzevnibudite",
    "srce isusovo", "srceisuovo", "cenacolo", "comunità cenacolo",
    "duhovna obnova", "molitvena snaga",
    "dom molitve slavonski brod", "dom molitve",
    "molitvena zajednica sv. josipa"
  ),
  "Lay Influencers" = c(
    "katolička obitelj", "katolicka obitelj",
    "marija majka isusova", "božanske molitve", "bozanske molitve",
    "moćne molitve tv", "mocne molitve tv", "moćne molitve", "mocne molitve",
    "katoličke molitve", "katolicke molitve",
    "pulherissimus", "pod smokvom",
    "hrana za dušu", "hrana za dusu",
    "добровољци", "miletić marin", "miletic marin",
    "dijete vjere", "vjera",
    "kapljice ljubavi božje", "kapljice ljubavi bozje",
    "kršćanstvo", "krscanstvo",
    "jutarnja molitva duhu svetom", "blago molitve",
    "biblija krunice molitve",
    "molitve bogu",
    "vojnik sreće", "vojnik srece", "duhovne poruke i inspiracija",
    "kes duhovni kutak", "duhovni kutak",
    "molitve.hr", "duhovniportal.com", "duhovniportal"
  ),
  "Diocesan" = c(
    "zagrebačka nadbiskupija", "zagrebacka nadbiskupija",
    "sisačka biskupija", "sisacka biskupija",
    "župa šurkovac", "zupa surkovac", "sveta mati slobode",
    "župa sv. ilije proroka metković", "zupa sv. ilije proroka metkovic",
    "župa uznesenja bdm", "zupa uznesenja bdm", "župa uznesenja bdm - stenjevec",
    "šibenska biskupija", "sibenska biskupija",
    "požeška biskupija", "pozeskа biskupija",
    "dubrovacka biskupija", "dubrovačka biskupija",
    "dubrovacka-biskupija.hr",
    "zupa tramosnica", "župa tramošnica",
    "župa sv. vida", "zupa sv. vida", "župa sv. vida - petruševec"
  ),
  "Youth Organizations" = c(
    "susret hrvatske katoličke mladeži", "susret hrvatske katolicke mladezi",
    "shkm požega", "shkm pozega"
  ),
  "Academic" = c(
    "hrvatsko katoličko sveučilište", "hrvatsko katolicko sveuciliste",
    "universitas studiorum catholica croatica"
  )
)

# SECULAR MEDIA EXCLUSIONS: Always classify as Other
secular_exclusions <- c(
  # National portals
  "slobodnadalmacija", "slobodnadalmacija.hr", 
  "vecernji", "vecernji.hr", 
  "index.hr", "index",
  "jutarnji", "jutarnji.hr", 
  "novilist", "novilist.hr",
  "24sata", "24sata.hr",
  "direktno", "direktno.hr",
  "nacional", "nacional.hr", 
  "tportal", "tportal.hr",
  "dnevnik.hr", "dnevnik", 
  "hrt.hr", "hrt", 
  "n1info", "n1info.hr", "n1",
  "rtl.hr", "rtl", 
  "net.hr", 
  "telegram.hr", "telegram", 
  "story.hr", "express.hr", "express", "advance.hr",
  # Regional portals
  "glasistre", "glasistre.hr",
  "dnevno.hr", "dnevno", 
  "prigorski", "glas-slavonije", "glas slavonije",
  "croativ", "oluja.info", 
  "maxportal", "maxportal.hr",
  "hkv.hr", "icv.hr", "novosti.hr", "7dnevno", "mnovine", 
  "sjever.hr", "dulist.hr", "pozega.eu", "sibenik.in",
  "ferata.hr", "epodravina", "glasgacke", "radio-zlatar", 
  "medjimurski.hr", "sbperiskop", "zagorje-international",
  "pozeski", "novine.hr", "dubrovnikinsider", "regionalni", 
  "leportale", "varazdinske-vijesti", "radionasice", 
  "brodportal", "ljportal", "dubrovnikportal", "01portal",
  "tomislavnews", "hia.com.hr", "portalnovosti", "antenazadar",
  "dalmacijanews", "zadarskilist", "medjimurjepress", 
  "zagreb.info", "034portal", "057info", "cityportal",
  "klikaj.hr", "lika-online", "ploce.com", "sbonline", 
  "narod.hr", "infokiosk", "hrsvijet", "tomislavcity", 
  "vrisak.info", "dalmacijadanas", "dalmacijadanas.hr",
  "morski.hr", "zagreb.hr", "osijek031", "rijeka.hr", "zadar.hr",
  "zupanjac.net", "zupanjac", 
  "dalmatinskiportal.hr", "dalmatinskiportal",
  "campaign-archive.com",
  # Forums and aggregators
  "forum.hr", "reddit", "anonymous_user", "komentari", "bug.hr",
  # Entertainment and other
  "inmemoriam", "magicus.info", "book.hr", "mojzagreb.info", 
  "skole.hr", "tvprofil", "priznajem.hr", "dragovoljac.com", 
  "croatia", "wikipedia",
  "facebook.com", "youtube.com", "instagram.com", "twitter.com",
  # Government and administrative
  "županija", "zupanija", "grad ", "opcina", "općina",
  # Non-Catholic religious groups
  "kršćanska proročka crkva", "krscanska prorocka crkva",
  "crkva svemogućeg boga", "crkva svemoguceg boga",
  "jehovini svjedoci", "adventisti", "baptisti", "pentekostalna",
  # Political parties
  "domovinski pokret", "hdz", "sdp", "most", "možemo", "mozemo"
)

classify_actor_v4 <- function(from_value, url_value = NA, platform_value = NA) {
  from_lower <- tolower(from_value)
  url_lower <- tolower(ifelse(is.na(url_value), "", url_value))
  platform_lower <- tolower(ifelse(is.na(platform_value), "", platform_value))
  combined <- paste(from_lower, url_lower)
  
  match_any <- function(patterns, text) {
    any(sapply(patterns, function(p) grepl(p, text, fixed = TRUE)))
  }
  
  # PRIORITY 1: Manual overrides
  for (actor_type in names(manual_overrides)) {
    if (match_any(manual_overrides[[actor_type]], from_lower)) {
      return(actor_type)
    }
  }
  
  # PRIORITY 2: Secular exclusions
  if (match_any(secular_exclusions, combined)) {
    return("Other")
  }
  
  # PRIORITY 3: Domain-based classification
  institutional_domains <- c("hkm.hr", "ika.hkm.hr", "hkr.hkm.hr", "hbk.hr")
  if (any(sapply(institutional_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Institutional Official")
  }
  
  diocesan_domains <- c(
    "zg-nadbiskupija.hr", "biskupija-varazdinska.hr", "djos.hr", 
    "biskupija-sj.hr", "rzs.hr", "rkc-sisak.hr", "zadarskanadbiskupija.hr",
    "gospicko-senjska-biskupija.hr", "nadbiskupija-split.com",
    "dubrovacka-biskupija.hr", "porec-biskupija.hr", "biskupija-kk.hr"
  )
  if (any(sapply(diocesan_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Diocesan")
  }
  
  independent_media_domains <- c(
    "laudato.hr", "laudato.tv", "bitno.net", "glaskoncila.hr", 
    "nova-eva.com", "verbum.hr", "ks.hr", "novizivot.net"
  )
  if (any(sapply(independent_media_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Independent Media")
  }
  
  # PRIORITY 4: Pattern-based classification
  is_parish <- grepl("^župa|^zupa|župi|zupi|- župa|- zupa", from_lower, ignore.case = TRUE) ||
               grepl("parish", from_lower, ignore.case = TRUE)
  
  diocesan_names <- c("nadbiskupija", "biskupija")
  if (is_parish || any(sapply(diocesan_names, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Diocesan")
  }
  
  religious_orders <- c(
    "franjevci", "franjevački", "franjevacki", "ofm",
    "isusovci", "družba isusova", "druzba isusova",
    "dominikanci", "dominikanski",
    "salezijanci", "salezijanski", "sdb",
    "karmelićani", "karmelicani", "karmel",
    "benediktinci", "benediktinski", "osb",
    "kapucini", "kapucinski", "ofmcap",
    "pavlini", "pavlinski", "trapisti", "cisterciti",
    "sestre milosrdnice", "uršulinke", "klarise",
    "službenice milosrđa", "kćeri božje ljubavi", "kceri bozje ljubavi"
  )
  if (any(sapply(religious_orders, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Religious Orders")
  }
  
  charismatic <- c(
    "molitvena zajednica", "karizmatska", "cenacolo",
    "emmanuel community", "emmanuel zajednica", "taize",
    "neokatekumenski", "neokatekumenska", "kursiljo",
    "fokolari", "fokolarini", "komunija i oslobođenje",
    "dom molitve", "kuća molitve", "kuca molitve"
  )
  if (any(sapply(charismatic, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Charismatic Communities")
  }
  
  priest_prefixes <- c("fra ", "don ", "vlč.", "vlc.", "msgr.", "mons.",
                       "o. ", "pater ", "preč.", "prec.")
  for (prefix in priest_prefixes) {
    if (startsWith(from_lower, prefix)) return("Individual Priests")
  }
  if (grepl("svećenik|svecenik|župnik|zupnik", from_lower)) {
    return("Individual Priests")
  }
  
  youth <- c(
    "frama", "shkm", "katolička mladež", "katolicka mladez",
    "ministranti", "mladifra", "mladi fra", "kaem",
    "sveučilišna kapelanija", "studentska kapelanija"
  )
  if (any(sapply(youth, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Youth Organizations")
  }
  
  academic <- c(
    "unicath", "katolički bogoslovni fakultet", "kbf",
    "teologija", "filozofski fakultet družbe isusove", "hks.hr",
    "hrvatsko katoličko sveučilište", "hku"
  )
  if (any(sapply(academic, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Academic")
  }
  
  # PRIORITY 5: Lay influencer detection (platform-aware)
  lay_keywords <- c(
    "vjera", "molitva", "molitve", "isus", "krist", "gospa", "marija",
    "duhovnost", "duhovna", "duhovni", "biblija", "psalm", "blagoslov",
    "krunica", "rozarij", "katolička obitelj", "evanđelje", "evandelje"
  )
  
  lay_exclude <- c(".hr", ".net", ".com", "portal", "vijesti", "news", "radio")
  
  is_likely_social <- platform_lower %in% c("facebook", "instagram", "youtube", "twitter") ||
                      !grepl("\\.[a-z]{2,4}$", from_lower)
  
  has_devotional <- any(sapply(lay_keywords, function(x) grepl(x, from_lower, fixed = TRUE)))
  has_media_marker <- any(sapply(lay_exclude, function(x) grepl(x, from_lower, fixed = TRUE)))
  
  if (is_likely_social && has_devotional && !has_media_marker) {
    return("Lay Influencers")
  }
  
  return("Other")
}

# Apply classification
dta[, ACTOR_TYPE := mapply(classify_actor_v4, FROM, URL, SOURCE_TYPE)]

cat("ACTOR_TYPE class:", class(dta$ACTOR_TYPE), "\n")
ACTOR_TYPE class: character 
Show code
cat("ACTOR_TYPE distribution:\n")
ACTOR_TYPE distribution:
Show code
print(table(dta$ACTOR_TYPE))

               Academic Charismatic Communities                Diocesan 
                    671                    1282                   13841 
      Independent Media  Institutional Official         Lay Influencers 
                  31401                   62744                   28451 
                  Other        Religious Orders     Youth Organizations 
                 468760                    1677                      52 

1.2 Corpus Overview

Show code
tibble(
  Metric = c("Total posts", "Unique sources", "Date range", "Platforms",
             "Years covered", "Posts with timestamp"),
  Value = c(
    format(n_posts, big.mark = ","),
    format(n_sources, big.mark = ","),
    date_range,
    paste(unique(dta$SOURCE_TYPE), collapse = ", "),
    paste(sort(unique(dta$Year)), collapse = ", "),
    format(sum(!is.na(dta$Hour)), big.mark = ",")
  )
) %>%
  kable(col.names = c("Metric", "Value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Metric Value
Total posts 608,879
Unique sources 16,426
Date range 2021-01-01 to 2025-12-31
Platforms web, facebook, twitter, comment, youtube, forum, reddit, instagram
Years covered 2021, 2022, 2023, 2024, 2025
Posts with timestamp 608,879

2 Analysis 4.1: Overall Temporal Patterns

The first analysis examines macro level trends in posting volume and engagement over the corpus period. This reveals overall growth or decline patterns and identifies major activity peaks.

Reading the charts:

  • Daily volume shows raw activity with smoothing to reveal trends
  • 7 day rolling average captures weekly patterns while reducing noise
  • 30 day rolling average shows monthly trend direction
  • YoY change indicates whether activity is growing or declining relative to prior year
Show code
daily_stats <- dta[, .(
  Posts = .N,
  Total_Interactions = sum(INTERACTIONS, na.rm = TRUE),
  Mean_Interactions = mean(INTERACTIONS, na.rm = TRUE),
  Unique_Sources = uniqueN(FROM)
), by = DATE][order(DATE)]

daily_stats[, Rolling_Posts_7d := frollmean(Posts, 7, align = "right")]
daily_stats[, Rolling_Posts_30d := frollmean(Posts, 30, align = "right")]
daily_stats[, Rolling_Interactions_7d := frollmean(Total_Interactions, 7, align = "right")]

2.1 Daily Posting Volume

Show code
ggplot(daily_stats[!is.na(DATE)], aes(x = DATE)) +
  geom_line(aes(y = Posts), alpha = 0.3, color = "gray50") +
  geom_line(aes(y = Rolling_Posts_7d), color = "#2c5f7c", linewidth = 0.8) +
  geom_line(aes(y = Rolling_Posts_30d), color = "#e07b39", linewidth = 1.2) +
  scale_x_date(date_labels = "%Y-%m", date_breaks = "6 months") +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Daily Posting Volume Over Time",
    subtitle = "Gray: daily count | Blue: 7 day rolling average | Orange: 30 day rolling average",
    x = NULL,
    y = "Number of Posts"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

2.3 Year over Year Comparison

Show code
yearly_stats <- dta[, .(
  Posts = .N,
  Total_Interactions = sum(INTERACTIONS, na.rm = TRUE),
  Mean_Interactions = mean(INTERACTIONS, na.rm = TRUE),
  Unique_Sources = uniqueN(FROM)
), by = Year][order(Year)]

yearly_stats[, Posts_Change := (Posts - shift(Posts)) / shift(Posts) * 100]
yearly_stats[, Interactions_Change := (Total_Interactions - shift(Total_Interactions)) / shift(Total_Interactions) * 100]

yearly_long <- yearly_stats %>%
  select(Year, Posts, Total_Interactions) %>%
  pivot_longer(cols = -Year, names_to = "Metric", values_to = "Value") %>%
  mutate(Metric = ifelse(Metric == "Posts", "Volume (Posts)", "Engagement (Interactions)"))

ggplot(yearly_long, aes(x = factor(Year), y = Value, fill = Metric)) +
  geom_col(position = "dodge", width = 0.7) +
  scale_fill_manual(values = c("Volume (Posts)" = "#2c5f7c", "Engagement (Interactions)" = "#4a9c6d")) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Annual Volume and Engagement",
    subtitle = "Year over year comparison",
    x = NULL,
    y = "Count",
    fill = NULL
  ) +
  theme(legend.position = "top")

2.4 Yearly Statistics Table

Show code
yearly_stats %>%
  mutate(
    Posts = format(Posts, big.mark = ","),
    Total_Interactions = format(Total_Interactions, big.mark = ","),
    Mean_Interactions = sprintf("%.1f", Mean_Interactions),
    Unique_Sources = format(Unique_Sources, big.mark = ","),
    Posts_Change = ifelse(is.na(Posts_Change), "—", sprintf("%+.1f%%", Posts_Change)),
    Interactions_Change = ifelse(is.na(Interactions_Change), "—", sprintf("%+.1f%%", Interactions_Change))
  ) %>%
  select(Year, Posts, Posts_Change, Total_Interactions, Interactions_Change, 
         Mean_Interactions, Unique_Sources) %>%
  kable(col.names = c("Year", "Posts", "YoY Change", "Interactions", "YoY Change", 
                      "Mean Int.", "Sources")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Year Posts YoY Change Interactions YoY Change Mean Int. Sources
2021 90,388 12,936,397 149.2 2,830
2022 84,535 -6.5% 10,779,254 -16.7% 131.6 3,137
2023 83,834 -0.8% 7,943,765 -26.3% 97.2 2,749
2024 114,187 +36.2% 9,139,533 +15.1% 82.1 6,088
2025 235,935 +106.6% 21,171,395 +131.6% 92.7 10,146

2.5 Peak Activity Days

Click to expand (Top 20 highest volume days)
Show code
top_days <- daily_stats[order(-Posts)][1:20]

top_days %>%
  mutate(
    Posts = format(Posts, big.mark = ","),
    Total_Interactions = format(Total_Interactions, big.mark = ","),
    Unique_Sources = format(Unique_Sources, big.mark = ",")
  ) %>%
  select(DATE, Posts, Total_Interactions, Unique_Sources) %>%
  kable(col.names = c("Date", "Posts", "Total Interactions", "Unique Sources")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Date Posts Total Interactions Unique Sources
2025-04-21 3,586 475,063 740
2025-04-22 2,259 149,956 579
2025-04-26 2,009 142,126 461
2025-04-23 1,560 148,731 507
2025-04-20 1,427 94,267 448
2024-12-25 1,387 79,702 433
2025-08-15 1,350 194,927 493
2025-04-24 1,324 79,308 430
2025-12-25 1,322 145,010 424
2025-04-25 1,297 105,827 396
2025-04-18 1,170 75,538 493
2025-05-08 1,163 238,433 433
2025-10-31 1,152 100,222 452
2025-12-24 1,150 113,818 483
2025-04-19 1,141 59,931 414
2024-12-24 1,120 73,018 452
2024-08-15 1,114 116,573 379
2025-11-29 1,000 137,968 377
2025-12-02 999 129,046 439
2025-04-27 996 71,676 338

3 Analysis 4.2: Liturgical Calendar Effects

Croatian Catholic digital communication is expected to follow liturgical rhythms. This analysis maps posting patterns to the Catholic liturgical calendar to identify seasonal effects.

Key seasons:

  • Advent: Four weeks before Christmas (preparation)
  • Christmas: December 25 through Epiphany (celebration)
  • Lent: Ash Wednesday to Holy Thursday (penitence)
  • Easter: Easter Sunday through Pentecost (celebration)
  • Ordinary Time: All other periods
Show code
calculate_easter <- function(year) {
  a <- year %% 19
  b <- year %/% 100
  c <- year %% 100
  d <- b %/% 4
  e <- b %% 4
  f <- (b + 8) %/% 25
  g <- (b - f + 1) %/% 3
  h <- (19 * a + b - d - g + 15) %% 30
  i <- c %/% 4
  k <- c %% 4
  l <- (32 + 2 * e + 2 * i - h - k) %% 7
  m <- (a + 11 * h + 22 * l) %/% 451
  month <- (h + l - 7 * m + 114) %/% 31
  day <- ((h + l - 7 * m + 114) %% 31) + 1
  as.Date(paste(year, month, day, sep = "-"))
}

assign_liturgical_season <- function(date) {
  year <- year(date)
  easter <- calculate_easter(year)
  
  ash_wednesday <- easter - 46
  palm_sunday <- easter - 7
  pentecost <- easter + 49
  
  advent_start <- as.Date(paste(year, "11", "27", sep = "-"))
  advent_start <- advent_start + (7 - lubridate::wday(advent_start) + 1) %% 7
  if (advent_start > as.Date(paste(year, "12", "03", sep = "-"))) {
    advent_start <- advent_start - 7
  }
  
  christmas <- as.Date(paste(year, "12", "25", sep = "-"))
  epiphany <- as.Date(paste(year + 1, "01", "06", sep = "-"))
  baptism_of_lord <- epiphany + (7 - lubridate::wday(epiphany)) %% 7 + 1
  if (lubridate::wday(epiphany) == 1) baptism_of_lord <- epiphany + 7
  
  prev_year_christmas <- as.Date(paste(year - 1, "12", "25", sep = "-"))
  prev_epiphany <- as.Date(paste(year, "01", "06", sep = "-"))
  
  if (date >= advent_start) {
    if (date < christmas) return("Advent")
    else return("Christmas")
  }
  
  if (date < prev_epiphany + 8 && month(date) == 1 && day(date) <= 13) {
    return("Christmas")
  }
  
  if (date >= ash_wednesday && date < easter) {
    return("Lent")
  }
  
  if (date >= easter && date < pentecost) {
    return("Easter")
  }
  
  return("Ordinary Time")
}

dta[, Liturgical_Season := sapply(DATE, assign_liturgical_season)]

3.1 Volume by Liturgical Season

Show code
season_stats <- dta[, .(
  Posts = .N,
  Total_Interactions = sum(INTERACTIONS, na.rm = TRUE),
  Mean_Interactions = mean(INTERACTIONS, na.rm = TRUE),
  Days = uniqueN(DATE)
), by = Liturgical_Season]

season_stats[, Posts_Per_Day := Posts / Days]
season_stats[, Interactions_Per_Day := Total_Interactions / Days]

season_order <- c("Advent", "Christmas", "Ordinary Time", "Lent", "Easter")
season_stats[, Liturgical_Season := factor(Liturgical_Season, levels = season_order)]

ggplot(season_stats[!is.na(Liturgical_Season)], 
       aes(x = Liturgical_Season, y = Posts_Per_Day, fill = Liturgical_Season)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = sprintf("%.0f", Posts_Per_Day)), vjust = -0.3) +
  scale_fill_manual(values = season_colors) +
  labs(
    title = "Average Daily Posting Volume by Liturgical Season",
    subtitle = "Posts per day normalized for season length",
    x = NULL,
    y = "Posts per Day"
  ) +
  theme(legend.position = "none")

3.2 Engagement by Liturgical Season

Show code
ggplot(season_stats[!is.na(Liturgical_Season)], 
       aes(x = Liturgical_Season, y = Mean_Interactions, fill = Liturgical_Season)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = sprintf("%.1f", Mean_Interactions)), vjust = -0.3) +
  scale_fill_manual(values = season_colors) +
  labs(
    title = "Mean Engagement by Liturgical Season",
    subtitle = "Average interactions per post",
    x = NULL,
    y = "Mean Interactions"
  ) +
  theme(legend.position = "none")

3.3 Liturgical Season Statistics

Show code
season_stats[order(Liturgical_Season)] %>%
  mutate(
    Posts = format(Posts, big.mark = ","),
    Total_Interactions = format(Total_Interactions, big.mark = ","),
    Mean_Interactions = sprintf("%.1f", Mean_Interactions),
    Posts_Per_Day = sprintf("%.0f", Posts_Per_Day),
    Interactions_Per_Day = sprintf("%.0f", Interactions_Per_Day)
  ) %>%
  select(Liturgical_Season, Days, Posts, Posts_Per_Day, Total_Interactions, 
         Interactions_Per_Day, Mean_Interactions) %>%
  kable(col.names = c("Season", "Days", "Posts", "Posts/Day", "Interactions", 
                      "Int./Day", "Mean Int.")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Season Days Posts Posts/Day Interactions Int./Day Mean Int.
Advent 124 57,617 465 5,022,468 40504 89.1
Christmas 93 36,805 396 4,453,420 47886 126.1
Ordinary Time 1030 378,292 367 38,139,503 37029 104.2
Lent 184 62,732 341 6,470,433 35165 105.8
Easter 190 73,433 386 7,884,520 41497 111.2

3.4 Major Feast Days Analysis

Feast days are specific dates that may generate activity spikes. Effect size measures how much activity increases compared to baseline.

Show code
feast_days <- data.table(
  Feast = c(
    "Christmas", "Easter Sunday", "All Saints", "Assumption", 
    "Epiphany", "Ash Wednesday", "Palm Sunday", "Pentecost",
    "Corpus Christi", "St. Joseph", "Annunciation", "Immaculate Conception"
  ),
  Month = c(12, NA, 11, 8, 1, NA, NA, NA, NA, 3, 3, 12),
  Day = c(25, NA, 1, 15, 6, NA, NA, NA, NA, 19, 25, 8),
  Type = c("Fixed", "Moveable", "Fixed", "Fixed", "Fixed", "Moveable", 
           "Moveable", "Moveable", "Moveable", "Fixed", "Fixed", "Fixed")
)

get_feast_dates <- function(feast_name, year) {
  easter <- calculate_easter(year)
  
  switch(feast_name,
    "Christmas" = as.Date(paste(year, 12, 25, sep = "-")),
    "Easter Sunday" = easter,
    "All Saints" = as.Date(paste(year, 11, 1, sep = "-")),
    "Assumption" = as.Date(paste(year, 8, 15, sep = "-")),
    "Epiphany" = as.Date(paste(year, 1, 6, sep = "-")),
    "Ash Wednesday" = easter - 46,
    "Palm Sunday" = easter - 7,
    "Pentecost" = easter + 49,
    "Corpus Christi" = easter + 60,
    "St. Joseph" = as.Date(paste(year, 3, 19, sep = "-")),
    "Annunciation" = as.Date(paste(year, 3, 25, sep = "-")),
    "Immaculate Conception" = as.Date(paste(year, 12, 8, sep = "-")),
    NA
  )
}

years <- unique(dta$Year)
feast_analysis <- data.table()

for (yr in years) {
  for (feast in feast_days$Feast) {
    feast_date <- get_feast_dates(feast, yr)
    if (!is.na(feast_date)) {
      window_start <- feast_date - 1
      window_end <- feast_date + 1
      
      feast_posts <- dta[DATE >= window_start & DATE <= window_end]
      baseline_posts <- dta[Year == yr & !(DATE >= window_start & DATE <= window_end)]
      
      if (nrow(feast_posts) > 0 && nrow(baseline_posts) > 0) {
        feast_analysis <- rbind(feast_analysis, data.table(
          Feast = feast,
          Year = yr,
          Date = feast_date,
          Posts = nrow(feast_posts),
          Mean_Daily_Posts = nrow(feast_posts) / 3,
          Baseline_Daily = nrow(baseline_posts) / uniqueN(baseline_posts$DATE),
          Total_Interactions = sum(feast_posts$INTERACTIONS, na.rm = TRUE),
          Mean_Interactions = mean(feast_posts$INTERACTIONS, na.rm = TRUE)
        ))
      }
    }
  }
}

feast_analysis[, Effect_Size := (Mean_Daily_Posts - Baseline_Daily) / Baseline_Daily * 100]

feast_summary <- feast_analysis[, .(
  Observations = .N,
  Mean_Posts_Per_Day = mean(Mean_Daily_Posts, na.rm = TRUE),
  Mean_Effect_Size = mean(Effect_Size, na.rm = TRUE),
  Mean_Interactions = mean(Mean_Interactions, na.rm = TRUE)
), by = Feast][order(-Mean_Effect_Size)]

ggplot(feast_summary, aes(x = reorder(Feast, Mean_Effect_Size), 
                          y = Mean_Effect_Size, fill = Mean_Effect_Size > 0)) +
  geom_col(width = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  geom_text(aes(label = sprintf("%+.0f%%", Mean_Effect_Size)), 
            hjust = ifelse(feast_summary$Mean_Effect_Size > 0, -0.1, 1.1), size = 3.5) +
  coord_flip() +
  scale_fill_manual(values = c("TRUE" = "#22c55e", "FALSE" = "#ef4444")) +
  labs(
    title = "Feast Day Effect on Posting Volume",
    subtitle = "Percentage change from baseline (3 day window around feast)",
    x = NULL,
    y = "Effect Size (%)"
  ) +
  theme(legend.position = "none")

3.5 Feast Day Statistics Table

Show code
feast_summary %>%
  mutate(
    Mean_Posts_Per_Day = sprintf("%.0f", Mean_Posts_Per_Day),
    Mean_Effect_Size = sprintf("%+.1f%%", Mean_Effect_Size),
    Mean_Interactions = sprintf("%.1f", Mean_Interactions)
  ) %>%
  kable(col.names = c("Feast Day", "Years Observed", "Mean Posts/Day", 
                      "Effect Size", "Mean Interactions")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Feast Day Years Observed Mean Posts/Day Effect Size Mean Interactions
Easter Sunday 4 827 +107.7% 97.6
Christmas 5 720 +91.2% 81.6
Assumption 5 570 +48.0% 146.8
Palm Sunday 4 431 +29.5% 113.9
All Saints 5 482 +23.5% 86.1
Corpus Christi 4 386 +12.3% 133.7
Ash Wednesday 4 412 +11.7% 94.3
Epiphany 5 387 +9.9% 148.9
Immaculate Conception 5 425 +8.7% 98.5
St. Joseph 4 371 +3.1% 106.5
Annunciation 4 356 -0.8% 122.0
Pentecost 4 334 -7.2% 170.0

4 Analysis 4.3: Event Response

This analysis examines how Croatian Catholic digital media responds to specific known events during the corpus period.

Event types:

  • Youth Event: SHKM and similar gatherings
  • Vatican: Papal and Holy See announcements
  • Political: Elections and policy events
  • Media: Infrastructure changes in Catholic media
Show code
known_events <- data.table(
  Event = c(
    "SHKM 2022",
    "SHKM 2024", 
    "Laudato TV Studio Opening",
    "Fiducia Supplicans",
    "Croatian Parliamentary Elections 2024",
    "Pope Francis Health Concerns",
    "World Youth Day 2023"
  ),
  Date = as.Date(c(
    "2022-04-30",
    "2024-04-27",
    "2021-05-15",
    "2023-12-18",
    "2024-04-17",
    "2023-03-29",
    "2023-08-01"
  )),
  Type = c(
    "Youth Event",
    "Youth Event",
    "Media",
    "Vatican",
    "Political",
    "Vatican",
    "Youth Event"
  ),
  Description = c(
    "Susret hrvatske katoličke mladeži",
    "Susret hrvatske katoličke mladeži",
    "Cardinal Bozanić blessed new studio",
    "Vatican declaration on same sex blessings",
    "Croatian parliamentary elections",
    "Pope Francis hospitalization",
    "WYD Lisbon"
  )
)

4.1 Event Response Profiles

Show code
event_response <- data.table()

for (i in 1:nrow(known_events)) {
  event_date <- known_events$Date[i]
  event_name <- known_events$Event[i]
  
  window_start <- event_date - 7
  window_end <- event_date + 14
  
  daily_response <- dta[DATE >= window_start & DATE <= window_end, .(
    Posts = .N,
    Interactions = sum(INTERACTIONS, na.rm = TRUE)
  ), by = DATE]
  
  daily_response[, `:=`(
    Event = event_name,
    Days_From_Event = as.integer(DATE - event_date)
  )]
  
  event_response <- rbind(event_response, daily_response)
}

event_response <- merge(event_response, known_events[, .(Event, Type)], by = "Event")

ggplot(event_response, aes(x = Days_From_Event, y = Posts, color = Event)) +
  geom_line(linewidth = 1) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "red") +
  facet_wrap(~Event, scales = "free_y", ncol = 2) +
  scale_x_continuous(breaks = seq(-7, 14, 7)) +
  labs(
    title = "Event Response Profiles",
    subtitle = "Daily posting volume around major events (red line = event day)",
    x = "Days from Event",
    y = "Posts"
  ) +
  theme(legend.position = "none")

4.2 Event Impact Summary

Show code
event_impact <- event_response[, .(
  Pre_Event_Mean = mean(Posts[Days_From_Event < 0], na.rm = TRUE),
  Event_Day = sum(Posts[Days_From_Event == 0], na.rm = TRUE),
  Post_Event_Mean = mean(Posts[Days_From_Event > 0 & Days_From_Event <= 7], na.rm = TRUE),
  Peak_Day = max(Posts, na.rm = TRUE),
  Total_Posts = sum(Posts, na.rm = TRUE),
  Total_Interactions = sum(Interactions, na.rm = TRUE)
), by = .(Event, Type)]

event_impact[, Event_Spike := (Event_Day - Pre_Event_Mean) / Pre_Event_Mean * 100]

event_impact[order(-Event_Spike)] %>%
  mutate(
    Pre_Event_Mean = sprintf("%.0f", Pre_Event_Mean),
    Event_Day = format(Event_Day, big.mark = ","),
    Post_Event_Mean = sprintf("%.0f", Post_Event_Mean),
    Peak_Day = format(Peak_Day, big.mark = ","),
    Total_Posts = format(Total_Posts, big.mark = ","),
    Event_Spike = sprintf("%+.0f%%", Event_Spike)
  ) %>%
  select(Event, Type, Pre_Event_Mean, Event_Day, Event_Spike, Peak_Day, Total_Posts) %>%
  kable(col.names = c("Event", "Type", "Pre Event Avg", "Event Day", 
                      "Spike %", "Peak Day", "Total Posts")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Event Type Pre Event Avg Event Day Spike % Peak Day Total Posts
Fiducia Supplicans Vatican 225 396 +76% 605 6,273
Pope Francis Health Concerns Vatican 271 360 +33% 507 7,089
World Youth Day 2023 Youth Event 167 197 +18% 523 4,775
Laudato TV Studio Opening Media 212 236 +11% 279 4,338
SHKM 2022 Youth Event 254 235 -7% 339 5,326

4.3 Actor Response to Events

Show code
actor_event_response <- data.table()

for (i in 1:nrow(known_events)) {
  event_date <- known_events$Date[i]
  event_name <- known_events$Event[i]
  
  window_start <- event_date - 3
  window_end <- event_date + 7
  
  actor_response <- dta[DATE >= window_start & DATE <= window_end, .(
    Posts = .N,
    Interactions = sum(INTERACTIONS, na.rm = TRUE)
  ), by = ACTOR_TYPE]
  
  actor_response[, Event := event_name]
  actor_event_response <- rbind(actor_event_response, actor_response)
}

actor_event_wide <- dcast(actor_event_response, ACTOR_TYPE ~ Event, value.var = "Posts", fill = 0)

actor_event_long <- actor_event_response[ACTOR_TYPE != "Other"]

ggplot(actor_event_long, aes(x = reorder(ACTOR_TYPE, Posts), y = Posts, fill = ACTOR_TYPE)) +
  geom_col(width = 0.7) +
  facet_wrap(~Event, scales = "free_x", ncol = 2) +
  coord_flip() +
  scale_fill_manual(values = actor_colors) +
  labs(
    title = "Actor Type Response to Major Events",
    subtitle = "Posts within event window (3 days before to 7 days after)",
    x = NULL,
    y = "Posts"
  ) +
  theme(legend.position = "none")

5 Analysis 4.4: Platform Migration

This analysis tracks how platform composition changes over time, revealing shifts in where Croatian Catholic digital communication occurs.

Show code
platform_monthly <- dta[, .(Posts = .N), by = .(Year, Month, SOURCE_TYPE)]
platform_monthly[, Date := as.Date(paste(Year, Month, "01", sep = "-"))]
platform_monthly[, Total := sum(Posts), by = Date]
platform_monthly[, Share := Posts / Total * 100]

ggplot(platform_monthly[!is.na(Date)], aes(x = Date, y = Share, fill = SOURCE_TYPE)) +
  geom_area(alpha = 0.8) +
  scale_fill_manual(values = platform_colors) +
  scale_x_date(date_labels = "%Y-%m", date_breaks = "6 months") +
  labs(
    title = "Platform Composition Over Time",
    subtitle = "Share of posts by platform (stacked area)",
    x = NULL,
    y = "Share (%)",
    fill = "Platform"
  ) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "right"
  )

5.2 Platform Growth Rates

Show code
platform_first_last <- platform_yearly_long[Year %in% c(min(Year), max(Year))]
platform_growth <- dcast(platform_first_last, SOURCE_TYPE ~ Year, value.var = "Posts")

names(platform_growth)[2:3] <- c("First_Year", "Last_Year")
platform_growth[, Growth_Rate := (Last_Year - First_Year) / First_Year * 100]
platform_growth[, CAGR := ((Last_Year / First_Year)^(1/(max(platform_yearly_long$Year) - min(platform_yearly_long$Year))) - 1) * 100]

ggplot(platform_growth[!is.na(Growth_Rate) & is.finite(Growth_Rate)], 
       aes(x = reorder(SOURCE_TYPE, Growth_Rate), y = Growth_Rate, 
           fill = Growth_Rate > 0)) +
  geom_col(width = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  geom_text(aes(label = sprintf("%+.0f%%", Growth_Rate)), 
            hjust = ifelse(platform_growth[!is.na(Growth_Rate) & is.finite(Growth_Rate)]$Growth_Rate > 0, -0.1, 1.1),
            size = 3.5) +
  coord_flip() +
  scale_fill_manual(values = c("TRUE" = "#22c55e", "FALSE" = "#ef4444")) +
  labs(
    title = "Platform Growth Rates",
    subtitle = paste("Total change from", min(platform_yearly_long$Year), "to", max(platform_yearly_long$Year)),
    x = NULL,
    y = "Growth Rate (%)"
  ) +
  theme(legend.position = "none")

5.3 Platform Statistics Table

Show code
platform_growth %>%
  mutate(
    First_Year = format(First_Year, big.mark = ","),
    Last_Year = format(Last_Year, big.mark = ","),
    Growth_Rate = sprintf("%+.1f%%", Growth_Rate),
    CAGR = sprintf("%+.1f%%", CAGR)
  ) %>%
  arrange(desc(as.numeric(gsub(",", "", Last_Year)))) %>%
  kable(col.names = c("Platform", paste(min(platform_yearly_long$Year)), 
                      paste(max(platform_yearly_long$Year)), "Total Growth", "CAGR")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Platform 2021 2025 Total Growth CAGR
web 77,988 152,048 +95.0% +18.2%
facebook 4,115 44,502 +981.5% +81.3%
youtube 5,030 26,891 +434.6% +52.1%
reddit 312 4,228 +1255.1% +91.9%
twitter 666 2,724 +309.0% +42.2%
instagram 0 2,122 +Inf% +Inf%
forum 1,032 1,779 +72.4% +14.6%
comment 1,245 1,641 +31.8% +7.1%

6 Analysis 4.5: Posting Rhythms

This analysis examines temporal patterns at the micro level, revealing when Croatian Catholic digital content is created.

Reading the charts:

  • Hourly patterns show peak activity times
  • Day of week patterns reveal weekly rhythms
  • Heatmap combines hour and day for full picture
  • Sunday Index above 1.0 means higher activity than weekday average
Show code
if ("TIME" %in% names(dta)) {
  dta[, Hour := as.integer(substr(TIME, 1, 2))]
} else {
  time_cols <- names(dta)[grepl("time|hour|TIME|HOUR", names(dta), ignore.case = TRUE)]
  cat("Available time-related columns:", paste(time_cols, collapse = ", "), "\n")
  if (length(time_cols) == 0) {
    cat("No time data available. Creating random hours for demonstration.\n")
    dta[, Hour := sample(0:23, .N, replace = TRUE)]
  }
}

if (!"DOW" %in% names(dta) && "DATE" %in% names(dta)) {
  dta[, DOW := lubridate::wday(DATE, label = TRUE, abbr = FALSE)]
  dta[, DOW_num := lubridate::wday(DATE)]
}

hourly_stats <- dta[!is.na(Hour), .(Posts = .N), by = Hour][order(Hour)]

ggplot(hourly_stats, aes(x = Hour, y = Posts)) +
  geom_col(fill = "#2c5f7c", width = 0.8) +
  geom_line(aes(group = 1), color = "#e07b39", linewidth = 1) +
  scale_x_continuous(breaks = 0:23) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Posting Volume by Hour of Day",
    subtitle = "Aggregated across all days in corpus",
    x = "Hour",
    y = "Total Posts"
  )

6.1 Day of Week Patterns

Show code
dow_stats <- dta[!is.na(DOW), .(
  Posts = .N,
  Mean_Interactions = mean(INTERACTIONS, na.rm = TRUE)
), by = DOW]

dow_order <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
dow_stats[, DOW := factor(DOW, levels = dow_order)]

ggplot(dow_stats[!is.na(DOW)], aes(x = DOW, y = Posts, fill = DOW == "Sunday")) +
  geom_col(width = 0.7) +
  geom_text(aes(label = format(Posts, big.mark = ",")), vjust = -0.3, size = 3.5) +
  scale_fill_manual(values = c("TRUE" = "#e07b39", "FALSE" = "#2c5f7c")) +
  scale_y_continuous(labels = comma, expand = expansion(mult = c(0, 0.1))) +
  labs(
    title = "Posting Volume by Day of Week",
    subtitle = "Sunday highlighted",
    x = NULL,
    y = "Total Posts"
  ) +
  theme(legend.position = "none")

6.2 Hour by Day Heatmap

Show code
hour_dow <- dta[!is.na(Hour) & !is.na(DOW), .(Posts = .N), by = .(DOW, Hour)]
hour_dow[, DOW := factor(DOW, levels = dow_order)]

ggplot(hour_dow[!is.na(DOW)], aes(x = Hour, y = DOW, fill = Posts)) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_viridis(option = "plasma", labels = comma) +
  scale_x_continuous(breaks = seq(0, 23, 2)) +
  labs(
    title = "Posting Intensity Heatmap",
    subtitle = "Hour of day by day of week",
    x = "Hour",
    y = NULL,
    fill = "Posts"
  ) +
  theme(panel.grid = element_blank())

6.3 Posting Rhythms by Actor Type

Show code
actor_hourly <- dta[!is.na(Hour) & ACTOR_TYPE != "Other", .(Posts = .N), by = .(ACTOR_TYPE, Hour)]
actor_hourly[, Total := sum(Posts), by = ACTOR_TYPE]
actor_hourly[, Share := Posts / Total * 100]

ggplot(actor_hourly, aes(x = Hour, y = Share, fill = ACTOR_TYPE)) +
  geom_col(width = 0.9) +
  facet_wrap(~ACTOR_TYPE, ncol = 3, scales = "free_y") +
  scale_fill_manual(values = actor_colors) +
  scale_x_continuous(breaks = seq(0, 23, 6)) +
  labs(
    title = "Hourly Posting Patterns by Actor Type",
    subtitle = "Share of each actor's posts by hour",
    x = "Hour",
    y = "Share (%)"
  ) +
  theme(legend.position = "none")

6.4 Institutional vs Personal Rhythms

Show code
dta[, Rhythm_Type := fifelse(
  ACTOR_TYPE %in% c("Institutional Official", "Diocesan", "Academic"),
  "Institutional",
  "Personal/Grassroots"
)]

rhythm_hourly <- dta[!is.na(Hour), .(Posts = .N), by = .(Rhythm_Type, Hour)]
rhythm_hourly[, Total := sum(Posts), by = Rhythm_Type]
rhythm_hourly[, Share := Posts / Total * 100]

ggplot(rhythm_hourly, aes(x = Hour, y = Share, color = Rhythm_Type)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  annotate("rect", xmin = 9, xmax = 17, ymin = 0, ymax = Inf, 
           alpha = 0.1, fill = "blue") +
  scale_color_manual(values = c("Institutional" = "#1a3c5a", "Personal/Grassroots" = "#e07b39")) +
  scale_x_continuous(breaks = 0:23) +
  labs(
    title = "Institutional vs Personal Posting Rhythms",
    subtitle = "Blue shaded area = typical office hours (9 to 17)",
    x = "Hour",
    y = "Share of Posts (%)",
    color = NULL
  ) +
  theme(legend.position = "top")

6.5 Sunday Morning Analysis

The Sunday Index compares Sunday posting volume to weekday average at each hour. Values above 1.0 indicate higher Sunday activity.

Show code
sunday_data <- dta[DOW == "Sunday" & !is.na(Hour)]
sunday_hourly <- sunday_data[, .(Posts = .N), by = Hour]

other_days <- dta[DOW != "Sunday" & !is.na(Hour)]
other_hourly <- other_days[, .(Posts = .N / 6), by = Hour]

sunday_comparison <- merge(
  sunday_hourly[, .(Hour, Sunday_Posts = Posts)],
  other_hourly[, .(Hour, Other_Days_Avg = Posts)],
  by = "Hour"
)

sunday_comparison[, Sunday_Index := Sunday_Posts / Other_Days_Avg]

ggplot(sunday_comparison, aes(x = Hour, y = Sunday_Index)) +
  geom_col(aes(fill = Sunday_Index > 1), width = 0.8) +
  geom_hline(yintercept = 1, linetype = "dashed", color = "red") +
  annotate("rect", xmin = 6, xmax = 12, ymin = 0, ymax = Inf, 
           alpha = 0.1, fill = "gold") +
  scale_fill_manual(values = c("TRUE" = "#22c55e", "FALSE" = "#ef4444")) +
  scale_x_continuous(breaks = 0:23) +
  labs(
    title = "Sunday Posting Pattern",
    subtitle = "Ratio of Sunday posts to weekday average (gold = typical Mass times)",
    x = "Hour",
    y = "Sunday Index (1 = weekday average)"
  ) +
  theme(legend.position = "none")

7 Analysis 4.6: Information Cascades

This analysis examines how content spreads across platforms and sources by tracking platform activity correlations.

Show code
extract_urls <- function(text) {
  if (is.na(text)) return(NA)
  urls <- str_extract_all(text, "https?://[^\\s<>\"']+")[[1]]
  if (length(urls) == 0) return(NA)
  return(paste(urls, collapse = ";"))
}

url_sample <- dta[sample(.N, min(100000, .N))]
url_sample[, Extracted_URLs := sapply(FULL_TEXT, extract_urls)]

7.1 Cross Platform Content Sharing

Show code
same_day_spread <- dta[, .(
  Platforms = uniqueN(SOURCE_TYPE),
  Sources = uniqueN(FROM),
  Total_Posts = .N
), by = DATE][Platforms > 1]

ggplot(same_day_spread, aes(x = Platforms)) +
  geom_histogram(binwidth = 1, fill = "#2c5f7c", color = "white") +
  scale_x_continuous(breaks = 1:8) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Daily Cross Platform Activity",
    subtitle = "Number of platforms active per day",
    x = "Number of Active Platforms",
    y = "Number of Days"
  )

7.2 Platform Interaction Patterns

Platform correlations reveal which platforms have synchronized activity patterns. High correlation suggests content flows between platforms or shared external triggers.

Show code
platform_daily <- dta[, .(Posts = .N), by = .(DATE, SOURCE_TYPE)]
platform_wide <- dcast(platform_daily, DATE ~ SOURCE_TYPE, value.var = "Posts", fill = 0)

platform_cor <- cor(platform_wide[, -1, with = FALSE], use = "complete.obs")

platform_cor_long <- as.data.table(as.table(platform_cor))
names(platform_cor_long) <- c("Platform1", "Platform2", "Correlation")

ggplot(platform_cor_long, aes(x = Platform1, y = Platform2, fill = Correlation)) +
  geom_tile(color = "white") +
  geom_text(aes(label = sprintf("%.2f", Correlation)), size = 3) +
  scale_fill_gradient2(low = "#ef4444", mid = "white", high = "#22c55e", 
                       midpoint = 0, limits = c(-1, 1)) +
  labs(
    title = "Platform Activity Correlation Matrix",
    subtitle = "Daily posting volume correlation between platforms",
    x = NULL,
    y = NULL,
    fill = "Correlation"
  ) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid = element_blank()
  )

7.3 Content Origin Analysis

Show code
first_post_by_source <- dta[, .(
  First_Post_Date = min(DATE),
  First_Post_Platform = SOURCE_TYPE[which.min(DATE)]
), by = FROM]

origin_platform <- first_post_by_source[, .(
  New_Sources = .N
), by = First_Post_Platform][order(-New_Sources)]

ggplot(origin_platform, aes(x = reorder(First_Post_Platform, New_Sources), 
                            y = New_Sources, fill = First_Post_Platform)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = format(New_Sources, big.mark = ",")), hjust = -0.1, size = 3.5) +
  coord_flip() +
  scale_fill_manual(values = platform_colors) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Platform of Origin for Sources",
    subtitle = "Where sources first appeared in the corpus",
    x = NULL,
    y = "Number of Sources"
  ) +
  theme(legend.position = "none")

7.4 Platform Peak Hours Table

Show code
hourly_platform <- dta[!is.na(Hour), .(Posts = .N), by = .(Hour, SOURCE_TYPE)]
hourly_platform[, Total := sum(Posts), by = SOURCE_TYPE]
hourly_platform[, Share := Posts / Total * 100]

peak_hours <- hourly_platform[, .(
  Peak_Hour = Hour[which.max(Posts)],
  Peak_Share = max(Share)
), by = SOURCE_TYPE][order(Peak_Hour)]

peak_hours %>%
  mutate(
    Peak_Hour = sprintf("%02d:00", Peak_Hour),
    Peak_Share = sprintf("%.1f%%", Peak_Share)
  ) %>%
  kable(col.names = c("Platform", "Peak Hour", "Share at Peak")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Platform Peak Hour Share at Peak
facebook 09:00 8.5%
web 10:00 7.6%
comment 11:00 6.3%
reddit 11:00 8.8%
twitter 13:00 6.7%
forum 14:00 7.5%
youtube 20:00 9.6%
instagram 21:00 7.8%

8 Summary and Key Findings

Show code
total_posts <- nrow(dta)
total_years <- length(unique(dta$Year))
avg_daily <- mean(daily_stats$Posts, na.rm = TRUE)
peak_day_posts <- max(daily_stats$Posts, na.rm = TRUE)
dominant_platform <- dta[, .N, by = SOURCE_TYPE][order(-N)][1]$SOURCE_TYPE

8.1 Temporal Dynamics Summary

Show code
cat("Sunday comparison data available:", nrow(sunday_comparison), "rows\n")
Sunday comparison data available: 0 rows
Show code
tibble(
  Finding = c(
    "Corpus period",
    "Total posts analyzed", 
    "Average daily posts",
    "Peak single day volume",
    "Dominant platform",
    "Most active liturgical season",
    "Peak posting hour",
    "Sunday index at 10:00"
  ),
  Value = c(
    date_range,
    format(total_posts, big.mark = ","),
    sprintf("%.0f", avg_daily),
    format(peak_day_posts, big.mark = ","),
    dominant_platform,
    as.character(season_stats[which.max(Posts_Per_Day)]$Liturgical_Season),
    sprintf("%02d:00", hourly_stats[which.max(Posts)]$Hour),
    ifelse(nrow(sunday_comparison[Hour == 10]) > 0, 
           sprintf("%.2f", sunday_comparison[Hour == 10]$Sunday_Index),
           "N/A")
  )
) %>%
  kable(col.names = c("Finding", "Value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Finding Value
Corpus period 2021-01-01 to 2025-12-31
Total posts analyzed 608,879
Average daily posts 376
Peak single day volume 3,586
Dominant platform web
Most active liturgical season Advent
Peak posting hour 09:00
Sunday index at 10:00 N/A

8.2 Hypotheses Testing Summary

Hypothesis Metric to Examine Finding
H14: Catholic digital activity follows liturgical calendar Season volume comparison See liturgical analysis
H15: Christmas and Easter show peak activity Feast day effect sizes Compare seasonal patterns
H16: Institutional actors post during office hours Hour by actor type Compare institutional vs personal rhythms
H17: Sunday shows distinctive patterns Sunday Index by hour See Sunday morning analysis
H18: Major events generate activity spikes Event response profiles Compare event spike percentages

9 Appendix: Classification Quality Diagnostics

Click to expand diagnostics (Quality assurance for actor classification)
Show code
actor_dist <- dta[, .(
  Posts = .N,
  Sources = uniqueN(FROM),
  Mean_Interactions = mean(INTERACTIONS, na.rm = TRUE)
), by = ACTOR_TYPE][order(-Posts)]

actor_dist %>%
  mutate(
    Posts = format(Posts, big.mark = ","),
    Sources = format(Sources, big.mark = ","),
    Mean_Interactions = round(Mean_Interactions, 1)
  ) %>%
  kable(col.names = c("Actor Type", "Posts", "Sources", "Mean Interactions"),
        caption = "Actor Type Distribution in Temporal Analysis") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Actor Type Distribution in Temporal Analysis
Actor Type Posts Sources Mean Interactions
Other 468,760 16,076 99.1
Institutional Official 62,744 202 56.6
Independent Media 31,401 18 210.2
Lay Influencers 28,451 62 224.7
Diocesan 13,841 50 46.0
Religious Orders 1,677 11 22.8
Charismatic Communities 1,282 12 153.7
Academic 671 5 17.3
Youth Organizations 52 4 128.8

10 Appendix: Technical Notes

10.1 Liturgical Calendar Algorithm

The liturgical season assignment uses the Computus algorithm to calculate Easter dates. Seasons are assigned as follows:

Advent begins four Sundays before Christmas and runs until December 24. Christmas begins December 25 and extends through Epiphany and the Baptism of the Lord. Lent begins on Ash Wednesday (46 days before Easter) and ends on Holy Thursday. Easter begins on Easter Sunday and extends through Pentecost (50 days). Ordinary Time fills the remaining periods.

10.2 Feast Day Effect Calculation

Feast day effects are calculated by comparing a three day window (day before, feast day, day after) against a yearly baseline excluding the feast window. The effect size represents the percentage difference from baseline daily volume.

10.3 Platform Growth Calculations

Compound Annual Growth Rate (CAGR) is calculated using the standard formula assuming geometric growth between first and last observed years in the corpus.

Show code
sessionInfo()
R version 4.5.2 (2025-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=Croatian_Croatia.utf8  LC_CTYPE=Croatian_Croatia.utf8   
[3] LC_MONETARY=Croatian_Croatia.utf8 LC_NUMERIC=C                     
[5] LC_TIME=Croatian_Croatia.utf8    

time zone: Europe/Zagreb
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] patchwork_1.3.2   zoo_1.8-14        viridis_0.6.5     viridisLite_0.4.2
 [5] kableExtra_1.4.0  knitr_1.50        scales_1.4.0      data.table_1.17.8
 [9] lubridate_1.9.4   forcats_1.0.1     stringr_1.6.0     dplyr_1.1.4      
[13] purrr_1.2.0       readr_2.1.6       tidyr_1.3.1       tibble_3.3.0     
[17] ggplot2_4.0.1     tidyverse_2.0.0  

loaded via a namespace (and not attached):
 [1] generics_0.1.4     xml2_1.5.1         lattice_0.22-7     stringi_1.8.7     
 [5] hms_1.1.4          digest_0.6.39      magrittr_2.0.4     evaluate_1.0.5    
 [9] grid_4.5.2         timechange_0.3.0   RColorBrewer_1.1-3 fastmap_1.2.0     
[13] Matrix_1.7-4       jsonlite_2.0.0     gridExtra_2.3      mgcv_1.9-3        
[17] textshaping_1.0.4  cli_3.6.5          rlang_1.1.6        splines_4.5.2     
[21] withr_3.0.2        yaml_2.3.11        tools_4.5.2        tzdb_0.5.0        
[25] vctrs_0.6.5        R6_2.6.1           lifecycle_1.0.4    htmlwidgets_1.6.4 
[29] pkgconfig_2.0.3    pillar_1.11.1      gtable_0.3.6       glue_1.8.0        
[33] systemfonts_1.3.1  xfun_0.54          tidyselect_1.2.1   rstudioapi_0.17.1 
[37] dichromat_2.0-0.1  farver_2.1.2       nlme_3.1-168       htmltools_0.5.8.1 
[41] labeling_0.4.3     rmarkdown_2.30     svglite_2.2.2      compiler_4.5.2    
[45] S7_0.2.1