5  Example: Candidate Choice

This chapter walks through a complete sconjoint analysis of the candidate-choice conjoint experiment of Saha and Weeks (2022). Respondents evaluate pairs of hypothetical political candidates and select one. The structural decomposition into direction and intensity lets us ask whether a near-zero average effect for candidate gender masks genuine heterogeneity — opposing camps of voters who prefer women and men.

The bundled sw2022 is the paper’s full analysis frame, so the quantities below reproduce the published estimates. The fit uses the paper’s production configuration (K = 10 cross-fitting folds and the default 1,000 Adam epochs); it is heavier than a quick demo and takes a couple of minutes to run.

5.1 Data preparation

Load the data and inspect its structure.

data(sw2022, package = "sconjoint")
dim(sw2022)
[1] 7146   37

The data are in long format: one row per respondent–task–profile. Each respondent contributes three forced-choice tasks; within a task two candidate profiles are presented and exactly one is chosen (choice == 1).

str(sw2022, list.len = 12)
'data.frame':   7146 obs. of  37 variables:
 $ respondent                      : chr  "R_02LtLl5av6RqSoV" "R_02LtLl5av6RqSoV" "R_02LtLl5av6RqSoV" "R_02LtLl5av6RqSoV" ...
 $ task                            : int  1 1 2 2 3 3 1 1 2 2 ...
 $ profile                         : int  1 2 1 2 1 2 1 2 1 2 ...
 $ choice                          : int  1 0 1 0 1 0 0 1 1 0 ...
 $ cand_genderMale                 : num  1 0 0 0 1 1 0 1 1 0 ...
 $ cand_runYes                     : num  1 1 0 0 1 0 1 0 1 1 ...
 $ cand_talentCollaborative        : num  0 0 0 0 0 0 0 1 0 0 ...
 $ cand_talentDetermined.to.Succeed: num  0 0 0 0 0 1 0 0 0 0 ...
 $ cand_talentEmpathetic           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cand_talentGood.Communicator    : num  0 0 0 1 0 0 1 0 0 0 ...
 $ cand_talentHard.Working         : num  0 1 1 0 1 0 0 0 1 0 ...
 $ cand_talentTough.Negotiator     : num  1 0 0 0 0 0 0 0 0 0 ...
  [list output truncated]
table(table(sw2022$respondent))

   6 
1191 

The thirteen candidate attributes are already coded as 0/1 dummies, with the omitted level of each attribute serving as its reference (Female for gender, no prior run for ambition, and so on). The respondent-level moderators in \(\mathbf Z\) are the full set of nineteen covariates from the paper — gender, age, income, education, party, region, employment status, ideology, vote choice, and a gender-attitudes scale. The convenience factor resp_party (Democrat / Independent / Republican) is carried alongside for subgroup labelling but is not part of \(\mathbf Z\).

The formula interface puts conjoint attributes to the left of | and moderators to the right. Column names containing dots are backquoted:

attrs <- c("cand_genderMale", "cand_runYes",
           "cand_talentCollaborative", "cand_talentDetermined.to.Succeed",
           "cand_talentEmpathetic", "cand_talentGood.Communicator",
           "cand_talentHard.Working", "cand_talentTough.Negotiator",
           "cand_agendaModerate.Changes", "cand_agendaComplete.Overhaul",
           "cand_child1.child", "cand_child2.children", "cand_child3.children")
Zvars <- c("gender_num", "age", "income", "educ_Middle", "educ_High",
           "party_Republican", "party_Independent", "region_NORTHEAST",
           "region_SOUTH", "region_WEST", "employ_parttime",
           "employ_homemaker", "employ_not_working", "employ_retired",
           "employ_student", "ideo_conservative", "vote_trump",
           "vote_clinton", "gender_att")

bt  <- function(x) paste0("`", x, "`")
sw_formula <- stats::as.formula(
  paste("choice ~", paste(bt(attrs), collapse = " + "),
        "|",       paste(bt(Zvars), collapse = " + "))
)

5.2 Fitting

We fit the production configuration: K = 10 cross-fitting folds and the default 1,000 Adam epochs. This is the recipe that reproduces the paper, so the chunk is cached — rendering reuses the saved fit rather than retraining each time. The loss trace later in the chapter shows the per-fold training curves.

