library(dplyr)
library(ggplot2)
library(knitr)
theme_conjoint_dark <- function(base_size = 13) {
  theme_minimal(base_size = base_size) +
    theme(
      plot.background = element_rect(fill = "#111827", color = NA),
      panel.background = element_rect(fill = "#111827", color = NA),
      panel.grid.major = element_line(color = "#374151"),
      panel.grid.minor = element_blank(),
      text = element_text(color = "#f9fafb"),
      plot.title = element_text(color = "#f9fafb", face = "bold"),
      plot.subtitle = element_text(color = "#d1d5db"),
      axis.title = element_text(color = "#e5e7eb"),
      axis.text = element_text(color = "#d1d5db"),
      strip.text = element_text(color = "#f9fafb", face = "bold"),
      strip.background = element_rect(fill = "#1f2937", color = NA),
      legend.background = element_rect(fill = "#111827", color = NA),
      legend.key = element_rect(fill = "#111827", color = NA),
      legend.text = element_text(color = "#d1d5db"),
      legend.title = element_text(color = "#f9fafb")
    )
}

num_fmt <- function(x, digits = 2) {
  format(round(as.numeric(x), digits), nsmall = digits)
}

Learning Objectives

By the end of this lab, I should be able to:

  • Explain how product features and attribute levels are used in conjoint analysis.
  • Describe why this lab uses a reduced profile design instead of all possible combinations.
  • Read and inspect conjoint data in long format.
  • Estimate attribute-level preferences using linear regression.
  • Interpret coefficients, predicted ratings, and visual outputs.
  • Translate the model results into product and marketing recommendations.

1 What Is Conjoint Analysis?

Conjoint analysis is a survey-based method for estimating how people trade off product features when forming an overall preference. Instead of asking respondents to directly state which features matter most, we ask them to rate realistic product profiles that bundle several attributes together.

The pattern of ratings across profiles allows the model to estimate how much each attribute level contributes to the overall rating.

Interpretation checkpoint: In this lab, a higher rating means the respondent found the auto insurance policy more attractive. The regression model estimates which policy features tend to raise or lower that expected rating.

2 Our Example: Shopping for Auto Insurance

This lab studies preferences for auto insurance policies using three attributes.

Attribute Levels
Deductible 500, 1000, 2000
Brand State Farm, AAA, Local
Pay-as-you-drive Yes, No

A full factorial design would create:

3 * 3 * 2
## [1] 18

That means there are 18 possible policies. Showing all 18 to each respondent would create more burden than necessary for a short classroom lab.

2.1 Why Six Profiles?

For a main-effects conjoint model, the required number of estimated terms is:

1 + (number of Deductible levels - 1) +
    (number of Brand levels - 1) +
    (number of Pay-as-you-drive levels - 1)

For this design:

1 + (3 - 1) + (3 - 1) + (2 - 1) = 6

Key clarification: The number 6 refers to the number of product profiles shown to each respondent, not the number of survey respondents. The intercept counts as 1, and each attribute contributes one fewer estimated parameter than its number of levels.

3 Data Collection Strategy

This is a full-profile, within-respondent conjoint exercise. Each respondent rates multiple policy profiles, not just one. This matters because the model needs to observe how ratings change as the policy attributes change.

Good data collection practice includes:

  • showing each respondent all required profiles;
  • randomizing the order of profiles when possible;
  • using a consistent rating scale, such as 1 to 7;
  • collecting enough responses to reduce uncertainty in the estimates;
  • keeping profiles realistic and easy to compare.

4 Reading and Inspecting the Dataset

if (!file.exists("insurance_conjoint.csv")) {
  stop("The file insurance_conjoint.csv was not found. Put it in the same folder as this RMD file.")
}

