Methodology

How the statistical analysis works, in plain language and with technical detail.

What counts as a violation

A violation is recorded when a vehicle is parked in a handicap-designated spot displaying:

This is the strictest, easiest-to-verify criterion. The following are not counted as violations, even though some are arguably illegal:

The criterion is simply: does this vehicle have any disability marker at all? If yes, it is not counted, regardless of any concerns about the placard's specific status. This deliberately undercounts the true rate of misuse — a driver using a borrowed or expired placard would not appear in this dataset — to avoid any judgment-based gray area in the recorded violations.

What is and isn't counted

The Bayesian analysis on this site uses two categories of observation:

1. Visits from April 16, 2025 onward. Every visit logged regardless of outcome. These form the bulk of the dataset.

2. Two prior visits in the week before April 16. Both violations. These were the impetus for this project: after witnessing two consecutive violations, I sought advice and was told to begin systematic documentation. I started tracking on April 16, which independently turned out to be a third violation. I have no photos and no exact dates for the two prior visits, but they were the only two visits I made in that window, and they are counted on that basis. Their entries in the log are flagged with an approximate-date marker and a note explaining the absence of contemporaneous documentation.

What is not counted: occasional photos I took across late 2024 and early 2025 of violations I happened to witness. During that period I did not record non-violation visits, so those photos are subject to selection bias — the implied rate would approach 100% by construction, since I only captured positives. Those entries appear in the log under "Pre-tracking — anecdotal photos" for evidentiary completeness only, and are excluded from the analysis below.

The Setup

Each daycare visit is an independent observation: either an illegal handicap parking violation is present, or it isn't. This is a classic Bernoulli trial, and the collection of all visits follows a Binomial distribution.

The Question

If handicap parking violations were rare — say they occur randomly at 5% of any given moment — how likely is it that I would observe violations on 7 out of 14 visits? And more importantly: given what I've actually observed, what should I believe the true violation rate to be?

Bayesian Analysis

Prior

We start with a Beta(1,1)\mathrm{Beta}(1, 1) prior — the uniform distribution over [0, 1]. This encodes total ignorance: before seeing any data, every possible violation rate from 0% to 100% is considered equally plausible. This is the most conservative choice and gives maximum weight to the data.

Likelihood

The data follows a Binomial likelihood:

P(Dp)=(nk)pk(1p)nkP(D \mid p) = \binom{n}{k}\, p^{k}\, (1-p)^{n-k}

where pp is the unknown true violation rate.

Posterior

Because the Beta distribution is the conjugate prior for the Binomial likelihood, the posterior is available in closed form:

Beta(1+7, 1+147)=Beta(8, 8)\mathrm{Beta}(1 + 7,\ 1 + 14 - 7) = \mathrm{Beta}(8,\ 8)

This is the updated belief about the true violation rate after seeing the data.

Credible Interval

The 95% credible interval is computed from the inverse CDF (quantile function) of the posterior Beta distribution at 0.025 and 0.975. Unlike a frequentist confidence interval, this directly says: "there is a 95% probability the true rate falls in this range."

Bayes Factor

The Bayes factor asks: how well does each explanation predict the data we actually observed? We compare two models. M0M_0 says the violation rate is exactly 5% — violations are rare, random events, and what I've seen is a string of bad luck. M1M_1 makes no assumption about the rate at all — it says "let the data speak" and allows any rate from 0% to 100%. The Bayes factor is the ratio of how probable the observed data is under each model:

BF=P(DM1)P(DM0)\mathrm{BF} = \dfrac{P(D \mid M_1)}{P(D \mid M_0)}

Under M0M_0 the marginal likelihood is simply Binom(k;n,0.05)\mathrm{Binom}(k;\, n,\, 0.05). Under M1M_1 with a uniform prior it simplifies to 1n+1\frac{1}{n+1} via the Beta-Binomial conjugate identity. A Bayes factor of 1 would mean both explanations fit the data equally well. Our Bayes factor is in the tens of thousands — meaning the data is overwhelmingly more consistent with an unknown (and evidently high) rate than with a fixed 5% rate. Note thatM1M_1 doesn't assume the rate is high; it's agnostic. The fact that it wins so decisively is because the data forces it toward a high rate. The posterior distribution above is where the actual estimate lives. Conventionally, a Bayes factor above 100 is considered decisive evidence against the simpler model, M0M_0, in favor of M1M_1.

Prior Sensitivity

The flat Beta(1,1)\mathrm{Beta}(1, 1) prior used in the main analysis is the most conservative choice — it encodes total ignorance, treating every possible violation rate as equally plausible before seeing the data. A natural question is whether the conclusion would change if we started from a skeptical position — say, a prior belief that the rate is probably low.

