3  Example: Candidate Choice

This chapter walks through a complete sconjoint analysis of a candidate-choice conjoint experiment whose design follows Saha and Weeks (2022). Respondents evaluate pairs of hypothetical political candidates varying on five attributes and select one. The structural decomposition into direction and intensity lets us ask whether a near-zero AMCE for candidate gender masks genuine heterogeneity — half the sample preferring women, half preferring men.

3.1 Data preparation

Load the bundled Saha-Weeks subset and inspect its structure.

data(sw2022, package = "sconjoint")
dim(sw2022)
[1] 7146   12
head(sw2022, 4)
         respondent task profile choice            agenda
1 R_3QRhChkenlzIXDL    3       1      0  Moderate Changes
2 R_3QRhChkenlzIXDL    1       1      0 Complete Overhaul
3 R_3QRhChkenlzIXDL    2       2      1  Moderate Changes
4 R_3QRhChkenlzIXDL    2       1      0  Moderate Changes
                 talent   children cand_gender prior_office resp_female age
1     Good Communicator 3 children        Male          Yes           0  55
2            Empathetic 2 children      Female          Yes           0  55
3         Collaborative    1 child      Female          Yes           0  55
4 Determined to Succeed    1 child        Male          Yes           0  55
       pid
1 Democrat
2 Democrat
3 Democrat
4 Democrat

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)
'data.frame':   7146 obs. of  12 variables:
 $ respondent  : chr  "R_3QRhChkenlzIXDL" "R_3QRhChkenlzIXDL" "R_3QRhChkenlzIXDL" "R_3QRhChkenlzIXDL" ...
 $ task        : int  3 1 2 2 3 1 2 3 3 1 ...
 $ profile     : int  1 1 2 1 2 2 2 1 2 1 ...
 $ choice      : int  0 0 1 0 1 1 0 0 1 0 ...
 $ agenda      : Factor w/ 3 levels "Very Few Changes",..: 2 3 2 2 1 1 3 3 2 2 ...
 $ talent      : Factor w/ 7 levels "Assertive","Collaborative",..: 5 4 2 3 4 4 5 6 2 2 ...
 $ children    : Factor w/ 4 levels "No children",..: 4 3 2 2 1 1 4 3 3 2 ...
 $ cand_gender : Factor w/ 2 levels "Male","Female": 1 2 2 1 1 2 1 1 2 1 ...
 $ prior_office: Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 2 1 ...
 $ resp_female : num  0 0 0 0 0 0 0 0 0 0 ...
 $ age         : num  55 55 55 55 55 55 42 42 42 42 ...
 $ pid         : Factor w/ 3 levels "Democrat","Republican (GOP)",..: 1 1 1 1 1 1 2 2 2 2 ...
table(table(sw2022$respondent))

   6 
1191 

For consistency with the paper, we re-level cand_gender so that Female is the reference and Male is the non-reference dummy. The paper reports the Male-relative-to-Female coefficient throughout.

sw2022$cand_gender <- stats::relevel(factor(sw2022$cand_gender),
                                     ref = "Female")
levels(sw2022$cand_gender)
[1] "Female" "Male"  

The respondent-level moderators available in \(\mathbf Z\) are resp_female (binary), age (standardized), and pid (3-level partisanship index). The formula interface puts conjoint attributes to the left of | and moderators to the right:

choice ~ agenda + talent + children + cand_gender + prior_office |
         resp_female + age + pid

3.2 Fitting

We fit a moderate configuration (K = 5 folds, 200 Adam epochs) that balances render speed against paper-quality estimates. A production run would use K = 10 and n_epochs >= 1000. The loss trace below shows the per-fold training curves; with K = 5 cross-fitting the loss has largely plateaued by epoch 150-200.

fit_sw <- scfit(
  choice ~ agenda + talent + children + cand_gender + prior_office |
           resp_female + age + pid,
  data        = sw2022,
  respondent  = "respondent",
  task        = "task",
  profile     = "profile",
  K           = 5L,
  n_epochs    = 200L,
  seed        = 2024
)
summary(fit_sw)
sc_fit summary
Call: scfit(formula = choice ~ agenda + talent + children + cand_gender + 
    prior_office | resp_female + age + pid, data = sw2022, respondent = "respondent", 
    task = "task", profile = "profile", K = 5L, n_epochs = 200L, 
    seed = 2024)