df_raw <- read.csv("insurance_conjoint.csv", stringsAsFactors = FALSE)
head(df_raw)
cat("Rows:", nrow(df_raw), "\n")
## Rows: 120
cat("Columns:", ncol(df_raw), "\n")
## Columns: 6
cat("Column names:", paste(names(df_raw), collapse = ", "), "\n")
## Column names: id, order_shown, Deductible, Brand, Pay_as_you_drive, rating
cat("Respondents:", length(unique(df_raw$id)), "\n")
## Respondents: 20
cat("Ratings per respondent:\n")
## Ratings per respondent:
print(table(df_raw$id))
## 
##  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 
##  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6  6

Lab step interpretation: The dataset is in long format. That means each row is one respondent-profile rating. This is the correct shape for lm() because the model needs one outcome value, rating, for each evaluated policy profile.

5 Preparing Variables and Estimating Attribute Importance

The model uses dummy variables for attribute levels. One level from each attribute becomes the baseline. The remaining coefficients are interpreted relative to that baseline.

# Professor-aligned preparation step:
# Treat the design attributes as categorical factors.
# Deductible looks numeric, but in conjoint analysis we want a separate
# part-worth utility for each level rather than assuming a straight-line slope.
df <- df_raw %>%
  mutate(
    Deductible = as.numeric(as.character(Deductible)),
    rating = as.numeric(rating)
  )

df$Deductible <- factor(
  df$Deductible,
  levels = c(500, 1000, 2000)
)

df$Brand <- factor(
  df$Brand,
  levels = c("State Farm", "AAA", "Local")
)

df$Pay_as_you_drive <- factor(
  df$Pay_as_you_drive,
  levels = c("No", "Yes")
)

str(df)
## 'data.frame':    120 obs. of  6 variables:
##  $ id              : int  1 1 1 1 1 1 2 2 2 2 ...
##  $ order_shown     : int  1 2 3 4 5 6 1 2 3 4 ...
##  $ Deductible      : Factor w/ 3 levels "500","1000","2000": 3 2 3 2 1 1 3 3 1 2 ...
##  $ Brand           : Factor w/ 3 levels "State Farm","AAA",..: 1 1 2 3 1 2 2 1 2 1 ...
##  $ Pay_as_you_drive: Factor w/ 2 levels "No","Yes": 1 1 2 2 2 1 2 1 1 1 ...
##  $ rating          : num  3 2 6 5 5 6 4 4 5 4 ...
# Professor naming convention: this is the part-worth main-effects model.
conjoint_model <- lm(rating ~ Deductible + Brand + Pay_as_you_drive, data = df)

# Keep m01 as an alias so the rest of the existing report still knits.
m01 <- conjoint_model

summary(conjoint_model)
## 
## Call:
## lm(formula = rating ~ Deductible + Brand + Pay_as_you_drive, 
##     data = df)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
##  -2.70  -0.70   0.05   0.55   2.55 
## 
## Coefficients:
##                                   Estimate             Std. Error t value
## (Intercept)          4.4500000000000001776  0.2696651594952785280  16.502
## Deductible1000      -0.9999999999999997780  0.3813641258577268878  -2.622
## Deductible2000      -0.7500000000000000000  0.2696651594952785835  -2.781
## BrandAAA             0.2500000000000001110  0.2696651594952785835   0.927
## BrandLocal          -0.6000000000000005329  0.4670737572769874668  -1.285
## Pay_as_you_driveYes  0.0000000000000002777  0.2696651594952785835   0.000
##                                 Pr(>|t|)    
## (Intercept)         < 0.0000000000000002 ***
## Deductible1000                   0.00993 ** 
## Deductible2000                   0.00634 ** 
## BrandAAA                         0.35585    
## BrandLocal                       0.20154    
## Pay_as_you_driveYes              1.00000    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.206 on 114 degrees of freedom
## Multiple R-squared:  0.2153, Adjusted R-squared:  0.1809 
## F-statistic: 6.257 on 5 and 114 DF,  p-value: 0.00003641
coef_table <- as.data.frame(summary(m01)$coefficients)
coef_table$term <- rownames(coef_table)
rownames(coef_table) <- NULL
names(coef_table) <- c("Estimate", "Std_Error", "t_value", "p_value", "term")
coef_table <- coef_table %>%
  select(term, Estimate, Std_Error, t_value, p_value) %>%
  mutate(
    abs_estimate = abs(Estimate),
    significance = case_when(
      p_value < 0.001 ~ "p < .001",
      p_value < 0.01 ~ "p < .01",
      p_value < 0.05 ~ "p < .05",
      TRUE ~ "not significant at .05"
    )
  )

