Monty Hall Simulation
Source: examples/monty_hall_simulation.py
Introduction
Model the Monty Hall game as a tiny two-condition drex.Study and simulate
100 random games for each strategy to show why switching usually wins more
often than staying.
Technical Implementation
Define a study with one manipulated factor (
strategy) and two levels:stayandswitch.Validate the study and materialize the two conditions with
drex.build_design.Simulate 100 seeded random Monty Hall games for each condition, write a summary CSV artifact, and print the resulting win counts.
1from __future__ import annotations
2
3import csv
4import random
5from pathlib import Path
6
7import design_research_experiments as drex
8
9DOORS = ("A", "B", "C")
10SIMULATED_GAMES = 100
11SIMULATION_SEED = 5
12
13
14def build_monty_hall_study(output_dir: Path) -> drex.Study:
15 """Build a study with one condition per contestant strategy."""
16 return drex.Study(
17 study_id="monty-hall-simulation",
18 title="Monty Hall Simulation",
19 description=(
20 "Compare stay versus switch by simulating random Monty Hall games "
21 "inside each study condition."
22 ),
23 factors=(
24 drex.Factor(
25 name="strategy",
26 description="Contestant decision after the host reveals a goat door.",
27 kind=drex.FactorKind.MANIPULATED,
28 levels=(
29 drex.Level(name="stay", value="stay"),
30 drex.Level(name="switch", value="switch"),
31 ),
32 ),
33 ),
34 hypotheses=(
35 drex.Hypothesis(
36 hypothesis_id="h1",
37 label="Switching improves win rate",
38 statement="Switching wins more often than staying in the Monty Hall game.",
39 independent_vars=("strategy",),
40 dependent_vars=("win_rate",),
41 linked_analysis_plan_id="ap1",
42 ),
43 ),
44 outcomes=(
45 drex.OutcomeSpec(
46 name="win_rate",
47 source_table="runs",
48 column="won",
49 aggregation="mean",
50 primary=True,
51 description="Share of scored conditions that end with the prize.",
52 ),
53 ),
54 analysis_plans=(
55 drex.AnalysisPlan(
56 analysis_plan_id="ap1",
57 hypothesis_ids=("h1",),
58 tests=("simulation_summary",),
59 outcomes=("win_rate",),
60 ),
61 ),
62 design_spec={"kind": "full_factorial", "randomize": False},
63 seed_policy=drex.SeedPolicy(base_seed=SIMULATION_SEED),
64 output_dir=output_dir,
65 problem_ids=("monty-hall-game",),
66 primary_outcomes=("win_rate",),
67 )
68
69
70def reveal_goat_door(*, prize_door: str, initial_choice: str, rng: random.Random) -> str:
71 """Randomly reveal one admissible goat door."""
72 goat_doors = [door for door in DOORS if door != prize_door and door != initial_choice]
73 if not goat_doors:
74 raise RuntimeError("Expected at least one goat door to reveal.")
75 return str(rng.choice(goat_doors))
76
77
78def resolve_final_choice(*, initial_choice: str, revealed_door: str, strategy: str) -> str:
79 """Return the contestant's final door after staying or switching."""
80 if strategy == "stay":
81 return initial_choice
82
83 for door in DOORS:
84 if door != initial_choice and door != revealed_door:
85 return door
86 raise RuntimeError("Expected exactly one switch target.")
87
88
89def simulate_condition(condition: drex.Condition, *, n_games: int, seed: int) -> dict[str, object]:
90 """Simulate one strategy condition over many random Monty Hall games."""
91 assignments = condition.factor_assignments
92 strategy = str(assignments["strategy"])
93 rng = random.Random(seed)
94 wins = 0
95
96 for _ in range(n_games):
97 prize_door = str(rng.choice(DOORS))
98 initial_choice = str(rng.choice(DOORS))
99 revealed_door = reveal_goat_door(
100 prize_door=prize_door,
101 initial_choice=initial_choice,
102 rng=rng,
103 )
104 final_choice = resolve_final_choice(
105 initial_choice=initial_choice,
106 revealed_door=revealed_door,
107 strategy=strategy,
108 )
109 wins += int(final_choice == prize_door)
110
111 return {
112 "condition_id": condition.condition_id,
113 "strategy": strategy,
114 "seed": seed,
115 "games": n_games,
116 "wins": wins,
117 "win_rate": round(wins / n_games, 2),
118 }
119
120
121def write_summary(path: Path, rows: list[dict[str, object]]) -> None:
122 """Write the per-condition simulation summary as a CSV artifact."""
123 if not rows:
124 raise RuntimeError("Expected scored rows before writing artifacts.")
125
126 path.parent.mkdir(parents=True, exist_ok=True)
127 with path.open("w", encoding="utf-8", newline="") as file_obj:
128 writer = csv.DictWriter(file_obj, fieldnames=list(rows[0]))
129 writer.writeheader()
130 writer.writerows(rows)
131
132
133def lookup_strategy(rows: list[dict[str, object]], *, strategy: str) -> dict[str, object]:
134 """Return the summary row for one strategy."""
135 for row in rows:
136 if row["strategy"] == strategy:
137 return row
138 raise RuntimeError(f"Missing strategy summary for {strategy!r}.")
139
140
141def main() -> None:
142 """Simulate random Monty Hall games for each strategy condition."""
143 output_dir = Path("artifacts") / "monty-hall"
144 study = build_monty_hall_study(output_dir)
145
146 errors = drex.validate_study(study)
147 if errors:
148 raise RuntimeError("\n".join(errors))
149
150 conditions = drex.build_design(study)
151 rows = [
152 simulate_condition(
153 condition,
154 n_games=SIMULATED_GAMES,
155 seed=study.seed_policy.base_seed,
156 )
157 for condition in conditions
158 ]
159
160 csv_path = output_dir / "simulation_summary.csv"
161 write_summary(csv_path, rows)
162
163 stay = lookup_strategy(rows, strategy="stay")
164 switch = lookup_strategy(rows, strategy="switch")
165
166 if float(switch["win_rate"]) <= float(stay["win_rate"]):
167 raise RuntimeError("Switching should strictly outperform staying in Monty Hall.")
168
169 print(f"Materialized {len(conditions)} conditions")
170 print(
171 f"Simulated {SIMULATED_GAMES} games per condition with seed {study.seed_policy.base_seed}"
172 )
173 print(f"stay wins {stay['wins']}/{stay['games']} = {stay['win_rate']:.2f}")
174 print(f"switch wins {switch['wins']}/{switch['games']} = {switch['win_rate']:.2f}")
175 print(f"Wrote simulation summary CSV to {csv_path}")
176
177
178if __name__ == "__main__":
179 main()
Expected Results
Run Command
PYTHONPATH=src python examples/monty_hall_simulation.py
The script prints 2 materialized conditions, simulates 100 games per condition
with a fixed seed, reports stay winning 35/100 and switch winning
65/100, and writes a summary CSV artifact under artifacts/monty-hall.