1191 respondents | 3573 observations | K = 5 folds
hidden = 32-32-16 | epochs = 200 | seed = 2024 | device = cpu

Coefficients (DML, respondent-clustered SE):
                            estimate std_error z_value   p_value     ci_lo
agendaModerate Changes       0.79178   0.06674  11.864 1.828e-32  0.660968
agendaComplete Overhaul      0.75877   0.07448  10.188 2.247e-24  0.612799
talentCollaborative          0.12379   0.09510   1.302 1.930e-01 -0.062594
talentDetermined to Succeed  0.18853   0.09269   2.034 4.196e-02  0.006856
talentEmpathetic             0.05126   0.09564   0.536 5.920e-01 -0.136188
talentGood Communicator      0.19390   0.09760   1.987 4.695e-02  0.002615
talentHard-Working           0.41964   0.09441   4.445 8.793e-06  0.234599
talentTough Negotiator       0.14792   0.09259   1.598 1.101e-01 -0.033547
children1 child              0.20583   0.07412   2.777 5.487e-03  0.060557
children2 children           0.36274   0.07191   5.045 4.545e-07  0.221806
children3 children           0.41481   0.07421   5.590 2.274e-08  0.269363
cand_genderMale             -0.09816   0.05151  -1.906 5.668e-02 -0.199120
prior_officeYes              0.07622   0.05378   1.417 1.564e-01 -0.029195
                               ci_hi
agendaModerate Changes      0.922582
agendaComplete Overhaul     0.904750
talentCollaborative         0.310174
talentDetermined to Succeed 0.370201
talentEmpathetic            0.238707
talentGood Communicator     0.385193
talentHard-Working          0.604672
talentTough Negotiator      0.329389
children1 child             0.351104
children2 children          0.503676
children3 children          0.560255
cand_genderMale             0.002792
prior_officeYes             0.181636

DML/iid SE ratio (mean): 1.036

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

As of v0.2, scfit() runs an empirical-Bayes MAP refinement (paper EnsC5) on top of the Stage-1 DNN by default (stage2 = "map_c5"). The summary above reports this as Stage 2: map_c5. DML point estimates and clustered standard errors are unchanged from v0.1 — they continue to use the Stage-1 single-DNN prediction (now stored on fit_sw$beta_hat_dnn). The refined betas (fit_sw$beta_hat) feed every distributional and individual-level quantity below. Set stage2 = "none" to recover v0.1 behavior, or stage2 = "mixed_logit" for the BLUP alternative from paper §A.4.

3.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)
     agendaModerate Changes     agendaComplete Overhaul 
                 0.79177533                  0.75877446 
        talentCollaborative talentDetermined to Succeed 
                 0.12379014                  0.18852857 
           talentEmpathetic     talentGood Communicator 
                 0.05125941                  0.19390393 
         talentHard-Working      talentTough Negotiator 
                 0.41963577                  0.14792072 
            children1 child          children2 children 
                 0.20583036                  0.36274078 
         children3 children             cand_genderMale 
                 0.41480863                 -0.09816363 
            prior_officeYes 
                 0.07622009 
plot_amce(fit_sw, groups = sw_groups, labels = sw_labels)

3.3.1 Structural AME vs reduced-form AMCE (paper Figure 1.B)