kable(
  coef_table,
  digits = 3,
  caption = "Regression coefficients for auto insurance conjoint model"
)
Regression coefficients for auto insurance conjoint model
term Estimate Std_Error t_value p_value abs_estimate significance
(Intercept) 4.45 0.270 16.502 0.000 4.45 p < .001
Deductible1000 -1.00 0.381 -2.622 0.010 1.00 p < .01
Deductible2000 -0.75 0.270 -2.781 0.006 0.75 p < .01
BrandAAA 0.25 0.270 0.927 0.356 0.25 not significant at .05
BrandLocal -0.60 0.467 -1.285 0.202 0.60 not significant at .05
Pay_as_you_driveYes 0.00 0.270 0.000 1.000 0.00 not significant at .05

Coefficient table read: This table is the source of truth for the written interpretation. The intercept represents the baseline policy rating. Each non-intercept coefficient estimates the change in predicted rating from switching to that level while holding the other features constant. In the tutorial output, Deductible1000 and Deductible2000 are statistically significant at the .01 level, while BrandAAA, BrandLocal, and Pay_as_you_driveYes are not significant at the .05 level.

6 Professor Part-Worth Utilities and Best Profile

The professor’s continuation adds four important outputs after the main regression model:

  1. centered part-worth utilities;
  2. range-based attribute importance;
  3. the highest-utility predicted profile;
  4. a sanity check against the actually observed profile averages.

6.1 Extracting Centered Part-Worth Utilities

The regression coefficients are relative to baseline levels. Centering them within each attribute makes them easier to compare as part-worth utilities because each attribute’s levels average to zero.

coefs <- coef(conjoint_model)

get_utilities <- function(attr, levels_vec, coefs) {
  u <- setNames(numeric(length(levels_vec)), levels_vec)
  
  for (lv in levels_vec[-1]) {
    term_candidates <- c(
      paste0(attr, lv),
      paste0(attr, make.names(lv))
    )
    matched_term <- term_candidates[term_candidates %in% names(coefs)]
    
    if (length(matched_term) > 0) {
      u[lv] <- coefs[matched_term[1]]
    }
  }
  
  # Center so each attribute's levels average to zero.
  u - mean(u)
}

util_deductible <- get_utilities("Deductible", levels(df$Deductible), coefs)
util_brand <- get_utilities("Brand", levels(df$Brand), coefs)
util_payd <- get_utilities("Pay_as_you_drive", levels(df$Pay_as_you_drive), coefs)

cat("Deductible:\n")
## Deductible:
print(round(util_deductible, 3))
##    500   1000   2000 
##  0.583 -0.417 -0.167
cat("Brand:\n")
## Brand:
print(round(util_brand, 3))
## State Farm        AAA      Local 
##      0.117      0.367     -0.483
cat("Pay_as_you_drive:\n")
## Pay_as_you_drive:
print(round(util_payd, 3))
##  No Yes 
##   0   0
utility_table <- data.frame(
  Attribute = c(
    rep("Deductible", length(util_deductible)),
    rep("Brand", length(util_brand)),
    rep("Pay_as_you_drive", length(util_payd))
  ),
  Level = c(
    names(util_deductible),
    names(util_brand),
    names(util_payd)
  ),
  Utility = round(c(util_deductible, util_brand, util_payd), 3)
)

