CoolFace
Apppublic

marimo-team/marimo-learn

sourceHugging Faceupdated 5mo agoView on Hugging Face
3likes
08_inspectors_paradox.py294 linesDownload Raw Back to queueing
1# /// script2# requires-python = ">=3.13"3# dependencies = [4#     "altair",5#     "asimpy",6#     "marimo",7#     "polars==1.24.0",8# ]9# ///10 11import marimo12 13__generated_with = "0.20.4"14app = marimo.App(width="medium")15 16 17@app.cell(hide_code=True)18def _():19    import marimo as mo20    import random21    import statistics22 23    import altair as alt24    import polars as pl25 26    from asimpy import Environment, Process27 28    return Environment, Process, alt, mo, pl, random, statistics29 30 31@app.cell(hide_code=True)32def _(mo):33    mo.md(r"""34    # The Inspector's Paradox35 36    ## *Why the Bus Is Always Late*37 38    Buses arrive at a stop with some average headway (gap between buses) of $\mu$ minutes. A passenger arrives at a uniformly random time and waits for the next bus. How long do they wait? The naive answer is $\mu / 2$: on average you land in the middle of a gap. The correct answer is almost always longer—sometimes much longer.39 40    The expected wait is not $\mu/2$ but:41 42    $$E[\text{wait}] = \frac{\mu}{2} + \frac{\sigma^2}{2\mu}$$43 44    where $\sigma^2 = \text{Var}[\text{headway}]$. The second term is always non-negative, so higher variance always means longer expected waits, even when the mean headway is unchanged.45 46    ### Three Bus Schedules with Mean Headway $\mu = 10$47 48    | Schedule    | $\sigma^2$ | Predicted wait | Naive wait |49    |-------------|-----------|----------------|-----------|50    | Regular     | 0         | 5.0            | 5.0       |51    | Exponential | 100       | 10.0           | 5.0       |52    | Clustered   | 64        | 8.2            | 5.0       |53 54    For exponentially distributed headways, $\sigma^2 = \mu^2$, so:55 56    $$E[\text{wait}] = \frac{\mu}{2} + \frac{\mu^2}{2\mu} = \mu$$57 58    A passenger waits on average for an *entire* mean headway — twice the naive expectation.59 60    ## Why This Happens: Length-Biased Sampling61 62    A passenger arriving at a random time is more likely to land inside a *long* gap than a short one, because long gaps occupy more time on the clock. This is called *length-biased sampling*. The interval containing your arrival is not a random headway: it is drawn from the length-biased distribution with density:63 64    $$f^*(h) = \frac{h \cdot f(h)}{\mu}$$65 66    The mean of this biased distribution is $\mu + \sigma^2/\mu$, and you arrive uniformly within it, giving expected wait $(\mu + \sigma^2/\mu)/2$.67 68    The same phenomenon explains why the average class size experienced by a student exceeds the average class size reported by the university (large classes have more students to report them).69 70    ## Why "Inspector's Paradox"?71 72    The name comes from quality control, where an inspector arrives at a random time to sample a production process and systematically encounters longer-than-average intervals. The paradox is that a random observer is more likely to land inside a long gap than a short one, so their experienced mean interval exceeds the true mean interval. It feels paradoxical because you'd expect a random arrival to see the average gap, but length-biased sampling guarantees they see worse-than-average gaps whenever there's any variance at all.73    """)74    return75 76 77@app.cell(hide_code=True)78def _(mo):79    mo.md(r"""80    ## Implementation81 82    A `BusService` process generates buses under three headway distributions (regular, exponential, clustered bimodal) and records their arrival times. After the simulation, passenger wait times are estimated by sampling $N$ uniformly random arrival times and finding the next bus for each, without needing explicit `Passenger` processes.83    """)84    return85 86 87@app.cell(hide_code=True)88def _(mo):89    sim_time_slider = mo.ui.slider(90        start=0,91        stop=100_000,92        step=1_000,93        value=20_000,94        label="Simulation time",95    )96 97    mean_headway_slider = mo.ui.slider(98        start=5.0,99        stop=30.0,100        step=1.0,101        value=10.0,102        label="Mean headway",103    )104 105    seed_input = mo.ui.number(106        value=192,107        step=1,108        label="Random seed",109    )110 111    run_button = mo.ui.run_button(label="Run simulation")112 113    mo.vstack([114        sim_time_slider,115        mean_headway_slider,116        seed_input,117        run_button,118    ])119    return mean_headway_slider, seed_input, sim_time_slider120 121 122@app.cell123def _(mean_headway_slider, seed_input, sim_time_slider):124    SIM_TIME = int(sim_time_slider.value)125    MEAN_HEADWAY = float(mean_headway_slider.value)126    SEED = int(seed_input.value)127    N_PASSENGERS = 20_000128    return MEAN_HEADWAY, N_PASSENGERS, SEED, SIM_TIME129 130 131@app.cell132def _(MEAN_HEADWAY, Process, random):133    class BusService(Process):134        def init(self, mode, bus_arrivals):135            self.mode = mode136            self.bus_arrivals = bus_arrivals137 138        async def run(self):139            while True:140                if self.mode == "regular":141                    headway = MEAN_HEADWAY142                elif self.mode == "exponential":143                    headway = random.expovariate(1.0 / MEAN_HEADWAY)144                elif self.mode == "clustered":145                    headway = MEAN_HEADWAY * 0.2 if random.random() < 0.5 else MEAN_HEADWAY * 1.8146                else:147                    raise ValueError(f"Unknown mode: {self.mode}")148                await self.timeout(headway)149                self.bus_arrivals.append(self.now)150 151    return (BusService,)152 153 154@app.cell155def _(BusService, Environment, SIM_TIME):156    def collect_buses(mode):157        bus_arrivals = []158        env = Environment()159        BusService(env, mode, bus_arrivals)160        env.run(until=SIM_TIME)161        return bus_arrivals162 163    return (collect_buses,)164 165 166@app.cell167def _(N_PASSENGERS, random, statistics):168    def expected_wait(bus_arrivals, n=N_PASSENGERS):169        max_t = bus_arrivals[-1]170        waits = []171        for _ in range(n):172            t = random.uniform(0.0, max_t * 0.95)173            for b in bus_arrivals:174                if b > t:175                    waits.append(b - t)176                    break177        return statistics.mean(waits) if waits else 0.0178 179    return (expected_wait,)180 181 182@app.cell183def _(statistics):184    def headway_variance(bus_arrivals):185        headways = [b - a for a, b in zip(bus_arrivals, bus_arrivals[1:])]186        return statistics.variance(headways) if len(headways) > 1 else 0.0187 188    return (headway_variance,)189 190 191@app.cell(hide_code=True)192def _(MEAN_HEADWAY, mo):193    mu = MEAN_HEADWAY194    naive = MEAN_HEADWAY / 2.0195    var_exp = mu ** 2196    var_clustered = 0.5 * (mu * 0.2 - mu) ** 2 + 0.5 * (mu * 1.8 - mu) ** 2197    mo.md(f"""198    ## Results199 200    Mean headway: {MEAN_HEADWAY} → naive expected wait = {naive:.1f}201 202    - **Exponential** (Var ≈ {var_exp:.1f}): predicted = {mu / 2 + var_exp / (2 * mu):.1f} (= full mean headway!)203    - **Clustered** (Var ≈ {var_clustered:.1f}): predicted = {mu / 2 + var_clustered / (2 * mu):.1f}204    """)205    return (naive,)206 207 208@app.cell209def _(MEAN_HEADWAY, collect_buses, expected_wait, headway_variance, naive, pl):210    def run_models():211        rows = []212        for mode in ["regular", "exponential", "clustered"]:213            buses = collect_buses(mode)214            var_h = headway_variance(buses)215            mean_w = expected_wait(buses)216            rows.append({217                "mode": mode,218                "var_headway": round(var_h, 4),219                "mean_wait": round(mean_w, 4),220                "predicted": round(MEAN_HEADWAY / 2.0 + var_h / (2.0 * MEAN_HEADWAY), 4),221                "ratio": round(mean_w / naive, 4),222            })223        return pl.DataFrame(rows)224 225    return (run_models,)226 227 228@app.cell229def _(SEED, random, run_models):230    random.seed(SEED)231    df = run_models()232    df233    return (df,)234 235 236@app.cell237def _(alt, df, naive, pl):238    chart = (239        alt.Chart(df)240        .mark_bar()241        .encode(242            x=alt.X("mode:N", title="Bus schedule type"),243            y=alt.Y("mean_wait:Q", title="Mean passenger wait"),244            color=alt.Color("mode:N", legend=None),245            tooltip=["mode:N", "mean_wait:Q", "ratio:Q"],246        )247        .properties(title="Inspector's Paradox: Mean Wait by Schedule Type")248    )249    naive_line = (250        alt.Chart(pl.DataFrame({"naive": [naive]}))251        .mark_rule(strokeDash=[4, 4], color="gray")252        .encode(y="naive:Q")253    )254    (chart + naive_line)255    return256 257 258@app.cell(hide_code=True)259def _(mo):260    mo.md(r"""261    ## Understanding the Math262 263    ### Length-biased sampling264 265    Suppose buses run on an irregular schedule where gaps between buses are either 2 minutes or 18 minutes, each with probability 1/2. The mean gap is $\mu = (2 + 18)/2 = 10$ minutes. Now ask: if you arrive at a completely random moment, which gap are you most likely to land inside?266 267    A 2-minute gap occupies only 2 minutes on the clock, but an 18-minute gap occupies 18. Out of every 20 minutes of clock time on average, 2 minutes belong to a short gap and 18 to a long one. So a random arrival lands in a short gap with probability $2/(2+18) = 1/10$ and in a long gap with probability $18/20 = 9/10$. The expected gap length you experience is:268 269    $$E[\text{gap experienced}] = \frac{1}{10} \cdot 2 + \frac{9}{10} \cdot 18 = 0.2 + 16.2 = 16.4 \text{ minutes}$$270 271    That is far above the mean gap of 10 minutes. You are disproportionately likely to land inside a long gap simply because it takes up more time.272 273    ### The wait formula274 275    Once you are inside a gap, you arrive uniformly within it, so on average you land in the middle. Your expected wait is half the gap length you experience. The full formula is:276 277    $$E[\text{wait}] = \frac{\mu}{2} + \frac{\sigma^2}{2\mu}$$278 279    Here $\mu$ is the mean gap and $\sigma^2 = \text{Var}[\text{gap}]$ is the variance of gap lengths. The first term, $\mu/2$, is what you would get if every gap were exactly $\mu$ (deterministic buses — arrive in the middle every time). The second term, $\sigma^2/(2\mu)$, is the extra waiting from length-biased sampling. It is always non-negative, so irregular buses always make you wait longer than regular buses with the same mean headway.280 281    ### Why variance matters282 283    The variance $\sigma^2$ measures how spread out the gap sizes are. A perfectly regular bus schedule has $\sigma^2 = 0$ and gives the naive answer $\mu/2$. An exponentially distributed schedule has $\sigma^2 = \mu^2$, which doubles the expected wait to $\mu$. More irregular buses, higher penalty.284 285    ### Connecting to expected values286 287    The formula arises from a standard result: the expected length of the gap containing a random arrival is $\mu + \sigma^2/\mu$. You can think of this as the mean gap plus a correction term proportional to the variance divided by the mean. Dividing by 2 (uniform arrival within the gap) gives the wait formula above.288    """)289    return290 291 292if __name__ == "__main__":293    app.run()294