The paper notes that 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. Below 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[, c("dummy_name", "estimate", "se", "ci_lo", "ci_hi", "source")]
)
df_both$label <- ifelse(df_both$dummy_name %in% names(sw_labels),
                        sw_labels[df_both$dummy_name],
                        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 should overlap nearly point-for-point — the structural model loses nothing the reduced-form AMCE delivers.

3.4 Individual-level preferences

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

beta <- predict(fit_sw)
dim(beta)
[1] 3573   13
head(beta, 3)
     agendaModerate Changes agendaComplete Overhaul talentCollaborative
[1,]              0.9287207               0.9091837          0.09351506
[2,]              0.9287207               0.9091837          0.09351506
[3,]              0.9287207               0.9091837          0.09351506
     talentDetermined to Succeed talentEmpathetic talentGood Communicator
[1,]                   0.1427614       0.07249129               0.1843436
[2,]                   0.1427614       0.07249129               0.1843436
[3,]                   0.1427614       0.07249129               0.1843436
     talentHard-Working talentTough Negotiator children1 child
[1,]          0.4430627              0.2376771       0.2612657
[2,]          0.4430627              0.2376771       0.2612657
[3,]          0.4430627              0.2376771       0.2612657
     children2 children children3 children cand_genderMale prior_officeYes
[1,]          0.2823692          0.4246726     -0.06571506      0.05990728
[2,]          0.2823692          0.4246726     -0.06571506      0.05990728
[3,]          0.2823692          0.4246726     -0.06571506      0.05990728

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)

3.5 Structural quantities

3.5.1 Attribute importance

Variance-decomposition share of utility carried by each attribute group, averaged across respondents. The “importance” answers: which attribute moves the choice the most?

sc_importance(fit_sw)
sc_quantity: importance
  estimate: data.frame with 5 rows
    attribute   share        se   ci_lo   ci_hi
       agenda 0.69663 0.0024118 0.69190 0.70136
       talent 0.09755 0.0006877 0.09620 0.09890
     children 0.17467 0.0015040 0.17172 0.17762
  cand_gender 0.01116 0.0004003 0.01037 0.01194
 prior_office 0.01999 0.0009213 0.01819 0.02180
plot_importance(fit_sw, labels = c(agenda = "Agenda", talent = "Talent",
  children = "Children", cand_gender = "Gender", prior_office = "Prior Office"))

Policy agenda dominates the decision variance for the median voter, ranking well above the other attributes. Read the exact shares off the plot above rather than quoting a fixed fraction — on the bundled data they reflect the reduced moderator set (see the note in the overview).

3.5.2 Direction and intensity

For each dummy, decompose preference into a direction (the sign of \(\hat\beta_{ik}\)) and an intensity (its magnitude). Near-zero average effects 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
      agendaModerate Changes 1.0000 0.000000  1.0000  1.0000
     agendaComplete Overhaul 1.0000 0.000000  1.0000  1.0000
         talentCollaborative 0.9832 0.005290  0.9728  0.9936
 talentDetermined to Succeed 0.9983 0.001679  0.9950  1.0016
            talentEmpathetic 0.9043 0.012376  0.8800  0.9285
     talentGood Communicator 1.0000 0.000000  1.0000  1.0000
          talentHard-Working 1.0000 0.000000  1.0000  1.0000
      talentTough Negotiator 0.5668 0.023883  0.5199  0.6136
             children1 child 0.9916 0.003749  0.9843  0.9990
          children2 children 1.0000 0.000000  1.0000  1.0000
  ... 3 more rows
-- intensity --
sc_quantity: intensity
  estimate: data.frame with 13 rows
                  dummy_name       u     se_u ci_lo_u ci_hi_u
      agendaModerate Changes 0.76901 0.005838 0.75756 0.78045
     agendaComplete Overhaul 0.75851 0.008085 0.74267 0.77436
         talentCollaborative 0.09212 0.001050 0.09006 0.09418
 talentDetermined to Succeed 0.17173 0.001293 0.16919 0.17426
            talentEmpathetic 0.06418 0.001183 0.06186 0.06650
     talentGood Communicator 0.18949 0.002653 0.18429 0.19469
          talentHard-Working 0.39942 0.002627 0.39427 0.40457
      talentTough Negotiator 0.18845 0.004117 0.18038 0.19652
             children1 child 0.21982 0.003316 0.21332 0.22632
          children2 children 0.32917 0.001378 0.32647 0.33187
  ... 3 more rows

For the Empathetic attribute, the near-zero average masks an almost perfectly polarized electorate: approximately half favor empathetic candidates while the other half oppose them.

