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.

1. Setup

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

2. API & Brand Setup

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")

3. LLM API Helper Function

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)
}

4. Generate Similarity Matrix

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

5. t-SNE Projection Function

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)
}

6. Run Analysis and Plot Map

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)