This analysis evaluates Fonterra’s social-media content and user engagement across its official social-media platforms. The analysis is completed entirely in R, with data preparation, text analysis, topic modelling, exploratory analysis, and bivariate statistical analysis conducted in the order presented below.
# Set global chunk options so the knitted report is tidy and readable
knitr::opts_chunk$set(message = FALSE, warning = FALSE, fig.width = 8, fig.height = 5)
options(scipen = 999)
# Install and load packages
if(!require("pacman")) install.packages("pacman")
## Loading required package: pacman
pacman::p_load(readr, dplyr, tidyr, stringr, lubridate, purrr, ggplot2, gridExtra, GGally, corrplot)
pacman::p_load(tidytext, textstem, wordcloud, textdata, topicmodels)
pacman::p_load(DescTools, skimr, finalfit, rstatix, huxtable, jtools)
# Import the original dataset. The CSV is not modified manually outside R.
fonterra_raw <- read_csv("fonterra.csv", show_col_types = FALSE)
fonterra <- fonterra_raw
# Preview the structure and a concise sample of the data
glimpse(fonterra)
## Rows: 9,001
## Columns: 9
## $ platform <chr> "Twitter/X", "Twitter/X", "Twitter/X", "Twitter/X", "Tw…
## $ url <chr> "https://x.com/Fonterra/status/1891573780557988062", "h…
## $ createdAt <chr> "2025/02/17 19:41:47", "2024/12/05 22:23:33", "2024/12/…
## $ text <chr> "Today we announced new incentives to help farms reduce…
## $ media <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ commentCount <dbl> 66, 1, 1, 303, 8, 2, 2, 0, 2, 0, 1, 1, 1, 0, 0, 1, 0, 0…
## $ shareCount <dbl> 2, 0, 0, 102, 3, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 1, 0, 0,…
## $ likeCount <dbl> 15, 0, 0, 704, 16, 1, 6, 0, 19, 0, 16, 1, 3, 18, 2, 14,…
## $ videoplayCount <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
fonterra |>
mutate(text_preview = str_trunc(text, 80)) |>
select(platform, createdAt, media, commentCount, shareCount, likeCount,
videoplayCount, text_preview) |>
slice_head(n = 6) |>
knitr::kable(caption = "Preview of the Fonterra social-media dataset")
| platform | createdAt | media | commentCount | shareCount | likeCount | videoplayCount | text_preview |
|---|---|---|---|---|---|---|---|
| Twitter/X | 2025/02/17 19:41:47 | NA | 66 | 2 | 15 | NA | Today we announced new incentives to help farms reduce emissions. |
“We’re gro… | |Twitter/X |2024/12/05 22:23:33 |NA | 1| 0| 0| NA|@ahsan_ansaar Hi there, please reach out to us at talktous@fonterra.com. Our … | |Twitter/X |2024/12/05 21:05:34 |NA | 1| 0| 0| NA|@ahsan_ansaar Hi there, could you please DM us with some information about wh… | |Twitter/X |2024/12/01 23:04:24 |NA | 303| 102| 704| NA|At Fonterra, food safety and quality is our number one priority.
Bovaer has … | |Twitter/X |2024/11/10 19:39:51 |NA | 8| 3| 16| NA|We’ve lifted our forecast Farmgate Milk Price midpoint for the season by 50c … | |Twitter/X |2024/10/10 19:36:23 |NA | 2| 0| 1| NA|Today we announced a new Appointed Director, Alistair Field, who will join th… |
The dataset contains 9001 social-media records and 9 original variables. The records cover Twitter/X, Facebook, Instagram, YouTube, TikTok, and LinkedIn.
createdAt variable was converted from character
format to a UTC date-time so that dates and time-based variables could
be analysed correctly.media field used different labels for equivalent
formats. Photo, Image, and image
were standardised to Image; Video and
video were standardised to Video; and
Sidecar was retained as Sidecar.media, comment, share, and video-play values
were not automatically replaced with zero. Missingness
is strongly platform-dependent, so a missing value may indicate that the
metric was unavailable or not extracted rather than that the true value
was zero. Treating all missing values as zero could bias
comparisons.# Summarise missing data before making analytical decisions
missing_summary <- fonterra |>
summarise(across(everything(), ~ sum(is.na(.)))) |>
pivot_longer(everything(), names_to = "variable", values_to = "missing_n") |>
mutate(missing_pct = round(100 * missing_n / nrow(fonterra), 2))
missing_summary |>
knitr::kable(caption = "Missing values in the original dataset")
| variable | missing_n | missing_pct |
|---|---|---|
| platform | 0 | 0.00 |
| url | 0 | 0.00 |
| createdAt | 0 | 0.00 |
| text | 82 | 0.91 |
| media | 6994 | 77.70 |
| commentCount | 293 | 3.26 |
| shareCount | 951 | 10.57 |
| likeCount | 0 | 0.00 |
| videoplayCount | 8115 | 90.16 |
# Check exact duplicates, duplicate URLs, and repeated text/timestamp combinations
duplicate_summary <- tibble::tibble(
check = c("Exact duplicate rows",
"Duplicate URLs",
"Repeated text at the same timestamp"),
n = c(sum(duplicated(fonterra)),
sum(duplicated(fonterra$url)),
sum(duplicated(fonterra[c("createdAt", "text")])))
)
duplicate_summary |>
knitr::kable(caption = "Duplicate checks")
| check | n |
|---|---|
| Exact duplicate rows | 0 |
| Duplicate URLs | 0 |
| Repeated text at the same timestamp | 1 |
# Convert the timestamp to a proper UTC date-time and standardise platform text
fonterra <- fonterra |>
mutate(
createdAt = ymd_hms(createdAt, tz = "UTC", quiet = TRUE),
platform = str_squish(platform),
media_clean = case_when(
str_to_lower(media) %in% c("photo", "image") ~ "Image",
str_to_lower(media) == "video" ~ "Video",
str_to_lower(media) == "sidecar" ~ "Sidecar",
TRUE ~ NA_character_
)
)
# Check whether engagement counts contain impossible negative values
negative_value_check <- fonterra |>
summarise(across(c(commentCount, shareCount, likeCount, videoplayCount),
~ sum(. < 0, na.rm = TRUE))) |>
pivot_longer(everything(), names_to = "variable", values_to = "negative_values")
negative_value_check |>
knitr::kable(caption = "Check for negative engagement counts")
| variable | negative_values |
|---|---|
| commentCount | 0 |
| shareCount | 0 |
| likeCount | 0 |
| videoplayCount | 0 |
# Show missing engagement values by platform to assess whether missingness is systematic
missing_by_platform <- fonterra |>
group_by(platform) |>
summarise(
posts = n(),
missing_comments = sum(is.na(commentCount)),
missing_shares = sum(is.na(shareCount)),
missing_video_plays = sum(is.na(videoplayCount)),
missing_media = sum(is.na(media)),
.groups = "drop"
)
missing_by_platform |>
knitr::kable(caption = "Missing engagement and media data by platform")
| platform | posts | missing_comments | missing_shares | missing_video_plays | missing_media |
|---|---|---|---|---|---|
| 1433 | 269 | 0 | 1115 | 680 | |
| 519 | 0 | 519 | 454 | 0 | |
| 326 | 24 | 37 | 326 | 94 | |
| TikTok | 108 | 0 | 0 | 0 | 0 |
| Twitter/X | 6220 | 0 | 0 | 6220 | 6220 |
| YouTube | 395 | 0 | 395 | 0 | 0 |
The missing-value table confirms that missingness is not random across the variables. For example, share counts are unavailable for some platforms and video-play counts are available only for a subset of posts. Therefore, the analysis uses available observations for each metric and avoids interpreting unavailable values as zero.
document_id gives every social-media record a unique
identifier and is needed to connect topic-modelling results back to the
original dataset.post_date, year, month, and
day_of_week support time-based analysis and make it
possible to examine changes in posting or engagement patterns over
time.word_count measures post/caption length and can be used
to investigate whether text length is associated with engagement.post_age_days measures the age of a post relative to
the most recent post in the dataset. This is useful because engagement
counts are cumulative and posts cover a long time period.complete_engagement identifies rows where likes,
comments, and shares are all observed. total_engagement is
calculated only for these complete rows, preventing unavailable metrics
from being incorrectly treated as zero.log_likes is a transformed version of likes used only
for visualisation. Social-media engagement is highly right-skewed, so
log1p() makes patterns and extreme values easier to display
while retaining posts with zero likes.# Create identifiers, date-based variables, text-length measures, and engagement variables
fonterra <- fonterra |>
mutate(
document_id = row_number(),
post_date = as.Date(createdAt),
year = year(createdAt),
month = month(createdAt, label = TRUE, abbr = TRUE),
day_of_week = wday(createdAt, label = TRUE, abbr = FALSE, week_start = 1),
word_count = str_count(str_squish(coalesce(text, "")), "\\S+"),
post_age_days = as.integer(max(post_date, na.rm = TRUE) - post_date),
complete_engagement = !is.na(commentCount) & !is.na(shareCount),
total_engagement = if_else(
complete_engagement,
likeCount + commentCount + shareCount,
NA_real_
),
log_likes = log1p(likeCount)
)
# Summarise the newly created variables
new_variable_summary <- fonterra |>
summarise(
earliest_post = min(post_date, na.rm = TRUE),
latest_post = max(post_date, na.rm = TRUE),
median_word_count = median(word_count, na.rm = TRUE),
complete_engagement_rows = sum(complete_engagement),
rows_with_total_engagement = sum(!is.na(total_engagement))
)
new_variable_summary |>
knitr::kable(caption = "Summary of selected new variables")
| earliest_post | latest_post | median_word_count | complete_engagement_rows | rows_with_total_engagement |
|---|---|---|---|---|
| 2010-11-08 | 2026-07-28 | 22 | 7763 | 7763 |
For term-frequency analysis and topic modelling, the text was converted into individual word tokens. URLs, punctuation, numbers, standard English stop words, and a small set of high-frequency but low-information organisation, location, temporal, and conversational terms were removed. Words were lemmatised so that common inflected forms were brought closer to a shared base form. This reduces noise and makes the resulting themes easier to interpret.
# Create a small custom stop-word list after reviewing high-frequency low-information terms
custom_stop_words <- tibble::tibble(
word = c("fonterra", "zealand", "nz", "https", "http", "www", "com", "amp",
"year", "years", "today", "day", "week", "time", "just", "really",
"good", "great", "like", "know", "thanks", "yeah")
)
# Combine standard English stop words with the custom list
all_stop_words <- bind_rows(
tidytext::stop_words |> select(word),
custom_stop_words
) |>
distinct(word)
# Prepare text, remove URLs, tokenize words, lemmatise, and remove stop words
text_tokens <- fonterra |>
filter(!is.na(text), str_squish(text) != "") |>
select(document_id, platform, text) |>
mutate(
text_clean = str_replace_all(text, "https?://\\S+|www\\.\\S+", " "),
text_clean = str_replace_all(text_clean, "@[[:alnum:]_]+", " ")
) |>
tidytext::unnest_tokens(word, text_clean) |>
filter(str_detect(word, "^[a-z]+$"), nchar(word) > 2) |>
mutate(word = textstem::lemmatize_words(word)) |>
anti_join(all_stop_words, by = "word") |>
filter(!is.na(word), word != "")
# Check how many documents and tokens remain after cleaning
text_preparation_summary <- text_tokens |>
summarise(
documents_with_tokens = n_distinct(document_id),
cleaned_tokens = n(),
unique_terms = n_distinct(word)
)
text_preparation_summary |>
knitr::kable(caption = "Text-preparation summary")
| documents_with_tokens | cleaned_tokens | unique_terms |
|---|---|---|
| 8843 | 155895 | 11245 |
Term frequency provides a first view of the subject matter appearing repeatedly across Fonterra’s content. Because a simple frequency count can be influenced by long captions or transcripts, it is interpreted together with the document-based topic model rather than used alone.
# Count the most frequent cleaned terms
term_frequency <- text_tokens |>
count(word, sort = TRUE, name = "frequency")
# Display the 20 most frequent terms
top_terms <- term_frequency |>
slice_head(n = 20)
top_terms |>
knitr::kable(caption = "Twenty most frequent cleaned terms")
| word | frequency |
|---|---|
| milk | 2236 |
| farmer | 1849 |
| farm | 1682 |
| dairy | 1519 |
| team | 1323 |
| product | 822 |
| people | 769 |
| support | 730 |
| food | 698 |
| share | 697 |
| business | 671 |
| world | 606 |
| site | 605 |
| community | 600 |
| plant | 583 |
| price | 575 |
| award | 554 |
| cheese | 524 |
| learn | 524 |
| water | 511 |
# Visualise the 20 most frequent terms
top_terms |>
ggplot(aes(x = reorder(word, frequency), y = frequency)) +
geom_col() +
coord_flip() +
labs(
title = "Most frequent terms in Fonterra social-media content",
x = "Term",
y = "Frequency"
) +
theme_minimal(base_size = 11)
The cleaned term-frequency results are led by terms such as milk, farmer, farm, dairy, team, product, people, support. Overall, the vocabulary is strongly centred on dairy production, milk, farmers/farms, food/products, and related organisational or community activity. This suggests that Fonterra’s social-media presence primarily communicates its dairy identity while also covering business, product, farmer, community, and sustainability-related material.
A six-topic Latent Dirichlet Allocation (LDA) model was fitted,
following the assignment guidance to begin with k = 6.
Terms appearing in fewer than five documents were excluded from the
model to reduce sparse, highly specific vocabulary. The topic numbers
are arbitrary; interpretation is based on each topic’s
highest-probability terms.
# Calculate document frequency and retain terms appearing in at least five documents
lda_vocabulary <- text_tokens |>
distinct(document_id, word) |>
count(word, name = "document_frequency") |>
filter(document_frequency >= 5)
# Create document-term counts for the LDA model
lda_counts <- text_tokens |>
semi_join(lda_vocabulary, by = "word") |>
count(document_id, word, name = "n")
# Convert tidy term counts into a document-term matrix
lda_dtm <- lda_counts |>
tidytext::cast_dtm(document_id, word, n)
# Fit a reproducible six-topic LDA model
set.seed(1234)
lda_model <- topicmodels::LDA(
lda_dtm,
k = 6,
control = list(seed = 1234)
)
# Extract word-topic probabilities (beta) and identify the top terms for each topic
topic_terms <- tidytext::tidy(lda_model, matrix = "beta") |>
group_by(topic) |>
slice_max(beta, n = 10, with_ties = FALSE) |>
ungroup()
# Display the key terms by topic
topic_keyword_table <- topic_terms |>
arrange(topic, desc(beta)) |>
group_by(topic) |>
summarise(key_terms = paste(term, collapse = ", "), .groups = "drop")
topic_keyword_table |>
knitr::kable(caption = "Top terms describing each LDA topic")
| topic | key_terms |
|---|---|
| 1 | team, product, question, hear, customer, test, safety, call, happen, food |
| 2 | farm, farmer, water, plant, cow, dairy, environment, live, emission, sustainability |
| 3 | tanker, milk, team, school, farmer, kid, check, site, love, share |
| 4 | milk, dairy, world, product, people, food, cheese, protein, learn, music |
| 5 | milk, price, farmer, business, result, china, market, million, forecast, strong |
| 6 | award, dairy, team, support, community, farmer, link, celebrate, win, winner |
# Visualise the ten highest-probability terms within each topic
topic_terms |>
mutate(term_plot = tidytext::reorder_within(term, beta, topic)) |>
ggplot(aes(x = term_plot, y = beta)) +
geom_col() +
coord_flip() +
facet_wrap(~ topic, scales = "free") +
tidytext::scale_x_reordered() +
labs(
title = "Highest-probability terms within the six LDA topics",
x = "Term",
y = "Word-topic probability (beta)"
) +
theme_minimal(base_size = 10)
# Extract document-topic probabilities (gamma) and keep the most probable topic per post
dominant_topic <- tidytext::tidy(lda_model, matrix = "gamma") |>
group_by(document) |>
slice_max(gamma, n = 1, with_ties = FALSE) |>
ungroup() |>
transmute(
document_id = as.integer(as.character(document)),
dominant_topic = topic,
topic_probability = gamma
)
# Join each post's most probable topic back to the main dataset
fonterra <- fonterra |>
left_join(dominant_topic, by = "document_id") |>
mutate(topic_factor = factor(dominant_topic))
# Calculate the prevalence of each dominant topic
topic_prevalence <- fonterra |>
filter(!is.na(dominant_topic)) |>
count(dominant_topic, name = "posts") |>
mutate(percent = round(100 * posts / sum(posts), 1)) |>
left_join(topic_keyword_table, by = c("dominant_topic" = "topic")) |>
arrange(desc(posts))
topic_prevalence |>
knitr::kable(caption = "Prevalence and key terms of the dominant LDA topics")
| dominant_topic | posts | percent | key_terms |
|---|---|---|---|
| 3 | 2717 | 30.9 | tanker, milk, team, school, farmer, kid, check, site, love, share |
| 4 | 1376 | 15.6 | milk, dairy, world, product, people, food, cheese, protein, learn, music |
| 6 | 1355 | 15.4 | award, dairy, team, support, community, farmer, link, celebrate, win, winner |
| 2 | 1231 | 14.0 | farm, farmer, water, plant, cow, dairy, environment, live, emission, sustainability |
| 5 | 1061 | 12.1 | milk, price, farmer, business, result, china, market, million, forecast, strong |
| 1 | 1055 | 12.0 | team, product, question, hear, customer, test, safety, call, happen, food |
The engagement variables have different levels of availability across
platforms. likeCount is complete for all records and is
therefore used as the main comparable engagement outcome across
platforms, media types, and topics. Comments, shares, and video plays
are also summarised, but their platform-specific missingness is taken
into account.
# Summarise the distribution and availability of each engagement metric
engagement_summary <- fonterra |>
select(likeCount, commentCount, shareCount, videoplayCount) |>
pivot_longer(everything(), names_to = "metric", values_to = "value") |>
group_by(metric) |>
summarise(
available_n = sum(!is.na(value)),
missing_n = sum(is.na(value)),
mean = mean(value, na.rm = TRUE),
median = median(value, na.rm = TRUE),
IQR = IQR(value, na.rm = TRUE),
maximum = max(value, na.rm = TRUE),
.groups = "drop"
) |>
mutate(across(c(mean, median, IQR, maximum), ~ round(., 2)))
engagement_summary |>
knitr::kable(caption = "Descriptive statistics for user-engagement metrics")
| metric | available_n | missing_n | mean | median | IQR | maximum |
|---|---|---|---|---|---|---|
| commentCount | 8708 | 293 | 7.69 | 0.0 | 1.00 | 4265 |
| likeCount | 9001 | 0 | 48.42 | 3.0 | 20.00 | 9528 |
| shareCount | 8050 | 951 | 4.41 | 1.0 | 3.00 | 1621 |
| videoplayCount | 886 | 8115 | 32997.64 | 2783.5 | 11026.75 | 1199468 |
# Summarise likes by social-media platform
platform_like_summary <- fonterra |>
group_by(platform) |>
summarise(
posts = n(),
mean_likes = mean(likeCount, na.rm = TRUE),
median_likes = median(likeCount, na.rm = TRUE),
IQR_likes = IQR(likeCount, na.rm = TRUE),
maximum_likes = max(likeCount, na.rm = TRUE),
.groups = "drop"
) |>
mutate(across(c(mean_likes, median_likes, IQR_likes, maximum_likes), ~ round(., 2))) |>
arrange(desc(median_likes))
platform_like_summary |>
knitr::kable(caption = "Like engagement by platform")
| platform | posts | mean_likes | median_likes | IQR_likes | maximum_likes |
|---|---|---|---|---|---|
| 326 | 196.44 | 141 | 141.25 | 1967 | |
| 519 | 146.94 | 132 | 117.50 | 1164 | |
| 1433 | 182.22 | 61 | 134.00 | 9528 | |
| TikTok | 108 | 36.89 | 7 | 8.25 | 1582 |
| Twitter/X | 6220 | 4.40 | 1 | 5.00 | 704 |
| YouTube | 395 | 7.86 | 0 | 4.00 | 493 |
# Visualise the highly skewed like distributions using a log transformation for display only
fonterra |>
ggplot(aes(x = reorder(platform, log_likes, FUN = median), y = log_likes)) +
geom_boxplot(outlier.alpha = 0.2) +
coord_flip() +
labs(
title = "Distribution of likes by social-media platform",
x = "Platform",
y = "log(1 + likes)"
) +
theme_minimal(base_size = 11)
# Summarise likes by recorded media type; rows with unrecorded media are excluded
media_like_summary <- fonterra |>
filter(!is.na(media_clean)) |>
group_by(media_clean) |>
summarise(
posts = n(),
mean_likes = mean(likeCount),
median_likes = median(likeCount),
IQR_likes = IQR(likeCount),
.groups = "drop"
) |>
mutate(across(c(mean_likes, median_likes, IQR_likes), ~ round(., 2))) |>
arrange(desc(median_likes))
media_like_summary |>
knitr::kable(caption = "Like engagement by recorded media type")
| media_clean | posts | mean_likes | median_likes | IQR_likes |
|---|---|---|---|---|
| Sidecar | 95 | 142.74 | 134.0 | 98.00 |
| Image | 930 | 199.29 | 113.5 | 154.00 |
| Video | 982 | 104.79 | 20.0 | 102.75 |
# Connect each model-derived topic with its observed like engagement
topic_engagement <- fonterra |>
filter(!is.na(dominant_topic)) |>
group_by(dominant_topic) |>
summarise(
posts = n(),
mean_likes = mean(likeCount),
median_likes = median(likeCount),
IQR_likes = IQR(likeCount),
.groups = "drop"
) |>
left_join(topic_keyword_table, by = c("dominant_topic" = "topic")) |>
mutate(across(c(mean_likes, median_likes, IQR_likes), ~ round(., 2))) |>
arrange(desc(median_likes))
topic_engagement |>
knitr::kable(caption = "Like engagement by dominant LDA topic")
| dominant_topic | posts | mean_likes | median_likes | IQR_likes | key_terms |
|---|---|---|---|---|---|
| 6 | 1355 | 48.38 | 10 | 49 | award, dairy, team, support, community, farmer, link, celebrate, win, winner |
| 2 | 1231 | 53.62 | 6 | 45 | farm, farmer, water, plant, cow, dairy, environment, live, emission, sustainability |
| 4 | 1376 | 52.77 | 6 | 33 | milk, dairy, world, product, people, food, cheese, protein, learn, music |
| 3 | 2717 | 68.50 | 3 | 23 | tanker, milk, team, school, farmer, kid, check, site, love, share |
| 5 | 1061 | 28.70 | 1 | 6 | milk, price, farmer, business, result, china, market, million, forecast, strong |
| 1 | 1055 | 9.83 | 0 | 2 | team, product, question, hear, customer, test, safety, call, happen, food |
# Create a log-scale scatter plot for posts where video-play counts are available
video_data <- fonterra |>
filter(!is.na(videoplayCount))
ggplot(video_data, aes(x = log1p(videoplayCount), y = log1p(likeCount))) +
geom_point(alpha = 0.35) +
geom_smooth(method = "loess", se = FALSE) +
labs(
title = "Association between video plays and likes",
x = "log(1 + video plays)",
y = "log(1 + likes)"
) +
theme_minimal(base_size = 11)
The large differences between means and medians show that engagement is highly right-skewed and includes a small number of very high-engagement posts. For example, the overall median is 3 likes while the mean is 48.42. Therefore, medians, IQRs, log-scale visualisations, and non-parametric tests are emphasised rather than relying on means alone.
The platform table shows substantial differences in raw like counts. The highest median-like platform in this dataset is LinkedIn, with a median of 141 likes per post. However, these are absolute counts rather than engagement rates: follower numbers, impressions, and reach are not available, and the platforms also cover different historical periods. The platform comparison is therefore descriptive evidence of different engagement distributions, not proof that one platform is intrinsically more effective.
For posts where media type was recorded, the highest median-like format is Sidecar, with a median of 134 likes. This result should be interpreted cautiously because media availability is platform-dependent and media type is therefore partly confounded with platform.
The topic-engagement table connects the LDA themes with audience response. In this model, Topic 6 has the highest median like count (10) and is characterised by the terms award, dairy, team, support, community, farmer, link, celebrate, win, winner. This provides a useful direction for future content testing, but pooled topic differences may also reflect platform, posting period, or audience-size differences.
Because likes are extremely right-skewed with many low values and several extreme observations, Kruskal-Wallis tests are used as the non-parametric alternative to one-way ANOVA when comparing likes across more than two groups. Spearman’s rank correlation is used to assess the relationship between video plays and likes without assuming a normal linear relationship.
# Test whether like distributions differ across social-media platforms
kw_platform <- kruskal.test(likeCount ~ platform, data = fonterra)
# Test whether like distributions differ across recorded media types
media_test_data <- fonterra |>
filter(!is.na(media_clean))
kw_media <- kruskal.test(likeCount ~ media_clean, data = media_test_data)
# Test whether like distributions differ across the six model-derived topics
topic_test_data <- fonterra |>
filter(!is.na(topic_factor))
kw_topic <- kruskal.test(likeCount ~ topic_factor, data = topic_test_data)
# Put the Kruskal-Wallis results into one concise table
kruskal_results <- tibble::tibble(
analysis = c("Likes by platform", "Likes by recorded media type", "Likes by LDA topic"),
statistic = c(unname(kw_platform$statistic),
unname(kw_media$statistic),
unname(kw_topic$statistic)),
df = c(unname(kw_platform$parameter),
unname(kw_media$parameter),
unname(kw_topic$parameter)),
p_value = c(kw_platform$p.value, kw_media$p.value, kw_topic$p.value)
) |>
mutate(
statistic = round(statistic, 2),
p_display = format.pval(p_value, digits = 3, eps = 0.001)
) |>
select(analysis, statistic, df, p_display)
kruskal_results |>
knitr::kable(caption = "Kruskal-Wallis tests of factors associated with like engagement")
| analysis | statistic | df | p_display |
|---|---|---|---|
| Likes by platform | 4728.39 | 5 | <0.001 |
| Likes by recorded media type | 327.80 | 2 | <0.001 |
| Likes by LDA topic | 967.02 | 5 | <0.001 |
# Assess the association between video plays and likes for posts with video-play data
video_like_correlation <- cor.test(
video_data$videoplayCount,
video_data$likeCount,
method = "spearman",
exact = FALSE
)
video_correlation_result <- tibble::tibble(
observations = nrow(video_data),
spearman_rho = round(unname(video_like_correlation$estimate), 3),
p_value = video_like_correlation$p.value,
p_display = format.pval(video_like_correlation$p.value, digits = 3, eps = 0.001)
)
video_correlation_result |>
select(observations, spearman_rho, p_display) |>
knitr::kable(caption = "Spearman correlation between video plays and likes")
| observations | spearman_rho | p_display |
|---|---|---|
| 886 | 0.731 | <0.001 |
p < .001 for each comparison). These
tests establish association, not causation. In particular, platform
audience size, historical coverage, and the uneven availability of media
information could contribute to the observed differences.p < .001). Posts that receive more video plays
also tend to receive more likes, although this does not show that video
plays cause likes and video-play data are available only for selected
platforms/posts.ChatGPT was used as a guided supporting tool to help interpret the
assignment requirements, structure the R Markdown workflow, review R
syntax, and identify suitable approaches for data preparation, text
analysis, topic modelling, exploratory analysis, and bivariate
statistical testing. GenAI was not used to manually alter the source
dataset. Data-dependent statements were cross-checked against the
supplied fonterra.csv, including the dataset structure,
missing-value patterns, engagement summaries, and the suitability of the
selected statistical methods.