3.5.3 Fraction preferring

What share of respondents have a positive coefficient on each dummy? This is the direction-only summary — 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       agendaModerate Changes     1.0000000  0.0000000000
2      agendaComplete Overhaul     1.0000000  0.0000000000
3          talentCollaborative     0.9916037  0.0083963056
4  talentDetermined to Succeed     0.9991604  0.0008396306
5             talentEmpathetic     0.9521411  0.0478589421
6      talentGood Communicator     1.0000000  0.0000000000
7           talentHard-Working     1.0000000  0.0000000000
8       talentTough Negotiator     0.7833753  0.2166246851
9              children1 child     0.9958018  0.0041981528
10          children2 children     1.0000000  0.0000000000
11          children3 children     1.0000000  0.0000000000
12             cand_genderMale     0.1032746  0.8967254408
13             prior_officeYes     0.6910160  0.3089840470
plot_fraction(fit_sw, groups = sw_groups, labels = sw_labels)

The paper’s directional finding for gender: the population-average AMCE for Male is near zero, but the individual-level fractions reveal that a clear majority of respondents have \(\hat\beta_{\text{Male}} < 0\), i.e. prefer Female candidates. The fraction-preferring panel above makes this visible per dummy. (Read the exact share off the rendered table rather than quoting a fixed number — it depends on the training configuration and, on the bundled data, on the reduced moderator set.)

3.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.102   se = 0.01019   95% CI = [1.082, 1.122]
TipLarge trimmed mean?

The ratio \(\hat\beta_j / \hat\beta_k\) is unbounded when the denominator is near zero. Inspect the ridgelines above to confirm the denominator column is well-separated from zero.

3.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 attribute levels; unmentioned attributes default to the reference level.

sc_counterfactual(
  fit_sw,
  ## Profile A: a Female Hard-Working candidate proposing a Complete
  ## Overhaul, against B: a Male Assertive candidate proposing the
  ## minimal change.  Female is the reference level so it does not
  ## appear in the dummy contrast; Male contributes +beta_Male to A's
  ## utility (negative in this design).
  A = list(agenda = "Complete Overhaul", talent = "Hard-Working",
           children = "2 children", cand_gender = "Female",
           prior_office = "Yes"),
  B = list(agenda = "Very Few Changes", talent = "Assertive",
           children = "No children", cand_gender = "Male",
           prior_office = "No")
)
sc_quantity: counterfactual
  estimate = 0.8345   se = 0.001346   95% CI = [0.8319, 0.8371]

3.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.8324   se = 0.001054   95% CI = [0.8304, 0.8345]

3.5.7 Heterogeneity test

For each dummy, the one-sided test that \(\mathrm{Var}(\beta_k(Z)) > 0\) exceeds zero. 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 p_adjusted
      agendaModerate Changes 0.040575 1.236e-03  32.83 1.280e-236 5.547e-236
     agendaComplete Overhaul 0.077806 2.308e-03  33.71 2.051e-249 1.333e-248
         talentCollaborative 0.001324 5.464e-05  24.23 6.031e-130 8.711e-130
 talentDetermined to Succeed 0.001997 1.035e-04  19.30  2.770e-83  2.770e-83
            talentEmpathetic 0.001803 8.731e-05  20.65  4.680e-95  5.070e-95
     talentGood Communicator 0.008376 3.092e-04  27.09 5.735e-162 1.243e-161
          talentHard-Working 0.008215 2.796e-04  29.38 4.967e-190 1.292e-189
      talentTough Negotiator 0.029352 8.235e-04  35.64 1.528e-278 1.986e-277
             children1 child 0.013132 4.021e-04  32.66 2.750e-234 8.939e-234
          children2 children 0.002261 1.027e-04  22.02 8.357e-108 9.877e-108
 sig
 ***
 ***
 ***
 ***
 ***
 ***
 ***
 ***
 ***
 ***
  ... 3 more rows
plot_hetero(fit_sw, groups = sw_groups, labels = sw_labels)

3.5.8 Subgroup analysis

