This project explores how students describe professors in RateMyProfessor reviews and whether certain instructor archetypes receive higher ratings.
Research Question:
What professor archetypes emerge from RateMyProfessor comments, and
which archetypes receive the highest ratings?
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.2.1 ✔ readr 2.2.0
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ ggplot2 4.0.3 ✔ tibble 3.3.1
## ✔ lubridate 1.9.5 ✔ tidyr 1.3.2
## ✔ purrr 1.2.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(tidytext)
library(topicmodels)
library(ggplot2)
library(dplyr)
library(tidyr)
RateMyProfessor_Sample_data <- read_csv("RMPData.csv")
## Rows: 20000 Columns: 51
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (17): professor_name, school_name, department_name, local_name, state_na...
## dbl (34): year_since_first_review, star_rating, diff_index, num_student, stu...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rmp_clean <- RateMyProfessor_Sample_data %>%
select(comments, star_rating, diff_index, would_take_agains) %>%
filter(!is.na(comments)) %>%
mutate(doc_id = row_number())
custom_stop_words <- tibble(
word = c(
"professor","prof","teacher","class","course",
"semester","students","student","lecture",
"lectures","really","also","one","get","got",
"would","take","taking","taken"
)
)
topic_colors <- c(
"#4E79A7",
"#F28E2B",
"#E15759",
"#76B7B2",
"#59A14F"
)
tokens <- rmp_clean %>%
unnest_tokens(word, comments) %>%
anti_join(stop_words) %>%
anti_join(custom_stop_words)
## Joining with `by = join_by(word)`
## Joining with `by = join_by(word)`
dtm <- tokens %>%
count(doc_id, word) %>%
cast_dtm(doc_id, word, n)
lda_model <- LDA(dtm, k = 5, control = list(seed = 1234))
beta_mat <- posterior(lda_model)$terms
topics <- as.data.frame(beta_mat) %>%
mutate(topic = row_number()) %>%
pivot_longer(
cols = -topic,
names_to = "term",
values_to = "beta"
)
top_terms <- topics %>%
group_by(topic) %>%
slice_max(beta, n = 10) %>%
ungroup()
ggplot(top_terms,
aes(beta,
reorder_within(term, beta, topic),
fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free") +
scale_y_reordered() +
scale_fill_manual(values = topic_colors) +
labs(
title = "Professor Archetypes Using Unigrams",
x = NULL,
y = "Importance"
) +
theme_minimal(base_size = 11)
bigrams <- rmp_clean %>%
unnest_tokens(bigram, comments, token = "ngrams", n = 2)
bigrams_sep <- bigrams %>%
separate(bigram, into = c("word1", "word2"), sep = " ")
bigrams_filtered <- bigrams_sep %>%
filter(!word1 %in% stop_words$word) %>%
filter(!word2 %in% stop_words$word) %>%
filter(!word1 %in% custom_stop_words$word) %>%
filter(!word2 %in% custom_stop_words$word)
bigrams_united <- bigrams_filtered %>%
unite(bigram, word1, word2, sep = " ")
bigrams_counts <- bigrams_united %>%
count(doc_id, bigram) %>%
group_by(bigram) %>%
filter(sum(n) > 20) %>%
ungroup()
bigram_dtm <- bigrams_counts %>%
cast_dtm(doc_id, bigram, n)
lda_bigram <- LDA(bigram_dtm, k = 5, control = list(seed = 1234))
# --- REPLACEMENT FOR tidy() ---
beta_mat_bigram <- posterior(lda_bigram)$terms
bigram_topics <- as.data.frame(beta_mat_bigram) %>%
mutate(topic = row_number()) %>%
pivot_longer(
cols = -topic,
names_to = "term",
values_to = "beta"
)
# --------------------------------
top_bigrams <- bigram_topics %>%
group_by(topic) %>%
slice_max(beta, n = 10) %>%
ungroup()
bigram_labels <- c(
"Supportive but Tough",
"Easy Grader / Extra Credit",
"Engaging / Real-World Teaching",
"Easy & Multiple Choice",
"Helpful & Approachable"
)
top_bigrams <- top_bigrams %>%
mutate(
archetype = bigram_labels[topic],
archetype = str_wrap(archetype, width = 20)
)
ggplot(top_bigrams,
aes(beta,
reorder_within(term, beta, archetype),
fill = archetype)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ archetype, scales = "free") +
scale_y_reordered() +
labs(
title = "Professor Archetypes Using Bigrams",
x = NULL,
y = "Importance"
) +
theme_minimal(base_size = 11)
# assign reviews to topics (NO tidy())
gamma_mat <- posterior(lda_model)$topics
assignments <- as.data.frame(gamma_mat) %>%
mutate(document = row_number()) %>%
pivot_longer(
cols = -document,
names_to = "topic",
values_to = "gamma"
) %>%
mutate(topic = as.integer(gsub("V", "", topic))) %>%
group_by(document) %>%
slice_max(gamma) %>%
ungroup()
rmp_topics <- assignments %>%
left_join(rmp_clean, by = c("document" = "doc_id"))
# add bigram archetype labels
bigram_labels <- c(
"Supportive but Tough",
"Easy Grader / Extra Credit",
"Engaging / Real-World Teaching",
"Easy & Multiple Choice",
"Helpful & Approachable"
)
ratings_by_topic <- rmp_topics %>%
group_by(topic) %>%
summarise(
avg_rating = mean(star_rating, na.rm = TRUE),
avg_difficulty = mean(diff_index, na.rm = TRUE),
n = n()
) %>%
mutate(archetype = bigram_labels[topic])
# plot
library(stringr)
ggplot(ratings_by_topic,
aes(x = reorder(archetype, avg_rating),
y = avg_rating,
fill = archetype)) +
geom_col() +
coord_flip() +
scale_x_discrete(labels = function(x) str_wrap(x, width = 20)) +
coord_cartesian(ylim = c(3.6, 3.7)) +
labs(
title = "Average Rating by Professor Archetype",
x = NULL,
y = "Average Rating"
) +
theme_minimal(base_size = 11) +
guides(fill = "none")
## Coordinate system already present.
## ℹ Adding new coordinate system, which will replace the existing one.
Distinct professor archetypes emerge from student comments. These
reflect a mix of teaching style (engaging, supportive) and grading style
(easy, extra credit).
Ratings are all relatively high and tightly clustered All archetypes fall in a narrow band (roughly 3.63–3.66). That means:
Slight preference for “easier” and engaging professors. This suggests students reward:
Perceived fairness or leniency
Practical, engaging instruction
“Supportive but Tough” ranks lowest (but still strong) Although still well-rated, “Supportive but Tough” is the lowest of the group. This may indicate:
While the analysis provides useful insight into perceived professor archetypes, it should be interpreted cautiously, with awareness of bias, limited representativeness, and the potential real-world impact on educators.