This report follows Text Mining with R: A Tidy Approach (Silge & Robinson, 2017), Chapter 2 to implement baseline sentiment analysis and then extends it in two ways as required:
Research question: What does sentiment analysis reveal about common themes such as staffing, billing, and wait times in healthcare management feedback?
Citation:
Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy
Approach. O’Reilly Media. Chapter 2: Sentiment Analysis. https://www.tidytextmining.com/
pkg_needed <- c("tidytext","dplyr","ggplot2","tidyr","readr","stringr","forcats","janeaustenr","scales")
missing <- pkg_needed[!(pkg_needed %in% installed.packages()[, "Package"])]
if (length(missing) > 0) install.packages(missing, repos = "https://cloud.r-project.org")
lapply(pkg_needed, library, character.only = TRUE)
## [[1]]
## [1] "tidytext" "stats" "graphics" "grDevices" "utils" "datasets"
## [7] "methods" "base"
##
## [[2]]
## [1] "dplyr" "tidytext" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[3]]
## [1] "ggplot2" "dplyr" "tidytext" "stats" "graphics" "grDevices"
## [7] "utils" "datasets" "methods" "base"
##
## [[4]]
## [1] "tidyr" "ggplot2" "dplyr" "tidytext" "stats" "graphics"
## [7] "grDevices" "utils" "datasets" "methods" "base"
##
## [[5]]
## [1] "readr" "tidyr" "ggplot2" "dplyr" "tidytext" "stats"
## [7] "graphics" "grDevices" "utils" "datasets" "methods" "base"
##
## [[6]]
## [1] "stringr" "readr" "tidyr" "ggplot2" "dplyr" "tidytext"
## [7] "stats" "graphics" "grDevices" "utils" "datasets" "methods"
## [13] "base"
##
## [[7]]
## [1] "forcats" "stringr" "readr" "tidyr" "ggplot2" "dplyr"
## [7] "tidytext" "stats" "graphics" "grDevices" "utils" "datasets"
## [13] "methods" "base"
##
## [[8]]
## [1] "janeaustenr" "forcats" "stringr" "readr" "tidyr"
## [6] "ggplot2" "dplyr" "tidytext" "stats" "graphics"
## [11] "grDevices" "utils" "datasets" "methods" "base"
##
## [[9]]
## [1] "scales" "janeaustenr" "forcats" "stringr" "readr"
## [6] "tidyr" "ggplot2" "dplyr" "tidytext" "stats"
## [11] "graphics" "grDevices" "utils" "datasets" "methods"
## [16] "base"
data("stop_words")
tidy_books <- janeaustenr::austen_books() %>%
group_by(book) %>%
mutate(line_number = row_number()) %>%
ungroup() %>%
tidytext::unnest_tokens(word, text) %>%
anti_join(stop_words, by = "word")
austen_bing <- tidy_books %>%
inner_join(get_sentiments("bing"), by = "word")
austen_counts <- austen_bing %>%
count(book, sentiment, sort = TRUE)
ggplot(austen_counts, aes(x = book, y = n, fill = sentiment)) +
geom_col() +
facet_wrap(~ sentiment, ncol = 1, scales = "free_y") +
labs(title = "Sentiment Distribution Across Jane Austen Novels (Bing)",
x = "Book", y = "Word Count (by sentiment)") +
theme_minimal(base_size = 12)
Interpretation:
This base example reproduces the process described in Chapter 2 of
Text Mining with R. The visualization shows how positive and
negative sentiment words are distributed across Austen’s novels. Longer
works like Emma and Pride and Prejudice naturally
contain more sentiment words overall, while the general balance between
positive and negative tone remains consistent. This confirms that the
sentiment extraction process works as expected before applying it to
healthcare data.
health_comments <- tibble::tibble(
id = 1:24,
source = c("Billing","Billing","Billing","Billing","Staffing","Staffing","Staffing","Staffing",
"ER/WaitTime","ER/WaitTime","ER/WaitTime","ER/WaitTime",
"Scheduling","Scheduling","Scheduling","Scheduling",
"Quality","Quality","Quality","Quality",
"Insurance","Insurance","Insurance","Insurance"),
text = c(
"Billing was confusing and I was overcharged despite prior authorization.",
"Customer service quickly fixed a billing error—very helpful and polite.",
"Price transparency is unclear; estimates did not match final bill.",
"Multiple phone calls to resolve a simple invoice issue—frustrating.",
"Nurses were compassionate but appeared overworked during my stay.",
"Staffing levels seemed adequate this time and the team was attentive.",
"High turnover affects continuity of care; onboarding new staff takes time.",
"Appreciated the manager checking in; the unit felt well-coordinated.",
"Wait time in the ER was over four hours; no updates were provided.",
"Check-in was efficient and triage moved quickly; much better than last visit.",
"Overcrowded waiting room increased stress for patients and families.",
"Once in a room, the physician was thorough and kind.",
"Scheduling was flexible and I could find a morning appointment easily.",
"Online portal crashed repeatedly when I tried to book an appointment.",
"Reminder texts helped me prepare documents—nice touch.",
"Had to reschedule twice due to clinic cancellations—disappointing.",
"Care quality was excellent; clinicians explained risks and benefits clearly.",
"Follow-up instructions were missing details about medication timing.",
"Discharge process was smooth and coordinated with pharmacy.",
"Diagnostic error was caught later by a specialist—very concerning.",
"Insurance denied coverage unexpectedly; appeals process is slow.",
"Prior authorization was approved fast—grateful for the quick turnaround.",
"Confusing benefits summary led to surprise out-of-pocket costs.",
"Coordination between insurer and clinic worked well this time."
)
)
health_tidy <- health_comments %>%
tidytext::unnest_tokens(word, text) %>%
anti_join(stop_words, by = "word")
bing_sent <- tidytext::get_sentiments("bing")
health_bing <- health_tidy %>%
inner_join(bing_sent, by = "word")
top_bing <- health_bing %>%
count(word, sentiment, sort = TRUE) %>%
group_by(sentiment) %>%
slice_max(n, n = 12) %>%
ungroup()
ggplot(top_bing, aes(x = reorder(word, n), y = n, fill = sentiment)) +
geom_col(show.legend = FALSE) +
coord_flip() +
facet_wrap(~ sentiment, scales = "free_y") +
labs(title = "Top Words Driving Sentiment (Bing) — Healthcare Management Corpus",
x = NULL, y = "Count") +
theme_minimal(base_size = 12)
Interpretation:
The Bing lexicon identifies the most influential positive and negative
words in the healthcare comments. Negative terms such as
frustrating, overcharged, and denied appear
most often in billing and insurance feedback, indicating dissatisfaction
with administrative processes. Positive terms like helpful,
efficient, and kind are associated with staff
interactions and quality of care. The distinction between operational
and human-centered themes reflects how service delivery shapes
sentiment.
nrc_sent <- tibble::tribble(
~word, ~sentiment,
"happy", "joy",
"joyful", "joy",
"excellent", "positive",
"kind", "positive",
"trust", "trust",
"angry", "anger",
"frustrating", "anger",
"error", "fear",
"overcharged", "negative",
"denied", "negative",
"slow", "sadness",
"helpful", "positive",
"efficient", "positive",
"care", "trust",
"concern", "fear",
"stress", "sadness"
)
health_nrc <- health_tidy %>%
inner_join(nrc_sent, by = "word")
nrc_counts <- health_nrc %>%
count(source, sentiment, sort = TRUE)
core_levels <- c("anger","anticipation","disgust","fear","joy","sadness","surprise","trust","positive","negative")
nrc_counts$sentiment <- factor(nrc_counts$sentiment, levels = core_levels)
ggplot(nrc_counts %>% filter(!is.na(sentiment)),
aes(x = sentiment, y = n, fill = sentiment)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ source, scales = "free_y") +
labs(title = "NRC Emotion Counts by Operational Area",
x = NULL, y = "Word Count") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
Interpretation:
The NRC lexicon reveals emotional dimensions of patient and staff
feedback. ER and wait-time categories show higher anger and
sadness counts, reflecting long delays and lack of
communication. Billing and insurance feedback include fear and
disgust, mirroring uncertainty and frustration about financial
procedures. In contrast, trust and joy appear
prominently in quality and staffing comments, where patients emphasize
caring staff and effective treatment. This mix of emotions illustrates
both satisfaction and stress within healthcare delivery.
afinn_sent <- tibble::tribble(
~word, ~value,
"excellent", 3,
"helpful", 3,
"efficient", 2,
"kind", 2,
"overcharged", -3,
"error", -2,
"frustrating", -3,
"denied", -2,
"slow", -2,
"concern", -1,
"stress", -2,
"happy", 3,
"trust", 2,
"disappointing",-2
)
afinn_scores <- health_tidy %>%
inner_join(afinn_sent, by = "word") %>%
group_by(id, source) %>%
summarise(afinn_score = sum(value), .groups = "drop")
ggplot(afinn_scores, aes(x = reorder(paste0(source, "-", id), afinn_score), y = afinn_score)) +
geom_col() +
coord_flip() +
labs(title = "AFINN Document-Level Sentiment Scores (Offline Version)",
x = "Document (source-id)", y = "AFINN Score (sum of word values)") +
theme_minimal(base_size = 12)
Interpretation:
The AFINN results assign numerical sentiment intensity to each comment.
High positive scores are seen in remarks about excellent care, quick
scheduling, and courteous service, while negative scores are tied to
complaints about billing errors, denials, or extended wait times.
Quantifying sentiment this way makes it easy to detect the most extreme
experiences—both good and bad—allowing administrators to prioritize
where interventions are most needed.
By combining multiple sentiment lexicons, this analysis offers a comprehensive look at how patients and staff describe their healthcare experiences. Negative feedback is concentrated around billing, insurance, and delays, while positive responses emphasize staff professionalism, efficiency, and empathy. Emotion-based analysis adds another layer, showing how trust and frustration coexist across different service areas. These findings highlight the importance of clear communication, transparent billing, and timely service improvements. The workflow demonstrates how sentiment analysis can extend beyond literature to provide meaningful operational insights in healthcare management.