This R Notebook implements a minimal, LLM-focused version of the perceptual analysis from Li et al. (2024).
It uses the Hugging Face API to generate a brand similarity matrix and then projects it to a 2D perceptual map using t-SNE.
Load necessary libraries. httr and jsonlite
are for API calls.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.2 ✔ tibble 3.2.1
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.0.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(Rtsne)
library(ggrepel)
library(knitr)
library(httr)
library(jsonlite)
##
## Attaching package: 'jsonlite'
##
## The following object is masked from 'package:purrr':
##
## flatten
You must get a Hugging Face API token (with ‘read’ access) from
https://huggingface.co/settings/tokens and paste it below.
Then set it using
Sys.setenv("api_token" = "YOUR_TOKEN").
API_URL = "https://router.huggingface.co/v1/chat/completions"
brands = c("BMW", "Mercedes", "Porsche", "Ferrari", "Ford", "Jeep", "Honda", "Toyota", "Lamborghini", "Mini", "Range Rover", "Aston Martin", "Mustang", "Hyundai", "Volkswagen")
This function queries the LLM for a similarity rating between two brands.
get_llm_similarity = function(brand1, brand2, api_token) {
prompt = paste0(
"You are a survey respondent. Your task is to rate brand similarity. ",
"Respond *only* with an integer from 0 (not similar) to 10 (very similar). ",
"How similar are ", brand1, " and ", brand2, "?"
)
response = POST(
url = API_URL,
add_headers(
`Authorization` = paste("Bearer", Sys.getenv("api_token")),
`Content-Type` = "application/json"),
body = toJSON(list(
messages = list(
list(
role = "user",
content = prompt)),
model = "openai/gpt-oss-120b",
stream = FALSE), auto_unbox = TRUE))
# Parse the response
gen_text = fromJSON(content(response, "text"))$choices$message$content |>
suppressMessages()
# Extract the first number
rating_str = str_extract(gen_text, "\\d+") |> as.numeric()
# Add a 1-second delay to avoid rate limiting
Sys.sleep(.5)
return(rating_str)
}
This block loops through all unique pairs of brands and calls the LLM to get a similarity rating, building the complete matrix.
n = length(brands)
sim_matrix = matrix(NA, nrow = n, ncol = n, dimnames = list(brands, brands))
# Set diagonal to 10 (perfectly similar)
diag(sim_matrix) = 10
# Loop through unique pairs
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
brand_i = brands[i]
brand_j = brands[j]
# Call LLM
rating = get_llm_similarity(brand_i, brand_j, HF_TOKEN)
cat(sprintf("Rating: %s vs %s: %s\n", brand_i, brand_j, rating))
# Fill matrix (it's symmetric)
sim_matrix[i, j] = rating
sim_matrix[j, i] = rating
}
}
## Rating: BMW vs Mercedes: 9
## Rating: BMW vs Porsche: 8
## Rating: BMW vs Ferrari: 7
## Rating: BMW vs Ford: 5
## Rating: BMW vs Jeep: 3
## Rating: BMW vs Honda: 5
## Rating: BMW vs Toyota: 3
## Rating: BMW vs Lamborghini: 7
## Rating: BMW vs Mini: 8
## Rating: BMW vs Range Rover: 5
## Rating: BMW vs Aston Martin: 5
## Rating: BMW vs Mustang: 2
## Rating: BMW vs Hyundai: 2
## Rating: BMW vs Volkswagen: 6
## Rating: Mercedes vs Porsche: 8
## Rating: Mercedes vs Ferrari: 7
## Rating: Mercedes vs Ford: 4
## Rating: Mercedes vs Jeep: 2
## Rating: Mercedes vs Honda: 4
## Rating: Mercedes vs Toyota: 5
## Rating: Mercedes vs Lamborghini: 6
## Rating: Mercedes vs Mini: 2
## Rating: Mercedes vs Range Rover: 5
## Rating: Mercedes vs Aston Martin: 6
## Rating: Mercedes vs Mustang: 2
## Rating: Mercedes vs Hyundai: 2
## Rating: Mercedes vs Volkswagen: 6
## Rating: Porsche vs Ferrari: 8
## Rating: Porsche vs Ford: 4
## Rating: Porsche vs Jeep: 2
## Rating: Porsche vs Honda: 5
## Rating: Porsche vs Toyota: 4
## Rating: Porsche vs Lamborghini: 8
## Rating: Porsche vs Mini: 4
## Rating: Porsche vs Range Rover: 5
## Rating: Porsche vs Aston Martin: 8
## Rating: Porsche vs Mustang: 5
## Rating: Porsche vs Hyundai: 2
## Rating: Porsche vs Volkswagen: 5
## Rating: Ferrari vs Ford: 3
## Rating: Ferrari vs Jeep: 2
## Rating: Ferrari vs Honda: 2
## Rating: Ferrari vs Toyota: 2
## Rating: Ferrari vs Lamborghini: 9
## Rating: Ferrari vs Mini: 2
## Rating: Ferrari vs Range Rover: 5
## Rating: Ferrari vs Aston Martin: 7
## Rating: Ferrari vs Mustang: 5
## Rating: Ferrari vs Hyundai: 1
## Rating: Ferrari vs Volkswagen: 2
## Rating: Ford vs Jeep: 6
## Rating: Ford vs Honda: 6
## Rating: Ford vs Toyota: 6
## Rating: Ford vs Lamborghini: 2
## Rating: Ford vs Mini: 5
## Rating: Ford vs Range Rover: 3
## Rating: Ford vs Aston Martin: 4
## Rating: Ford vs Mustang: 9
## Rating: Ford vs Hyundai: 6
## Rating: Ford vs Volkswagen: 6
## Rating: Jeep vs Honda: 4
## Rating: Jeep vs Toyota: 5
## Rating: Jeep vs Lamborghini: 2
## Rating: Jeep vs Mini: 2
## Rating: Jeep vs Range Rover: 5
## Rating: Jeep vs Aston Martin: 1
## Rating: Jeep vs Mustang: 2
## Rating: Jeep vs Hyundai: 4
## Rating: Jeep vs Volkswagen: 4
## Rating: Honda vs Toyota: 8
## Rating: Honda vs Lamborghini: 2
## Rating: Honda vs Mini: 5
## Rating: Honda vs Range Rover: 2
## Rating: Honda vs Aston Martin: 2
## Rating: Honda vs Mustang: 2
## Rating: Honda vs Hyundai: 7
## Rating: Honda vs Volkswagen: 6
## Rating: Toyota vs Lamborghini: 2
## Rating: Toyota vs Mini: 4
## Rating: Toyota vs Range Rover: 4
## Rating: Toyota vs Aston Martin: 2
## Rating: Toyota vs Mustang: 2
## Rating: Toyota vs Hyundai: 6
## Rating: Toyota vs Volkswagen: 7
## Rating: Lamborghini vs Mini: 2
## Rating: Lamborghini vs Range Rover: 4
## Rating: Lamborghini vs Aston Martin: 6
## Rating: Lamborghini vs Mustang: 5
## Rating: Lamborghini vs Hyundai: 2
## Rating: Lamborghini vs Volkswagen: 2
## Rating: Mini vs Range Rover: 2
## Rating: Mini vs Aston Martin: 2
## Rating: Mini vs Mustang: 2
## Rating: Mini vs Hyundai: 2
## Rating: Mini vs Volkswagen: 5
## Rating: Range Rover vs Aston Martin: 6
## Rating: Range Rover vs Mustang: 2
## Rating: Range Rover vs Hyundai: 2
## Rating: Range Rover vs Volkswagen: 4
## Rating: Aston Martin vs Mustang: 5
## Rating: Aston Martin vs Hyundai: 2
## Rating: Aston Martin vs Volkswagen: 2
## Rating: Mustang vs Hyundai: 3
## Rating: Mustang vs Volkswagen: 2
## Rating: Hyundai vs Volkswagen: 6
This function projects the data into 2D for plotting. It takes a dissimilarity matrix as input.
project_to_2d_tsne = function(dissim_matrix, title = "Perceptual Map") {
set.seed(42)
tsne_out = Rtsne(dissim_matrix, is_distance = TRUE,
perplexity = 2, verbose = FALSE)
# Format data for plotting
plot_data = as.data.frame(tsne_out$Y)
colnames(plot_data) = c("Dim1", "Dim2")
plot_data$brand = rownames(dissim_matrix)
# Create the plot
p = ggplot(plot_data, aes(x = Dim1, y = Dim2, label = brand)) +
geom_point(color = viridis::mako(1, begin = .6), size = 3, alpha = 0.7) +
geom_text_repel(box.padding = 0.5) +
ggtitle(title) +
theme_minimal(base_size = 14)
return(p)
}
Finally, we convert our similarity matrix (where 10 = similar) to a dissimilarity matrix (where 0 = similar) and plot the map.
# Convert similarity (0-10) to dissimilarity (10-0)
dissim_matrix = 10 - sim_matrix
# Generate and display the map
map_llm = project_to_2d_tsne(dissim_matrix, "LLM Perceptual Map")
print(map_llm)