fit_sw <- scfit(
  sw_formula,
  data       = sw2022,
  respondent = "respondent",
  task       = "task",
  profile    = "profile",
  K          = 10L,
  seed       = 42L,
  stage2     = "map_c5",
  parallel   = TRUE,
  n_cores    = 10L
)
summary(fit_sw)
sc_fit summary
Call: scfit(formula = sw_formula, data = sw2022, respondent = "respondent", 
    task = "task", profile = "profile", K = 10L, seed = 42L, 
    parallel = TRUE, n_cores = 10L, stage2 = "map_c5")

1191 respondents | 3573 observations | K = 10 folds
hidden = 32-32-16 | epochs = 1000 | seed = 42 | device = cpu | parallel (10 cores)

Coefficients (DML, respondent-clustered SE):
                                 estimate std_error z_value   p_value
cand_genderMale                  -0.10309   0.06847 -1.5055 1.322e-01
cand_runYes                       0.05932   0.06830  0.8684 3.851e-01
cand_talentCollaborative          0.18081   0.11827  1.5288 1.263e-01
cand_talentDetermined.to.Succeed  0.26879   0.11525  2.3323 1.969e-02
cand_talentEmpathetic             0.06556   0.12456  0.5263 5.987e-01
cand_talentGood.Communicator      0.25585   0.11803  2.1676 3.019e-02
cand_talentHard.Working           0.54652   0.11942  4.5763 4.732e-06
cand_talentTough.Negotiator       0.16607   0.11643  1.4264 1.538e-01
cand_agendaModerate.Changes       0.95279   0.08493 11.2189 3.293e-29
cand_agendaComplete.Overhaul      0.94238   0.09733  9.6820 3.597e-22
cand_child1.child                 0.26575   0.09388  2.8308 4.643e-03
cand_child2.children              0.42296   0.09278  4.5588 5.146e-06
cand_child3.children              0.51191   0.09074  5.6416 1.685e-08
                                    ci_lo   ci_hi
cand_genderMale                  -0.23729 0.03112
cand_runYes                      -0.07456 0.19319
cand_talentCollaborative         -0.05099 0.41261
cand_talentDetermined.to.Succeed  0.04291 0.49467
cand_talentEmpathetic            -0.17857 0.30969
cand_talentGood.Communicator      0.02451 0.48719
cand_talentHard.Working           0.31246 0.78059
cand_talentTough.Negotiator      -0.06212 0.39427
cand_agendaModerate.Changes       0.78634 1.11925
cand_agendaComplete.Overhaul      0.75161 1.13315
cand_child1.child                 0.08175 0.44974
cand_child2.children              0.24112 0.60481
cand_child3.children              0.33407 0.68976

DML/iid SE ratio (mean): 1.032

Stage 2: map_c5 | mean(sigma_prior) = 0.1272
NoteThe Stage-2 default

scfit() reports Stage 2: map_c5, the empirical-Bayes MAP refinement that runs by default; Chapter 3 explains the Stage-1 / Stage-2 split and the stage2 options. DML estimates use the Stage-1 DNN (fit_sw$beta_hat_dnn); the refined fit_sw$beta_hat feeds the distributional and individual-level quantities below.

5.3 Population-average estimates

The DML point estimates \(\hat\theta_k\) on the logit scale. Inspect numerically, then visualize as a coefficient plot grouped by attribute.

coef(fit_sw)
                 cand_genderMale                      cand_runYes 
                     -0.10308858                       0.05931866 
        cand_talentCollaborative cand_talentDetermined.to.Succeed 
                      0.18081164                       0.26878850 
           cand_talentEmpathetic     cand_talentGood.Communicator 
                      0.06555814                       0.25584984 
         cand_talentHard.Working      cand_talentTough.Negotiator 
                      0.54652422                       0.16607249 
     cand_agendaModerate.Changes     cand_agendaComplete.Overhaul 
                      0.95279401                       0.94237735 
               cand_child1.child             cand_child2.children 
                      0.26574584                       0.42296203 
            cand_child3.children 
                      0.51191480 

Agenda dominates: relative to keeping the status quo, both moderate changes and a complete overhaul raise candidate utility by about 0.95 on the logit scale. Among the talent cues, hard-working is the strongest (about +0.55). The average effect of a male candidate is small and negative (about \(-0.10\), confidence interval covering zero) — the near-zero average we return to below.

plot_amce(fit_sw, groups = sw_groups, labels = sw_labels)