kable(
  utility_table,
  caption = "Part-worth utilities by attribute and level"
)
Part-worth utilities by attribute and level
Attribute Level Utility
500 Deductible 500 0.583
1000 Deductible 1000 -0.417
2000 Deductible 2000 -0.167
State Farm Brand State Farm 0.117
AAA Brand AAA 0.367
Local Brand Local -0.483
No Pay_as_you_drive No 0.000
Yes Pay_as_you_drive Yes 0.000

How to read this table: Positive part-worth utilities add preference relative to the average level within that attribute. Negative utilities subtract preference relative to that attribute’s average. The values are most meaningful when compared within the same attribute.

6.2 Attribute Importance from Utility Ranges

The professor’s method calculates each attribute’s importance as the spread between its highest and lowest part-worth utilities, then normalizes those spreads to sum to 100%.

importance <- c(
  Deductible = diff(range(util_deductible)),
  Brand = diff(range(util_brand)),
  Pay_as_you_drive = diff(range(util_payd))
)

importance_pct <- round(100 * importance / sum(importance), 1)

importance_table <- data.frame(
  Attribute = names(importance_pct),
  Importance_Pct = importance_pct
)

kable(
  importance_table,
  caption = "Relative importance of each attribute (%)"
)
Relative importance of each attribute (%)
Attribute Importance_Pct
Deductible Deductible 54.1
Brand Brand 45.9
Pay_as_you_drive Pay_as_you_drive 0.0

Current importance read: The largest overall attribute driver is Deductible, accounting for approximately 54.1% of the total modeled utility swing.

6.3 Best Level per Attribute: Highest-Utility Profile

This builds the model-predicted ideal policy by selecting the highest part-worth level within each attribute.

best_deductible <- names(which.max(util_deductible))
best_brand <- names(which.max(util_brand))
best_payd <- names(which.max(util_payd))

best_profile_partworth <- data.frame(
  Deductible = factor(best_deductible, levels = levels(df$Deductible)),
  Brand = factor(best_brand, levels = levels(df$Brand)),
  Pay_as_you_drive = factor(best_payd, levels = levels(df$Pay_as_you_drive))
)

predicted_rating_partworth <- predict(conjoint_model, newdata = best_profile_partworth)

kable(
  best_profile_partworth,
  caption = "Highest-utility predicted profile from part-worth utilities"
)
Highest-utility predicted profile from part-worth utilities
Deductible Brand Pay_as_you_drive
500 AAA Yes
cat("Predicted rating:", round(predicted_rating_partworth, 2), "\n")
## Predicted rating: 4.7

Current ideal-profile read: The highest-utility predicted policy combines a 500 deductible, AAA brand, and Pay-as-you-drive = Yes. The model-predicted rating is 4.70.

6.4 Sanity Check Against Observed Profiles

This step compares the model’s ideal profile with the actual average ratings for the profiles respondents saw.

observed_avg <- aggregate(
  rating ~ Deductible + Brand + Pay_as_you_drive,
  data = df,
  FUN = mean
)

observed_avg <- observed_avg[order(-observed_avg$rating), ]

kable(
  observed_avg,
  digits = 2,
  caption = "Observed mean rating by profile"
)
Observed mean rating by profile
Deductible Brand Pay_as_you_drive rating
3 500 AAA No 4.70
4 500 State Farm Yes 4.45
5 2000 AAA Yes 3.95
2 2000 State Farm No 3.70
1 1000 State Farm No 3.45
6 1000 Local Yes 2.85

Observed-profile check: The highest observed average profile is 500 deductible, AAA, Pay-as-you-drive = No, with a mean rating of 4.70. If this is close to the model-predicted ideal profile above, the main-effects model is behaving consistently with the raw ratings. If it differs, that is a useful limitation to flag.

6.5 How to Interpret the Regression Output

The baseline policy is the omitted combination: a 500 deductible, State Farm as the brand, and no pay-as-you-drive feature. The model estimates this baseline rating as 4.45.