Re-average the respondent-level betas over a subset of respondents, then compare across subsets. Below: compare the per-attribute means for female vs male respondents on the gender attribute.

fem <- fit_sw$Z[, "resp_female"] > 0.5
sub_fem  <- sc_subgroup(fit_sw, fem)
sub_male <- sc_subgroup(fit_sw, !fem)
rbind(
  female = sub_fem$estimate[sub_fem$estimate$dummy_name == "cand_genderMale",
                            c("theta", "se")],
  male   = sub_male$estimate[sub_male$estimate$dummy_name == "cand_genderMale",
                            c("theta", "se")]
)
             theta          se
female -0.10391925 0.003125790
male   -0.09565024 0.003224108
plot_subgroup(
  fit_sw,
  subgroup = list(Female = fit_sw$Z[, "resp_female"] > 0.5,
                  Male   = fit_sw$Z[, "resp_female"] <= 0.5),
  groups = sw_groups, labels = sw_labels,
  title = "Subgroup AMCE: female vs male respondents"
)

3.5.9 Loss trace

plot(fit_sw, "loss_trace")

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

New in v0.2.1: 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 §6 heuristics. On the Saha-Weeks design (T ≈ 3, modest covariate set), expect mean-only support; the talent attributes are well-pinned by Z, while gender and prior-office rely most on T.

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.361

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):
  talentTough Negotiator          0.936
  talentGood Communicator         0.807
  talentHard-Working              0.805
  talentDetermined to Succeed     0.501
  talentEmpathetic                0.475
Bottom coefficients (rely most on T for recovery):
  children1 child                 0.183
  children3 children              0.0433
  prior_officeYes                 0.0344
  children2 children              0.0235
  cand_genderMale                 0.00671

3.5.11 Validating against the homogeneous-logit AMCE

The paper’s Appendix D shows 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.998

Pooled comparison (first 10 rows):
                   attribute dml_theta dml_se homog_logit_coef homog_logit_se
      agendaModerate Changes    0.7918 0.0667           0.7914         0.0142
     agendaComplete Overhaul    0.7588 0.0745           0.7696         0.0158
         talentCollaborative    0.1238 0.0951           0.1393         0.0217
 talentDetermined to Succeed    0.1885 0.0927           0.2163         0.0212
            talentEmpathetic    0.0513 0.0956           0.0959         0.0216
     talentGood Communicator    0.1939 0.0976           0.2040         0.0221
          talentHard-Working    0.4196 0.0944           0.4352         0.0213
      talentTough Negotiator    0.1479 0.0926           0.1757         0.0210
             children1 child    0.2058 0.0741           0.2386         0.0167
          children2 children    0.3627 0.0719           0.3572         0.0162
      diff abs_diff
  0.000408 0.000408
 -0.010787 0.010787
 -0.015532 0.015532
 -0.027781 0.027781
 -0.044631 0.044631
 -0.010083 0.010083
 -0.015571 0.015571
 -0.027826 0.027826
 -0.032771 0.032771
  0.005521 0.005521

In a full paper-quality fit (K = 10, n_epochs >= 1000) the correlation across attribute levels is very high (close to 1). Any material drop is a sign of either insufficient training or a genuinely non-logit data-generating process.

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

Every sc_* quantity now 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       agendaModerate Changes       1.000    1.000
2      agendaComplete Overhaul       1.000    1.000
3          talentCollaborative       0.992    1.000
4  talentDetermined to Succeed       0.999    0.976
5             talentEmpathetic       0.952    0.937
6      talentGood Communicator       1.000    1.000
7           talentHard-Working       1.000    1.000
8       talentTough Negotiator       0.783    0.784
9              children1 child       0.996    0.994
10          children2 children       1.000    1.000
11          children3 children       1.000    1.000
12             cand_genderMale       0.103    0.063
13             prior_officeYes       0.691    0.772

The Stage-2 refinement typically shifts the more polarized columns (e.g. cand_genderMale, talentEmpathetic) toward smaller fractions and the consensus columns (talentHard-Working, agendaModerate Changes) toward 100% — this is the within-respondent shrinkage at work.