5.3.1 Structural AME vs reduced-form AMCE

The structural model’s average marginal effect (AME) on the probability scale nearly coincides with the standard linear-probability-model AMCE, confirming that the structural estimator nests reduced-form AMCE analysis. We compute both and overlay them as point-ranges on the probability scale.

## Structural AME on the probability scale (delta-method via plogis')
ame_struct <- sc_average(fit_sw, scale = "probability")
ame_df     <- ame_struct$estimate
ame_df$source <- "Structural AME"

## Linear-probability-model AMCE
lpm        <- sc_baseline_lpm(fit_sw)
amce_df    <- data.frame(
  dummy_name = names(stats::coef(lpm)),
  estimate   = unname(stats::coef(lpm)),
  se         = sqrt(diag(stats::vcov(lpm))),
  source     = "LPM AMCE",
  stringsAsFactors = FALSE
)
amce_df$ci_lo <- amce_df$estimate - 1.96 * amce_df$se
amce_df$ci_hi <- amce_df$estimate + 1.96 * amce_df$se

df_both <- rbind(
  ame_df[, c("dummy_name", "estimate", "se", "ci_lo", "ci_hi", "source")],
  amce_df[amce_df$dummy_name %in% names(sw_labels),
          c("dummy_name", "estimate", "se", "ci_lo", "ci_hi", "source")]
)
df_both$label <- sw_labels[df_both$dummy_name]
df_both$label <- factor(df_both$label, levels = rev(unname(sw_labels)))
df_both$group <- sw_groups[df_both$dummy_name]
df_both$group <- factor(df_both$group, levels = unique(sw_groups))

ggplot(df_both, aes(x = estimate, y = label, color = source)) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
  geom_pointrange(aes(xmin = ci_lo, xmax = ci_hi),
                  position = position_dodge(width = 0.5),
                  size = 0.35, fatten = 2) +
  scale_color_manual(values = c("Structural AME" = "#2166AC",
                                "LPM AMCE"        = "#B2182B")) +
  facet_grid(group ~ ., scales = "free_y", space = "free_y") +
  labs(x = "Effect on Pr(choice)",
       y = NULL, color = NULL,
       title = "Structural AME vs LPM AMCE (probability scale)") +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom",
        strip.text.y    = element_text(angle = 0, hjust = 0, face = "bold"))

The two series overlap nearly point-for-point: the structural model loses nothing the reduced-form AMCE provides. The sc_validate_amce() check at the end of the chapter quantifies this agreement.

5.4 Individual-level preferences

Pull the respondent-level \(\hat\beta(\mathbf Z_i)\) matrix. With the default stage2 = "map_c5", this is the MAP-refined hybrid view.

beta <- predict(fit_sw)
dim(beta)
[1] 3573   13
round(beta[1:3, 1:6], 2)
     cand_genderMale cand_runYes cand_talentCollaborative
[1,]           -0.08        0.25                     0.19
[2,]           -0.08        0.25                     0.19
[3,]           -0.08        0.25                     0.19
     cand_talentDetermined.to.Succeed cand_talentEmpathetic
[1,]                             0.26                  0.21
[2,]                             0.26                  0.21
[3,]                             0.26                  0.21
     cand_talentGood.Communicator
[1,]                         0.29
[2,]                         0.29
[3,]                         0.29

Ridgeline densities show the distribution of \(\hat\beta_k(\mathbf Z_i)\) per dummy across respondents. The vertical dashed line is at zero; the solid line in each density is the median.

plot(fit_sw, "beta_ridgelines", groups = sw_groups, labels = sw_labels)

The gender density is centred just left of zero but spreads to both sides — the visual signature of two opposing camps rather than a shared near-indifference, a pattern we make precise next.

5.5 Structural quantities

5.5.1 Attribute importance

The variance-decomposition share of utility carried by each attribute, averaged across respondents. Importance answers: which attribute moves the choice the most? Because the bundled formula enters each level as a separate dummy, we first tell sc_importance() which dummies belong to the same attribute (via the sw_attr_map() helper defined in the setup chunk), then read off the paper’s five-attribute decomposition.

fit_grp <- sw_attr_map(fit_sw)
imp <- sc_importance(fit_grp, design = "uniform")
imp$estimate[, c("attribute", "share", "se")]
    attribute      share           se