Each coefficient shows the expected rating change compared with that baseline, holding the other attributes constant. Positive coefficients increase the expected rating relative to the baseline. Negative coefficients lower the expected rating relative to the baseline.

Current model read: The largest absolute coefficient is Deductible1000, with an estimated effect of -1.00. This is the single strongest modeled attribute-level movement in the current lab output.

7 Predicted Ratings

Predicted ratings let us rank policy profiles using the fitted regression model. This is useful because the model can estimate expected ratings for combinations that were not directly shown to respondents.

design_df <- df %>%
  distinct(Deductible, Brand, Pay_as_you_drive) %>%
  arrange(Deductible, Brand, Pay_as_you_drive)

kable(
  design_df,
  caption = "Unique tested profiles found in the dataset"
)
Unique tested profiles found in the dataset
Deductible Brand Pay_as_you_drive
500 State Farm Yes
500 AAA No
1000 State Farm No
1000 Local Yes
2000 State Farm No
2000 AAA Yes
fit_tested <- predict(m01, newdata = design_df, interval = "confidence")

design_out <- cbind(design_df, fit_tested) %>%
  arrange(desc(fit))

kable(
  design_out,
  digits = 2,
  caption = "Predicted rating for each tested profile"
)
Predicted rating for each tested profile
Deductible Brand Pay_as_you_drive fit lwr upr
2 500 AAA No 4.70 4.17 5.23
1 500 State Farm Yes 4.45 3.92 4.98
6 2000 AAA Yes 3.95 3.42 4.48
5 2000 State Farm No 3.70 3.17 4.23
3 1000 State Farm No 3.45 2.92 3.98
4 1000 Local Yes 2.85 2.32 3.38
full_grid <- expand.grid(
  Deductible = levels(df$Deductible),
  Brand = levels(df$Brand),
  Pay_as_you_drive = levels(df$Pay_as_you_drive)
)

full_grid$Deductible <- factor(full_grid$Deductible, levels = levels(df$Deductible))
full_grid$Brand <- factor(full_grid$Brand, levels = levels(df$Brand))
full_grid$Pay_as_you_drive <- factor(full_grid$Pay_as_you_drive, levels = levels(df$Pay_as_you_drive))

fit_full <- predict(m01, newdata = full_grid, interval = "confidence")

full_out <- cbind(full_grid, fit_full) %>%
  arrange(desc(fit))

kable(
  full_out,
  digits = 2,
  caption = paste("Predicted rating across all", nrow(full_grid), "possible combinations")
)
Predicted rating across all 18 possible combinations
Deductible Brand Pay_as_you_drive fit lwr upr
4 500 AAA No 4.70 4.17 5.23
13 500 AAA Yes 4.70 4.17 5.23
1 500 State Farm No 4.45 3.92 4.98
10 500 State Farm Yes 4.45 3.92 4.98
15 2000 AAA Yes 3.95 3.42 4.48
6 2000 AAA No 3.95 3.42 4.48
16 500 Local Yes 3.85 2.92 4.78
7 500 Local No 3.85 2.66 5.04
14 1000 AAA Yes 3.70 2.77 4.63
5 1000 AAA No 3.70 2.94 4.46
12 2000 State Farm Yes 3.70 3.17 4.23
3 2000 State Farm No 3.70 3.17 4.23
11 1000 State Farm Yes 3.45 2.69 4.21
2 1000 State Farm No 3.45 2.92 3.98
18 2000 Local Yes 3.10 2.17 4.03
9 2000 Local No 3.10 1.91 4.29
17 1000 Local Yes 2.85 2.32 3.38
8 1000 Local No 2.85 2.09 3.61

7.1 How to Interpret the Prediction Tables

The first prediction table ranks only the profiles that appear in the dataset. The second table expands the logic to all 18 theoretically possible policies.

Current model read: The best predicted full-combination policy is 500 deductible, AAA, Pay-as-you-drive = No, with a predicted rating of 4.70. The lowest predicted combination is 1000 deductible, Local, Pay-as-you-drive = No, with a predicted rating of 2.85.