The table below repeats the analysis under four priors of increasing skepticism, from total ignorance to a moderately confident belief that the rate is around 5%. The general form is Beta(a0, b0)Beta(a0+k, b0+nk)\mathrm{Beta}(a_0,\ b_0) \to \mathrm{Beta}(a_0 + k,\ b_0 + n - k), with Bayes factor B(a0+k, b0+nk)B(a0, b0)p0k(1p0)nk\dfrac{B(a_0 + k,\ b_0 + n - k)}{B(a_0,\ b_0) \cdot p_0^{k}\, (1 - p_0)^{n - k}} against the same p0=0.05p_0 = 0.05 null.

PriorBelief encodedPosteriorMean95% CIP(rate > 5%)Bayes factor
Beta(1, 1)Used in main analysisNo assumptionBeta(8, 8)50.0%27% – 73%100.00%35,605×
Beta(1, 4)Probably lowBeta(8, 11)42.1%22% – 64%100.00%20,944×
Beta(1, 9)~10%, weakly heldBeta(8, 16)33.3%16% – 53%100.00%4,206×
Beta(1, 19)~5%, moderately heldBeta(8, 26)23.5%11% – 39%99.98%314×

The posterior mean shifts as the prior becomes more skeptical — pulled toward the prior's center of mass — but the probability that the true rate exceeds 5% remains overwhelming under every prior. Even a prior carrying more than twice the weight of the data (Beta(1,19)\mathrm{Beta}(1, 19), equivalent to ~20 pseudo-observations of low rates) cannot pull the posterior below the 5% threshold with meaningful probability. The data is strong enough that the starting assumption doesn't matter.

Frequentist Confirmation

As an independent check, a one-sided binomial exact test is also computed. The p-value is P(Xkn, p0=0.05)P(X \geq k \mid n,\ p_0 = 0.05) — the probability of observing at least as many violations as recorded, assuming the null hypothesis that the true rate is 5%.

With k=7k = 7 violations in n=14n = 14 visits, the p-value is 1.96×1061.96 \times 10^{-6} — about 1 in 509,748. The conventional threshold for statistical significance is 0.05 (5%). This result falls roughly 10,000× below that threshold. Both the Bayesian and frequentist approaches reach the same conclusion.

Assumptions & Limitations

Independence: Each visit is treated as independent. This is reasonable since visits are spaced days or weeks apart.

Sampling: Visits are not scheduled around parking lot conditions — they are driven by childcare logistics. There is no selection bias toward or against violation times.

Binary outcome: Each visit is coded as violation/no-violation. When multiple vehicles are illegally parked (as has occurred), it still counts as one violation event. The true severity may be underestimated.

Baseline rate: The 5% null rate is a deliberately conservative choice — not grounded in published data, but set roughly five times higher than my twenty-year personal intuition (closer to ~1%) for the rate at a typical compliant facility. The reasoning: if the data overwhelmingly rejects the noise hypothesis even at this generous threshold, the conclusion is more defensible than it would be at a tighter, easier-to-contest null. A reader who would set the bar lower (1%) gets a stronger result; one who would set it higher (10–20%) gets a weaker but still overwhelming one.

Observer coverage: I attend only a fraction of pickup/dropoff events at this facility. Violations during events I'm not present for are not captured, meaning the documented incidents are almost certainly an undercount.

Documentary coverage: Some counted visits include contemporaneous photos of the violation; others do not — capturing a photo isn't always possible during pickup/dropoff, with hands full and vehicles departing. Entries without a photo are not weighted differently in the analysis; a recorded observation counts the same regardless of how it was documented. Where a photo of the violation isn't available, the entry's notes describe what was observed and any corroborating context.

Recall on the prior 2 entries: The two visits in the week before April 16 are counted based on personal recollection rather than contemporaneous records. Their dates are flagged as approximate. A skeptical reader can verify the conclusion is robust to their exclusion: with only the post-April-16 entries, the Bayes factor against the 5% null remains overwhelming.

Source Code

All analysis code runs client-side with no external dependencies. You can verify the implementation directly: the load-bearing math — posterior parameters, the credible interval, the Bayes factor under each prior, the frequentist p-value — lives in bayes.ts and is reproduced verbatim below. The same file is bundled into the JavaScript that renders the chart and the prior-sensitivity table on this site, so the code shown here is the code that runs.

View bayes.ts (analysis source)
// Bayesian Beta-Binomial helpers shared between server-rendered pages
// (e.g. the verdict banner) and the React analysis component. Keeping a
// single source of truth prevents the banner from disagreeing with the
// chart when the data changes.