1 cand_gender 0.16051704 0.0037788949
2    cand_run 0.02033821 0.0009475866
3 cand_talent 0.20272507 0.0031909265
4 cand_agenda 0.53135141 0.0054917708
5  cand_child 0.08506827 0.0015072658
plot_importance(fit_grp, labels = c(
  cand_agenda = "Agenda", cand_talent = "Talent",
  cand_child = "Children", cand_gender = "Gender", cand_run = "Prior run"))

Policy agenda dominates the decision variance, carrying roughly half the explained variation. Candidate gender accounts for about 16% of the variance — the third-largest attribute, behind agenda and talent and well ahead of children and prior office. This is the first sign that gender matters far more than its near-zero average effect suggests: the variance is large precisely because respondents disagree about its sign.

5.5.2 Direction and intensity

For each dummy, decompose preference into a direction (the sign of \(\hat\beta_{ik}\)) and an intensity (its magnitude). A near-zero average effect can hide either consensus (everyone close to zero) or polarization (large opposing intensities cancelling).

sc_direction_intensity(fit_sw)
sc_quantity_bivariate: direction_intensity
-- direction --
sc_quantity: direction
  estimate: data.frame with 13 rows
                       dummy_name        d     se_d ci_lo_d  ci_hi_d
                  cand_genderMale -0.06969 0.028918 -0.1264 -0.01301
                      cand_runYes  0.25945 0.027996  0.2046  0.31432
         cand_talentCollaborative  0.38203 0.026790  0.3295  0.43454
 cand_talentDetermined.to.Succeed  0.94962 0.009085  0.9318  0.96743
            cand_talentEmpathetic  0.01092 0.028987 -0.0459  0.06773
     cand_talentGood.Communicator  0.84215 0.015632  0.8115  0.87279
          cand_talentHard.Working  0.97649 0.006249  0.9642  0.98874
      cand_talentTough.Negotiator  0.35013 0.027154  0.2969  0.40335
      cand_agendaModerate.Changes  0.97817 0.006024  0.9664  0.98998
     cand_agendaComplete.Overhaul  0.88077 0.013727  0.8539  0.90768
  ... 3 more rows
-- intensity --
sc_quantity: intensity
  estimate: data.frame with 13 rows
                       dummy_name      u     se_u ci_lo_u ci_hi_u
                  cand_genderMale 0.5744 0.012386  0.5502  0.5987
                      cand_runYes 0.1523 0.003455  0.1455  0.1590
         cand_talentCollaborative 0.2136 0.004637  0.2045  0.2227
 cand_talentDetermined.to.Succeed 0.2449 0.003271  0.2385  0.2513
            cand_talentEmpathetic 0.5609 0.012312  0.5368  0.5850
     cand_talentGood.Communicator 0.2769 0.004781  0.2675  0.2862
          cand_talentHard.Working 0.4838 0.006025  0.4720  0.4956
      cand_talentTough.Negotiator 0.4464 0.009701  0.4274  0.4655
      cand_agendaModerate.Changes 0.9121 0.012158  0.8882  0.9359
     cand_agendaComplete.Overhaul 0.9713 0.017771  0.9364  1.0061
  ... 3 more rows

Candidate gender is the paper’s clearest example of cancellation: its direction summary sits near zero (a slim majority lean toward female candidates), but its intensity is substantial — the opposing camps hold their preferences with real strength, and they cancel in the average.

5.5.3 Fraction preferring

What share of respondents have a positive coefficient on each dummy? This is the direction-only summary — it ignores intensity.

frac <- sc_fraction_preferring(fit_sw, threshold = 0)
frac$estimate[, c("dummy_name", "frac_positive", "frac_negative")]
                         dummy_name frac_positive frac_negative
1                   cand_genderMale     0.4651553   0.534844668
2                       cand_runYes     0.6297229   0.370277078
3          cand_talentCollaborative     0.6910160   0.308984047
4  cand_talentDetermined.to.Succeed     0.9748111   0.025188917
5             cand_talentEmpathetic     0.5054576   0.494542401
6      cand_talentGood.Communicator     0.9210747   0.078925273
7           cand_talentHard.Working     0.9882452   0.011754828
8       cand_talentTough.Negotiator     0.6750630   0.324937028
9       cand_agendaModerate.Changes     0.9890848   0.010915197
10     cand_agendaComplete.Overhaul     0.9403862   0.059613770
11                cand_child1.child     0.9840470   0.015952981
12             cand_child2.children     1.0000000   0.000000000
13             cand_child3.children     0.9949622   0.005037783
plot_fraction(fit_sw, groups = sw_groups, labels = sw_labels)