8 Visualizing Attribute Importance

8.1 Professor Visual 1: Part-Worth Utilities

This plot translates the centered utility table into a visual comparison of preferred and less-preferred levels within each attribute.

utility_plot_df <- utility_table %>%
  mutate(
    Level = factor(Level, levels = Level[order(Attribute, -Utility)])
  )

ggplot(utility_plot_df, aes(x = reorder(Level, Utility), y = Utility, fill = Attribute)) +
  geom_col(show.legend = FALSE) +
  geom_hline(yintercept = 0, color = "#f87171", linetype = "dashed") +
  geom_text(
    aes(label = round(Utility, 2)),
    hjust = ifelse(utility_plot_df$Utility >= 0, -0.15, 1.15),
    color = "#f9fafb",
    size = 3.5
  ) +
  coord_flip() +
  facet_wrap(~Attribute, scales = "free_y") +
  labs(
    title = "Part-Worth Utilities by Attribute Level",
    subtitle = "Centered utilities: positive levels add preference within their attribute; negative levels subtract preference",
    x = NULL,
    y = "Centered Part-Worth Utility"
  ) +
  theme_conjoint_dark(base_size = 13)

8.2 Professor Visual 2: Relative Attribute Importance

This is the most direct chart for explaining which attribute matters most overall because each bar is on the same 0% to 100% importance scale.

importance_plot_df <- importance_table %>%
  mutate(Attribute = reorder(Attribute, Importance_Pct))

ggplot(importance_plot_df, aes(x = Attribute, y = Importance_Pct)) +
  geom_col(fill = "#a78bfa") +
  geom_text(
    aes(label = paste0(Importance_Pct, "%")),
    hjust = -0.15,
    color = "#f9fafb",
    size = 4
  ) +
  coord_flip() +
  labs(
    title = "Relative Importance of Each Attribute",
    subtitle = "Calculated from each attribute's part-worth utility range",
    x = NULL,
    y = "Relative Importance (%)"
  ) +
  ylim(0, max(importance_plot_df$Importance_Pct) * 1.25) +
  theme_conjoint_dark(base_size = 13)

8.3 Professor Visual 3: Observed Mean Rating by Profile

This chart is a raw-data sanity check. It shows which actually observed profile received the highest average rating.

observed_plot_df <- observed_avg %>%
  mutate(
    profile_label = paste(Deductible, Brand, Pay_as_you_drive, sep = " | "),
    is_top = rating == max(rating)
  )

ggplot(observed_plot_df, aes(x = reorder(profile_label, rating), y = rating, fill = is_top)) +
  geom_col(show.legend = FALSE) +
  geom_text(
    aes(label = round(rating, 2)),
    hjust = -0.15,
    color = "#f9fafb",
    size = 3.5
  ) +
  coord_flip() +
  labs(
    title = "Mean Rating by Observed Profile",
    subtitle = "Raw average ratings for the exact policy profiles shown to respondents",
    x = "Profile: Deductible | Brand | Pay-as-you-drive",
    y = "Mean Rating"
  ) +
  ylim(0, max(observed_plot_df$rating) * 1.15) +
  theme_conjoint_dark(base_size = 13)

8.4 Marginal Means Plot

Marginal means show the raw average rating for each level of each attribute. This is a descriptive view: it does not control for the other attributes the way the regression model does.

mm_deduct <- df %>%
  group_by(level = Deductible) %>%
  summarise(mean_rating = mean(rating), .groups = "drop") %>%
  mutate(attribute = "Deductible")

mm_brand <- df %>%
  group_by(level = Brand) %>%
  summarise(mean_rating = mean(rating), .groups = "drop") %>%
  mutate(attribute = "Brand")

mm_pad <- df %>%
  group_by(level = Pay_as_you_drive) %>%
  summarise(mean_rating = mean(rating), .groups = "drop") %>%
  mutate(attribute = "Pay-as-you-drive")