export function logGamma(z: number): number {
  const c = [
    76.18009172947146,
    -86.50532032941677,
    24.01409824083091,
    -1.231739572450155,
    0.1208650973866179e-2,
    -0.5395239384953e-5,
  ];
  let x = z;
  let y = z;
  let tmp = x + 5.5;
  tmp -= (x + 0.5) * Math.log(tmp);
  let ser = 1.000000000190015;
  for (let j = 0; j < 6; j++) ser += c[j] / ++y;
  return -tmp + Math.log((2.5066282746310005 * ser) / x);
}

export function logBinom(n: number, k: number): number {
  if (k < 0 || k > n) return -Infinity;
  if (k === 0 || k === n) return 0;
  let s = 0;
  for (let i = 0; i < k; i++) s += Math.log(n - i) - Math.log(i + 1);
  return s;
}

export function incompleteBeta(x: number, a: number, b: number): number {
  if (x <= 0) return 0;
  if (x >= 1) return 1;
  const lnB = logGamma(a) + logGamma(b) - logGamma(a + b);
  const front = Math.exp(a * Math.log(x) + b * Math.log(1 - x) - lnB) / a;
  let sum = 0;
  let term = 1;
  for (let i = 0; i < 600; i++) {
    term *= (x * (a + b + i)) / (a + 1 + i);
    sum += term;
    if (Math.abs(term) < 1e-15) break;
  }
  return front * (1 + sum);
}

export function betaQuantile(a: number, b: number, p: number): number {
  let lo = 0;
  let hi = 1;
  for (let i = 0; i < 300; i++) {
    const mid = (lo + hi) / 2;
    if (incompleteBeta(mid, a, b) < p) lo = mid;
    else hi = mid;
  }
  return (lo + hi) / 2;
}

export interface PosteriorStats {
  n: number;
  k: number;
  aPost: number;
  bPost: number;
  mean: number;
  ciLo: number;
  ciHi: number;
  probAbove: number;
  bayesFactor: number;
  pValue: number;
}

export function posteriorStats(
  n: number,
  k: number,
  p0: number = 0.05,
): PosteriorStats {
  const aPost = 1 + k;
  const bPost = 1 + n - k;
  const mean = aPost / (aPost + bPost);
  const ciLo = betaQuantile(aPost, bPost, 0.025);
  const ciHi = betaQuantile(aPost, bPost, 0.975);
  const probAbove = 1 - incompleteBeta(p0, aPost, bPost);

  const likH0 = Math.exp(
    logBinom(n, k) + k * Math.log(p0) + (n - k) * Math.log(1 - p0),
  );
  const margH1 = 1 / (n + 1);
  const bayesFactor = margH1 / likH0;

  let pValue = 0;
  for (let i = k; i <= n; i++) {
    pValue += Math.exp(
      logBinom(n, i) + i * Math.log(p0) + (n - i) * Math.log(1 - p0),
    );
  }

  return { n, k, aPost, bPost, mean, ciLo, ciHi, probAbove, bayesFactor, pValue };
}

export interface PriorPosteriorStats {
  a0: number;
  b0: number;
  aPost: number;
  bPost: number;
  mean: number;
  ciLo: number;
  ciHi: number;
  probAbove: number;
  bayesFactor: number;
}

// Posterior + Bayes factor against M0 (rate = p0) under an arbitrary
// Beta(a0, b0) prior. The C(n,k) factor cancels between M1 and M0 in
// the BF ratio, so it is omitted from both — all computation in log
// space to avoid underflow on small p0^k terms.
export function posteriorStatsForPrior(
  n: number,
  k: number,
  a0: number,
  b0: number,
  p0: number = 0.05,
): PriorPosteriorStats {
  const aPost = a0 + k;
  const bPost = b0 + n - k;
  const mean = aPost / (aPost + bPost);
  const ciLo = betaQuantile(aPost, bPost, 0.025);
  const ciHi = betaQuantile(aPost, bPost, 0.975);
  const probAbove = 1 - incompleteBeta(p0, aPost, bPost);

  const logBetaPost =
    logGamma(aPost) + logGamma(bPost) - logGamma(aPost + bPost);
  const logBetaPrior = logGamma(a0) + logGamma(b0) - logGamma(a0 + b0);
  const logMargM1 = logBetaPost - logBetaPrior;
  const logLikM0 = k * Math.log(p0) + (n - k) * Math.log(1 - p0);
  const bayesFactor = Math.exp(logMargM1 - logLikM0);

  return { a0, b0, aPost, bPost, mean, ciLo, ciHi, probAbove, bayesFactor };
}