The directional finding for gender: the population-average effect for male is near zero, yet a clear majority of respondents have \(\hat\beta_{\text{Male}} < 0\) — about 54% prefer female candidates against 46% who prefer men. The electorate is split close to evenly on gender, not indifferent to it. The partisan structure behind that split appears in the subgroup section below.

5.5.4 Marginal rate of substitution

The respondent-averaged trimmed ratio \(\hat\beta_j / \hat\beta_k\) between two dummies, with respondent-clustered SE. Trimming at \(\{0.01, 0.99\}\) guards the unbounded ratio when the denominator is near zero.

sc_mrs(fit_sw, numerator = 1L, denominator = 2L)
sc_quantity: mrs
  estimate = -1.679   se = 0.812   95% CI = [-3.271, -0.08798]
TipLarge trimmed mean?

The ratio \(\hat\beta_j / \hat\beta_k\) is unbounded when the denominator is near zero. Here the numerator is the male dummy, whose recovered effect is near zero for many respondents, so the ratio is noisy; inspect the ridgelines above to confirm the denominator column is well-separated from zero before reading a MRS literally.

5.5.5 Counterfactual choice probability

Predicted choice probability for a specific head-to-head profile pair, averaged across respondents. Each profile is a named list of dummy settings; unmentioned dummies stay at zero (the reference level).

sc_counterfactual(
  fit_sw,
  ## Profile A: a Female, Empathetic candidate proposing a Complete
  ## Overhaul, with two children and a prior run for office. Female is
  ## the reference level, so cand_genderMale stays 0.
  ## Profile B: a Male, Tough-Negotiator candidate with no prior run and
  ## the omitted (status-quo) agenda.
  A = list(cand_talentEmpathetic = 1, cand_agendaComplete.Overhaul = 1,
           cand_child2.children = 1, cand_runYes = 1),
  B = list(cand_genderMale = 1, cand_talentTough.Negotiator = 1)
)
sc_quantity: counterfactual
  estimate = 0.7631   se = 0.03403   95% CI = [0.6964, 0.8298]

5.5.6 Optimal profile

The single profile that maximizes the population-average utility (greedy attribute-by-attribute selection over the discrete attribute levels).

sc_optimal_profile(fit_sw)
sc_quantity: optimal_profile
  estimate = 0.9773   se = 0.0008319   95% CI = [0.9757, 0.9789]

5.5.7 Heterogeneity test

For each dummy, the one-sided test that \(\mathrm{Var}(\beta_k(Z)) > 0\). Useful for screening which attributes genuinely vary across respondents.

sc_heterogeneity_test(fit_sw, adjust = "bh")
sc_quantity: heterogeneity_test
  estimate: data.frame with 13 rows
                       dummy_name var_beta    se_var t_stat    p_value
                  cand_genderMale  0.50773 0.0203844  24.91 3.053e-137
                      cand_runYes  0.03394 0.0014738  23.03 1.266e-117
         cand_talentCollaborative  0.05622 0.0023005  24.44 3.560e-132
 cand_talentDetermined.to.Succeed  0.01427 0.0006844  20.85  6.901e-97
            cand_talentEmpathetic  0.49426 0.0205142  24.09 1.462e-128
     cand_talentGood.Communicator  0.03531 0.0015258  23.14 8.429e-119
          cand_talentHard.Working  0.04462 0.0021305  20.95  1.041e-97
      cand_talentTough.Negotiator  0.25649 0.0109576  23.41 1.800e-121
      cand_agendaModerate.Changes  0.17947 0.0071617  25.06 6.757e-139
     cand_agendaComplete.Overhaul  0.43795 0.0206981  21.16  1.145e-99
 p_adjusted sig
 1.984e-136 ***
 2.352e-117 ***
 1.543e-131 ***
  8.155e-97 ***
 4.753e-128 ***
 1.826e-118 ***
  1.353e-97 ***
 4.680e-121 ***
 8.784e-138 ***
  1.654e-99 ***
  ... 3 more rows
plot_hetero(fit_sw, groups = sw_groups, labels = sw_labels)

Candidate gender shows highly significant variance in \(\beta(Z)\) — the formal counterpart to this cancellation: the spread around the near-zero mean is real, not sampling noise.