mm_all <- bind_rows(mm_deduct, mm_brand, mm_pad)
mm_all$level <- as.character(mm_all$level)
mm_all$level <- factor(
  mm_all$level,
  levels = c("500", "1000", "2000", "State Farm", "AAA", "Local", "No", "Yes")
)

kable(
  mm_all,
  digits = 2,
  caption = "Marginal mean ratings by attribute level"
)
Marginal mean ratings by attribute level
level mean_rating attribute
500 4.58 Deductible
1000 3.15 Deductible
2000 3.83 Deductible
State Farm 3.87 Brand
AAA 4.32 Brand
Local 2.85 Brand
No 3.95 Pay-as-you-drive
Yes 3.75 Pay-as-you-drive
ggplot(mm_all, aes(x = level, y = mean_rating, fill = attribute)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~attribute, scales = "free_x") +
  labs(
    title = "Average Rating by Attribute Level",
    subtitle = "Raw averages before regression adjustment",
    x = NULL,
    y = "Mean Rating"
  ) +
  theme_conjoint_dark(base_size = 13)

8.5 How to Interpret the Marginal Means Plot

Current graph read: The highest raw marginal mean is 500 under Deductible, with an average rating of 4.58. The lowest raw marginal mean is Local under Brand, with an average rating of 2.85.

Use this graph as an intuitive first look. It is useful for seeing what respondents tended to rate higher or lower on average, but it should not be the final answer by itself because attribute levels are bundled together in profiles.

8.6 Regression Coefficient Plot

The coefficient plot shows the model-adjusted effects relative to the baseline levels.

co_plot <- coef_table %>%
  filter(term != "(Intercept)")

ggplot(co_plot, aes(x = reorder(term, Estimate), y = Estimate)) +
  geom_col(fill = "#60a5fa") +
  geom_errorbar(
    aes(
      ymin = Estimate - 1.96 * Std_Error,
      ymax = Estimate + 1.96 * Std_Error
    ),
    width = 0.2,
    color = "#f9fafb"
  ) +
  geom_hline(yintercept = 0, color = "#f87171", linetype = "dashed") +
  coord_flip() +
  labs(
    title = "Regression Coefficients Relative to Baseline",
    subtitle = "Positive values increase predicted rating; negative values decrease it",
    x = NULL,
    y = "Estimated Rating Change"
  ) +
  theme_conjoint_dark(base_size = 13)

8.7 How to Interpret the Coefficient Plot

Current graph read: The most positive coefficient is BrandAAA at 0.25. The most negative coefficient is Deductible1000 at -1.00. The largest absolute movement is Deductible1000, which is the strongest single modeled attribute-level effect in this lab.

The red zero line is important. Bars near zero suggest little modeled impact compared with the baseline. Bars far from zero suggest stronger estimated preference movement. Confidence intervals crossing zero indicate more uncertainty around whether that effect is clearly different from zero.

9 Discussion Questions

9.1 Question 1: Interpret the variable coefficients and ratings on attribute levels, and report strategic insights.

The model baseline is a policy with a 500 deductible, State Farm as the brand, and no pay-as-you-drive feature. The predicted baseline rating is 4.45.

The coefficients show how expected rating changes when one level changes while the other features are held constant. In this lab output, the strongest overall movement is Deductible1000, with an estimated effect of -1.00. The most positive movement is BrandAAA, while the most negative movement is Deductible1000.

The professor’s range-based importance calculation makes this more defensible than relying on visual guesswork alone. In the current output, Deductible is the largest overall driver, accounting for about 54.1% of the total modeled utility swing. I would use this as the primary prioritization signal while still checking the part-worth table and observed profile averages for context.

Strategically, the company should focus on the feature levels that increase predicted rating and be cautious about the levels that reduce predicted rating. The marginal means plot gives a quick descriptive read, while the regression coefficient plot is better for model-adjusted interpretation.

