Challenges · C05 · ★★★☆☆

Trolley problems: is a 1-7 rating a number?

Difficulty
★★★☆☆
Time
3-4 hours
Data
Cushman, Young & Hauser (2006): 9,930 permissibility ratings (1-7) of trolley-style dilemmas by 331 people
Skills
Designing a posterior predictive check for discrete data · ordered-logistic regression · the ordered transform and initial values · interpreting on the outcome scale · varying intercepts with 100+ groups · LOO · judging when a "wrong" model still gives the right answer

The brief

A survey-research team ran a moral-judgement study. Each participant read 30 short stories in which someone's action leads to a death, and rated how morally permissible the action was from 1 (absolutely not) to 7 (absolutely). The stories vary three features:

  • action - the harm comes from doing something rather than from failing to act;
  • intention - the death is a means to the agent's goal, not a side effect;
  • contact - the agent physically touches the victim (in this coding contact stories are not also flagged as action).

The team analyses the ratings the way most survey teams do: ordinary linear regression on the numbers 1-7. A reviewer has told them that this is indefensible for Likert-type data. The team lead is unconvinced: "Everyone does it and the effects are huge. Show me one conclusion that would change."

Your job is to find out who is right - and to be honest about it in both directions.

How this notebook works

  • Each task states what to deliver, not how. Write your code in the YOUR CODE HERE cells.
  • Stuck? h.hint("task2") reveals hints one level at a time: nudge → approach → code skeleton. Try to get by on nudges.
  • h.check("task2", cut_first=...) compares your numbers with the reference solution.
  • A full worked solution lives in notebooks/solutions/. Open it only when you are done (or truly stuck).
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
from scipy import stats
from scipy.special import expit, logit

from pymc_challenges import Hints, data

RANDOM_SEED = 2006
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-variat")

h = Hints("C05")
h.tasks()

C05 - Trolley problems: is a 1-7 rating a number?

  • task0 - Look at the outcome (2 hints)
  • task1 - The team's linear model and a check that exposes it (3 hints, 2 check(s))
  • task2 - Ordered logistic, cutpoints only (3 hints, 2 check(s))
  • task3 - Ordinal regression with predictors (3 hints, 4 check(s))
  • task4 - Predictions on the response scale (3 hints, 3 check(s))
  • task5 - Participant and story effects (3 hints, 2 check(s))
  • task6 - Does the metric shortcut change a conclusion? (3 hints, 4 check(s))

The data

One row per rating. response is the 1-7 rating, id the participant, story one of 12 story settings (a boat, a burning building, a switch...), and action, intention, contact the 0/1 features of the version of the story that was shown. age, male and edu describe the participant.

data.describe("trolley")
trolley = data.load("trolley")
trolley.head()
trolley: 9930 ordinal (1-7) permissibility ratings from 331 participants.
  source: Cushman, Young & Hauser (2006) moral-judgement experiment, via `rethinking`.
  url:    https://raw.githubusercontent.com/rmcelreath/rethinking/master/data/Trolley.csv
case response order id age male edu action intention contact story action2
0 cfaqu 4 2 96;434 14 0 Middle School 0 0 1 aqu 1
1 cfbur 3 31 96;434 14 0 Middle School 0 0 1 bur 1
2 cfrub 4 16 96;434 14 0 Middle School 0 0 1 rub 1
3 cibox 3 32 96;434 14 0 Middle School 0 1 1 box 1
4 cibur 3 4 96;434 14 0 Middle School 0 1 1 bur 1

Only six combinations of the three features occur in the experiment. We will call them scenarios and refer to them by these names throughout:

scenarios = pd.DataFrame(
    {"action": [0, 1, 0, 0, 1, 0], "intention": [0, 0, 0, 1, 1, 1], "contact": [0, 0, 1, 0, 0, 1]},
    index=pd.Index(
        ["none", "action", "contact", "intention", "action+intention", "contact+intention"],
        name="scenario",
    ),
)
key = ["action", "intention", "contact"]
trolley["scenario"] = pd.Categorical(
    trolley[key].merge(scenarios.reset_index(), on=key, how="left")["scenario"],
    categories=scenarios.index,
)
scenarios.assign(n=trolley.scenario.value_counts().reindex(scenarios.index))
action intention contact n
scenario
none 0 0 0 1655
action 1 0 0 2648
contact 0 0 1 993
intention 0 1 0 1986
action+intention 1 1 0 1655
contact+intention 0 1 1 993

Task 0 · Look at the outcome

Deliver

  1. The distribution of response over its seven values, overall and for each scenario.
  2. The cumulative proportions $P(\text{response} \le k)$ for $k = 1..6$ and the same numbers on the log-odds scale. Keep them - you will meet them again in Task 2.
  3. Two sentences: which features of this outcome will a Normal likelihood be unable to reproduce?
Your turn Work this task in the notebook.

Task 1 · The team's model

This is the model in the team's paper:

$$\text{response}_i \sim \text{Normal}(\mu_i, \sigma), \qquad \mu_i = a + b_A A_i + b_I I_i + b_C C_i + b_{AI} A_i I_i + b_{CI} C_i I_i$$