5.5.8 Subgroup analysis

Re-average the respondent-level betas over a subset of respondents, then compare across subsets. The paper’s main result is partisan: the null average effect of gender masks Democrats who prefer women and Republicans who prefer men. We recover it by splitting on resp_party. Subgroup masks are aligned to the rows of fit_sw$beta_hat, which Z shares, so we build them from the party indicators in fit_sw$Z.

Z   <- fit_sw$Z
dem <- Z[, "party_Republican"] == 0 & Z[, "party_Independent"] == 0
ind <- Z[, "party_Independent"] == 1
rep <- Z[, "party_Republican"] == 1

male_row <- function(mask) {
  s <- sc_subgroup(fit_sw, mask)
  s$estimate[s$estimate$dummy_name == "cand_genderMale", c("theta", "se")]
}
rbind(
  Democrat    = male_row(dem),
  Independent = male_row(ind),
  Republican  = male_row(rep)
)
                 theta         se
Democrat    -0.2695236 0.03494222
Independent -0.1613402 0.03652892
Republican   0.2269224 0.03063728

The recovered party means show the paper’s finding directly: the average male coefficient is clearly negative for Democrats (they prefer women), clearly positive for Republicans (they prefer men), and near zero for Independents. Two camps of comparable strength pull in opposite directions, and the overall average lands near zero by cancellation, not by indifference.

plot_subgroup(
  fit_sw,
  subgroup = list(Democrat = dem, Independent = ind, Republican = rep),
  dummies  = "cand_genderMale",
  labels   = sw_labels,
  title    = "Recovered male-candidate preference, by party"
)

We can run the same split on the respondent’s own gender, coded in the standardized moderator gender_num (its two levels label the two coded groups; the package does not store which sign is female, so we keep the labels neutral):

g_lo <- fit_sw$Z[, "gender_num"] < 0
g_hi <- fit_sw$Z[, "gender_num"] > 0
rbind(
  `gender_num < 0` = male_row(g_lo),
  `gender_num > 0` = male_row(g_hi)
)
                     theta         se
gender_num < 0 -0.19362972 0.02864491
gender_num > 0  0.05204795 0.02891781

5.5.9 Loss trace

plot(fit_sw, "loss_trace")

5.5.10 Design diagnostic (R²_Z + recovery-tier hint)

sc_design_diagnostic() estimates per-coefficient \(\hat R^2_{Z,k}\) from the MAP posterior and reports which recovery tiers the design supports (mean / distributional / individual / ratio) per the paper’s design-guidance heuristics. On the Saha-Weeks design (three tasks per respondent, the full nineteen-covariate set) the talent attributes are well pinned by \(\mathbf Z\), while gender, prior run, and children rely most on the within-respondent task variation.

sc_design_diagnostic(fit_sw)
sc_design_diagnostic --- recovery-tier hint
[experimental: estimator over-estimates R^2_Z when Z is
 uninformative (validation bias +0.4 at true R^2_Z = 0.10).
 Use for relative comparisons across coefficients; do not
 treat tier hints as a pass/fail gate.  See ?sc_design_diagnostic.]
Stage 2: map_c5
Respondents: 1191 | Tasks: 3573 | T_mean: 3 | mean R^2_Z: 0.594

Recovery tiers:
  [YES] mean & aggregate        (any reasonable design)
  [NO]  distributional          (T >= 5 and R^2_Z >= 0.35)
  [NO]  individual-level        (T >= 8 and R^2_Z >= 0.55)
  [NO]  ratio (MRS / WTP)       (T >= 10 and R^2_Z >= 0.55 and N >= 5000)

Top coefficients by R^2_Z (best-pinned by Z):
  cand_talentTough.Negotiator     0.992
  cand_talentCollaborative        0.966
  cand_talentHard.Working         0.957
  cand_talentEmpathetic           0.951
  cand_talentGood.Communicator    0.946
Bottom coefficients (rely most on T for recovery):
  cand_agendaModerate.Changes     0.429
  cand_child3.children            0.157
  cand_child1.child               0.0895
  cand_runYes                     0.0612
  cand_child2.children            0.06

5.5.11 Validating against the homogeneous-logit AMCE