Visual summary for Question 1: interpreting the model baseline, coefficients, plots, and strategic implications.

Visual summary for Question 1: interpreting the model baseline, coefficients, plots, and strategic implications.

Source-of-truth note: The coefficient table above should be treated as the official numeric output for this report. The infographic is included as a visual explanation aid.

9.2 Question 2: Which single attribute level, if changed, would move a policy’s predicted rating the most?

Deductible1000 has the largest absolute coefficient, with an estimated effect of -1.00. This is demonstrated well in the regression coefficient graphic by the length of the bar. The longer the bar is from zero, the larger the expected movement in predicted rating.

This is the best answer because it uses the regression model rather than only raw averages. The marginal means plot is useful as a sanity check, but the coefficient plot is more appropriate for answering which attribute level moves predicted rating the most while holding the other attributes constant.

Visual summary for Question 2: identifying the single attribute level with the largest predicted rating movement.

Visual summary for Question 2: identifying the single attribute level with the largest predicted rating movement.

9.3 Question 3: What would be the primary challenges if conducting a project like this independently?

The main challenges would be designing realistic attributes and thoughtfully curated extremes like in the evil corporation and customer utopia examples, choosing levels that respondents can understand, keeping the survey short and interesting and engaging enough to avoid fatigue, collecting enough high-quality responses, randomizing profile order, and cleaning the data into the correct format for conducting analysis.

A second challenge is interpretation. Conjoint results are easy to overstate. The model estimates preference patterns within this sample and survey design; it does not prove that the same effects would hold for every insurance customer in the real market. Sample size should be much larger in a real business study, but this lab is useful because it demonstrates proof of concept and workflow reproducibility.

Visual summary for Question 3: major challenges in independently conducting a conjoint study.

Visual summary for Question 3: major challenges in independently conducting a conjoint study.

9.4 Question 4: How would you ensure useful and meaningful responses from participants?

I would meet with a focus group of different subject matter experts and conduct a short workshop to identify and rank target attributes to compare.

I would use clear descriptions, realistic policy profiles, a consistent rating scale, randomized profile order, and simple instructions. Clear descriptions reduce confusion. Realistic policy profiles reduce fake or meaningless trade-offs. A consistent rating scale reduces measurement noise. Randomized profile order helps mitigate order effects and fatigue effects. Simple instructions reduce misunderstanding and incomplete responses.

I would also screen for respondents who understand or are likely to purchase auto insurance. This helps mitigate the risk of collecting ratings from people who do not understand the product category or are not relevant to the business decision. I would check for incomplete responses, extremely fast completions, repeated identical ratings across every profile, and patterned or careless rating behavior such as simply clicking through 1, 2, 3, 4, 5, 6, 7 without evaluating the profiles. These checks are meant to mitigate low-effort responses, straight-lining, patterned answering, missing data, and other data quality problems.

To improve quality, I would pilot the survey with a small group first, revise confusing wording, and make sure the final dataset has one row per respondent-profile combination so the regression model can be estimated correctly. I would optimize my collection process instead of handling it ad hoc like we did in the other short lab.

Visual summary for Question 4: safeguards for useful and meaningful participant responses.

Visual summary for Question 4: safeguards for useful and meaningful participant responses.

10 Lab Takeaways

This lab shows how conjoint analysis turns ratings of bundled product profiles into estimates of attribute-level preference. The most important steps are:

  1. define clear attributes and levels;
  2. use a manageable profile design;
  3. collect ratings in long format;
  4. set factor levels intentionally;
  5. fit the main-effects part-worth regression model;
  6. convert baseline-relative coefficients into centered part-worth utilities;
  7. calculate range-based attribute importance;
  8. identify the highest-utility predicted profile;
  9. sanity-check the result against observed profile averages;
  10. translate the output into practical product or marketing decisions.

The business value is that a company can use a small, structured survey to estimate which product features are most likely to improve customer appeal before building or launching the final product.