Deliver

  1. The fitted model with clean diagnostics.
  2. A posterior predictive check that you design to show the team what, if anything, is wrong with it. The responses are seven integers and the scenarios are very different from one another - a single smoothed density overlay will not do. Make the check reusable: you will want to run the same check on a better model later.
  3. One number for the abstract: what share of the model's predicted ratings are impossible (below 1 or above 7)?
Your turn Work this task in the notebook.

Task 2 · An ordinal likelihood

Leave the predictors aside for a moment. Model the seven categories as an ordered logistic outcome: a latent continuous "permissibility" with a logistic distribution, cut into seven bins by six cutpoints.

Deliver

  1. Before fitting: what does your prior on the cutpoints imply for the seven category probabilities? Show it.
  2. The fitted intercept-only model with clean diagnostics. If your first attempt dies before a single draw is taken, do not reach for a different sampler - find out which term of the log-probability is to blame at the starting point, and fix the model.
  3. Compare the posterior cutpoints with a number you already have from Task 0. What are the cutpoints, in words?
Your turn Work this task in the notebook.

Task 3 · Ordinal regression

Add the team's predictors - action, intention, contact and the two interactions with intention - to the ordinal model, with the convention that a positive coefficient means "more permissible".

Deliver

  1. Priors for the coefficients that you can defend on the log-odds scale.
  2. The fitted model with clean diagnostics. (Memory warning: by default pm.OrderedLogistic stores the seven category probabilities of every row for every draw. With 9,930 rows that is gigabytes. Find the switch.)
  3. Your Task 1 check, run on this model and shown next to the linear one.
Your turn Work this task in the notebook.

Task 4 · Back to the scale people answered on

"The contact:intention coefficient is -1.2 on the latent log-odds scale" is not a sentence you can put in front of the survey team.

Deliver, from the ordinal model, with 94% intervals:

  1. A figure of the predicted distribution over the seven categories for each scenario.
  2. A table with the expected rating and $P(\text{response} \le 3)$ ("leans impermissible") for each scenario.
  3. The headline contrast: going from none to contact+intention, how much does the expected rating fall, and by how many percentage points does $P(\text{response} \le 3)$ rise?
Your turn Work this task in the notebook.

Task 5 · People differ

Every participant rated 30 stories, and so far the models have treated their 30 answers as independent. Some people are strict, some lenient; some stories are simply more upsetting than others whatever their features.

To stay inside a laptop runtime budget this task uses a random subsample of 150 participants (4,500 ratings), defined below. Use it for every model in this task so that they can be compared.

Deliver

  1. An ordinal model with a varying intercept per participant and an effect per story, with clean diagnostics. 150 correlated intercepts next to six cutpoints is a location-identification problem waiting to happen - deal with it in the model.
  2. How large is the between-participant standard deviation compared with the biggest treatment effect? Put it in words.
  3. A LOO comparison with the Task 3 model refitted to the same subsample. How much do the participant and story effects matter for prediction?
  4. Put the coefficients of the two models side by side. They all moved away from zero. Explain why that is expected and not a contradiction.
# a dedicated generator, so that everybody gets the same 150 people whatever was run before
sub_ids = np.sort(np.random.default_rng(RANDOM_SEED).choice(trolley.id.unique(), size=150, replace=False))
sub = trolley[trolley.id.isin(sub_ids)].reset_index(drop=True)
print(f"{sub.id.nunique()} participants, {len(sub)} ratings")
150 participants, 4500 ratings
Your turn Work this task in the notebook.

Task 6 · So was the reviewer right?

Time to answer the team lead. Use the two population models fitted to the full data (Tasks 1 and 3) and, for the last part, the hierarchical model.

Deliver

  1. For every pair of scenarios (15 pairs) the effect of moving from one to the other according to each model and according to the raw data, on three quantities: the expected rating, $P(\text{response} \le 3)$ and $P(\text{response} = 1)$ ("absolutely impermissible"). For the linear model, take its predictive distribution rounded to the nearest category, with everything below 1.5 counted as 1 and everything above 6.5 as 7. For which pair, and which quantity, do the two models disagree most - and which of them is closer to the data?
  2. The linear model says the none → contact+intention drop is the same number of rating points for everybody. What does the hierarchical ordinal model say for a typical participant, a strict one (2 sd below) and a lenient one (2 sd above)? Is there any sign of this in the raw data?
  3. A paragraph for the team lead: which of their conclusions survive, which do not, and what should they change?
Your turn Work this task in the notebook.

Going further

  • The ordinal model over-predicts the midpoint 4 in the two harshest scenarios. Relax proportional odds - for example let intention shift the cutpoints rather than the latent mean, or add a "fence-sitter" mixture component at 4 - and see whether the Task 1 check comes clean.
  • Give every participant their own contact:intention slope (correlated with their intercept via pm.LKJCholeskyCov). Does it reproduce the gradient in the observed drops across the leniency fifths that the intercept-only model missed?
  • edu is itself ordinal. Model it as a monotonic predictor: a total effect times the cumulative sum of a pm.Dirichlet simplex over the education steps. Add age and male.
  • Fit the hierarchical model to all 331 participants (a few minutes with nutpie). Do the subsample's conclusions hold up?
h.progress()

C05: 0/20 hints used, 17/17 checks passed.

Reference solution

A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.

Open the worked solution →