The paper’s Supplementary Materials (Section D) show that the structural model nests the standard reduced-form AMCE: the DML population-average parameter \(\theta_k = \mathbb{E}[\beta_k(Z)]\) equals the pooled homogeneous-logit coefficient on attribute \(k\) when the logit specification holds. The sc_validate_amce() export turns this into a one-line sanity check.

v <- sc_validate_amce(fit_sw)
v
sc_validate_amce -- pooled and (optionally) subgroup comparison
Stage 2: map_c5
N obs: 3573, N respondents: 1191, P attributes: 13
Pooled correlation (DML theta vs homogeneous logit coef): 0.997

Pooled comparison (first 10 rows):
                        attribute dml_theta dml_se homog_logit_coef
                  cand_genderMale   -0.1031 0.0685          -0.0952
                      cand_runYes    0.0593 0.0683           0.0699
         cand_talentCollaborative    0.1808 0.1183           0.1393
 cand_talentDetermined.to.Succeed    0.2688 0.1152           0.2163
            cand_talentEmpathetic    0.0656 0.1246           0.0959
     cand_talentGood.Communicator    0.2558 0.1180           0.2040
          cand_talentHard.Working    0.5465 0.1194           0.4352
      cand_talentTough.Negotiator    0.1661 0.1164           0.1757
      cand_agendaModerate.Changes    0.9528 0.0849           0.7914
     cand_agendaComplete.Overhaul    0.9424 0.0973           0.7696
 homog_logit_se     diff abs_diff
         0.0116 -0.00791  0.00791
         0.0122 -0.01061  0.01061
         0.0217  0.04149  0.04149
         0.0212  0.05248  0.05248
         0.0216 -0.03033  0.03033
         0.0221  0.05186  0.05186
         0.0213  0.11132  0.11132
         0.0210 -0.00967  0.00967
         0.0142  0.16143  0.16143
         0.0158  0.17282  0.17282

At the production configuration used here (K = 10, 1,000 epochs) the correlation across attribute levels is very high (close to 1), confirming that the structural estimator reproduces the reduced-form benchmark. A material drop would signal either insufficient training or a genuinely non-logit data-generating process.

5.5.12 Comparing the Stage-2 view to the raw DNN view

Every sc_* quantity accepts a which_beta argument with default "hybrid" (Stage-2 refined). Passing "dnn" reads the unrefined Stage-1 DNN mean, which lives in fit_sw$beta_hat_dnn.

frac_hyb <- sc_fraction_preferring(fit_sw, which_beta = "hybrid")
frac_dnn <- sc_fraction_preferring(fit_sw, which_beta = "dnn")
data.frame(
  attribute   = frac_hyb$estimate$dummy_name,
  hybrid_pref = round(frac_hyb$estimate$frac_positive, 3),
  dnn_pref    = round(frac_dnn$estimate$frac_positive, 3)
)
                          attribute hybrid_pref dnn_pref
1                   cand_genderMale       0.465    0.433
2                       cand_runYes       0.630    0.736
3          cand_talentCollaborative       0.691    0.720
4  cand_talentDetermined.to.Succeed       0.975    0.931
5             cand_talentEmpathetic       0.505    0.541
6      cand_talentGood.Communicator       0.921    0.919
7           cand_talentHard.Working       0.988    0.970
8       cand_talentTough.Negotiator       0.675    0.668
9       cand_agendaModerate.Changes       0.989    0.983
10     cand_agendaComplete.Overhaul       0.940    0.893
11                cand_child1.child       0.984    0.963
12             cand_child2.children       1.000    0.994
13             cand_child3.children       0.995    0.990

The hybrid and raw-DNN fractions can differ because the Stage-2 MAP update pulls each respondent’s coefficients toward the mean stage. How much a column’s fraction moves — and in which direction — depends on how much it leaned on noisy individual variation; compare the two columns in the table above.

5.6 Summary

The candidate application is the clearest illustration of why averages can mislead:

  • The population marginal effect of a male candidate is close to zero, which a reduced-form analysis would read as voter indifference to gender.
  • The recovered distribution shows the opposite: the near-zero average is the sum of two opposed camps, with Democrats favoring women and Republicans favoring men.
  • Policy agenda dominates the importance decomposition, and gender is the one attribute that divides voters in direction rather than only in intensity.
  • With only three tasks per respondent, we read this application as illustrative of what the model can describe in a deliberately sparse design; the paper rests its distributional conclusions mainly on the richer democracy and tax conjoints.