Files
observable/docs/ufo-animal-style-paradox.md

8.6 KiB
Raw Blame History

title, toc
title toc
Paradoxe de lAnimal Style false

🛸 Le Paradoxe de lAnimal Style

Question absurde mais testable : Les OVNIs en Californie survolent-ils plus souvent des zones proches des In-N-Out ?

display(html`
  <style>
    .alien-board {
      background: #0b0e14;
      border: 1px solid #1c2333;
      border-radius: 12px;
      padding: 1rem;
      color: #d8e1ff;
    }
    .alien-kpi {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 0.75rem;
      margin-top: 0.75rem;
    }
    .alien-kpi .card {
      background: #111726;
      border: 1px solid #1d2940;
      border-radius: 10px;
      padding: 0.7rem;
      color: #d8e1ff;
    }
    .alien-kpi .value {
      font-size: 1.25rem;
      font-weight: 700;
      color: #39ff14;
    }
    @media (max-width: 960px) {
      .alien-kpi {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }
  </style>
`);
const [inNOut, ufoSightings] = await Promise.all([
  FileAttachment("./data/in-n-out-ca-csv.json").json(),
  FileAttachment("./data/ufo-ca-sightings-v2.json").json(),
]);
const [d3, topojson] = await Promise.all([
  import("https://cdn.jsdelivr.net/npm/d3@7/+esm"),
  import("https://cdn.jsdelivr.net/npm/topojson-client@3/+esm"),
]);

const usAtlasUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json";
const us = await fetch(usAtlasUrl).then((r) => r.json());
const states = topojson.feature(us, us.objects.states);
const california = states.features.find((s) => Number(s.id) === 6);
function haversineKm(lat1, lon1, lat2, lon2) {
  const toRad = (deg) => (deg * Math.PI) / 180;
  const R = 6371;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(a));
}

function nearestStore(ufo, stores) {
  let bestStore = null;
  let bestDistance = Infinity;

  for (const store of stores) {
    const distance = haversineKm(
      ufo.latitude,
      ufo.longitude,
      store.latitude,
      store.longitude,
    );
    if (distance < bestDistance) {
      bestDistance = distance;
      bestStore = store;
    }
  }

  return { store: bestStore, distanceKm: bestDistance };
}
const shapes = [
  "all",
  ...d3
    .sort(Array.from(new Set(ufoSightings.map((d) => d.shape || "unknown"))))
    .slice(0, 20),
];

const selectedShape = view(
  Inputs.select(shapes, {
    label: "Filtre de forme OVNI",
    value: "all",
    format: (d) => (d === "all" ? "Toutes les formes" : d),
  }),
);

const hungerRadiusKm = view(
  Inputs.range([5, 80], {
    label: "Curseur de fringale extraterrestre (km)",
    value: 24,
    step: 1,
  }),
);

const hexbinWidth = view(
  Inputs.range([8, 28], {
    label: "Largeur des hexagones radar",
    value: 14,
    step: 1,
  }),
);
const filteredUfo = ufoSightings.filter((ufo) =>
  selectedShape === "all" ? true : ufo.shape === selectedShape,
);

const enrichedUfo = filteredUfo.map((ufo) => {
  const nearest = nearestStore(ufo, inNOut);
  return {
    ...ufo,
    nearestStoreId: nearest.store?.id ?? null,
    nearestStoreLat: nearest.store?.latitude ?? null,
    nearestStoreLon: nearest.store?.longitude ?? null,
    nearestDistanceKm: nearest.distanceKm,
    closeToBurger: nearest.distanceKm <= hungerRadiusKm,
  };
});

const hungerRate =
  enrichedUfo.length > 0
    ? (enrichedUfo.filter((d) => d.closeToBurger).length / enrichedUfo.length) *
      100
    : 0;

const byStore = d3.rollups(
  enrichedUfo,
  (rows) => ({
    sightings: rows.length,
    avgDuration: d3.mean(rows, (d) => d.durationSeconds) ?? 0,
    medDistance: d3.median(rows, (d) => d.nearestDistanceKm) ?? 0,
  }),
  (d) => d.nearestStoreId,
);

const storeHotspots = byStore
  .map(([storeId, stats]) => {
    const store = inNOut.find((d) => d.id === storeId);
    if (!store) return null;
    return {
      ...store,
      sightings: stats.sightings,
      avgDuration: stats.avgDuration,
      medDistance: stats.medDistance,
    };
  })
  .filter(Boolean)
  .sort((a, b) => b.sightings - a.sightings);

const topHotspots = storeHotspots.slice(0, 12);
display(html`
  <div class="alien-board">
    <div style="font-size:1.05rem;line-height:1.4;">
      <strong>Dossier classifié:</strong> “Les zones de survol OVNI sont-elles
      alignées avec les territoires In-N-Out ?”
    </div>
    <div class="alien-kpi">
      <div class="card">
        <div>Apparitions OVNI (filtre actif)</div>
        <div class="value">${enrichedUfo.length.toLocaleString("fr-FR")}</div>
      </div>
      <div class="card">
        <div>In-N-Out en Californie</div>
        <div class="value">${inNOut.length.toLocaleString("fr-FR")}</div>
      </div>
      <div class="card">
        <div>Taux de faim extraterrestre</div>
        <div class="value">${hungerRate.toFixed(1)}%</div>
      </div>
      <div class="card">
        <div>Rayon de corrélation</div>
        <div class="value">${hungerRadiusKm} km</div>
      </div>
    </div>
  </div>
`);
const map = Plot.plot({
  width,
  height: 700,
  projection: { type: "albers", domain: california },
  style: { background: "#0f0f0f", color: "#d8e1ff" },
  color: {
    label: "Proximité burger",
    domain: ["≤ rayon", "> rayon"],
    range: ["#39FF14", "#00F3FF"],
  },
  marks: [
    Plot.geo(california, {
      fill: "#0f0f0f",
      stroke: "#303847",
      strokeWidth: 0.8,
    }),
    Plot.dot(enrichedUfo, {
      x: "longitude",
      y: "latitude",
      r: (d) => (d.closeToBurger ? 2.4 : 1.8),
      fill: (d) => (d.closeToBurger ? "≤ rayon" : "> rayon"),
      fillOpacity: 0.38,
      stroke: "#0f0f0f",
      strokeOpacity: 0.15,
      tip: true,
      title: (d) =>
        `OVNI — ${d.city}\nForme: ${d.shape}\nDistance In-N-Out: ${d.nearestDistanceKm.toFixed(2)} km`,
    }),
    Plot.dot(inNOut, {
      x: "longitude",
      y: "latitude",
      fill: "#E31837",
      stroke: "#FFFFFF",
      strokeOpacity: 0.45,
      strokeWidth: 0.5,
      symbol: "star",
      r: 2.8,
      tip: true,
      title: (d) =>
        `${d.name}\nLat: ${d.latitude.toFixed(3)}\nLon: ${d.longitude.toFixed(3)}`,
    }),
  ],
});

display(map);
const scatter = Plot.plot({
  width,
  height: 420,
  style: { background: "#0f0f0f", color: "#d8e1ff" },
  x: { label: "Distance au In-N-Out le plus proche (km)", grid: true },
  y: { label: "Durée observation OVNI (secondes)", type: "log", grid: true },
  color: {
    legend: true,
    range: ["#00F3FF", "#39FF14"],
  },
  marks: [
    Plot.ruleX([hungerRadiusKm], { stroke: "#FFCC00", strokeDasharray: "4,4" }),
    Plot.dot(enrichedUfo, {
      x: "nearestDistanceKm",
      y: (d) => Math.max(1, d.durationSeconds),
      fill: (d) => (d.closeToBurger ? "≤ rayon" : "> rayon"),
      r: 2.4,
      fillOpacity: 0.65,
      tip: true,
      title: (d) =>
        `Ville: ${d.city}\nForme: ${d.shape}\nDistance In-N-Out: ${d.nearestDistanceKm.toFixed(2)} km\nDurée: ${d.durationSeconds}s`,
    }),
  ],
});

display(scatter);
display(
  Plot.plot({
    width,
    height: 420,
    marginLeft: 260,
    style: { background: "#0f0f0f", color: "#d8e1ff" },
    x: { label: "Apparitions OVNI (cellule de Voronoï)", grid: true },
    y: { label: null },
    marks: [
      Plot.barX(topHotspots, {
        x: "sightings",
        y: (d) =>
          `${d.name} (${d.latitude.toFixed(2)}, ${d.longitude.toFixed(2)})`,
        sort: { y: "x", reverse: true },
        fill: "#E31837",
        fillOpacity: 0.85,
        tip: true,
        title: (d) =>
          `In-N-Out hotspot\nApparitions OVNI: ${d.sightings}\nDistance médiane: ${d.medDistance.toFixed(2)} km\nDurée moyenne: ${Math.round(d.avgDuration)} s`,
      }),
    ],
  }),
);
const conclusion =
  hungerRate >= 75
    ? "👽 Corrélation CRITIQUE : les aliens semblent avoir le menu secret."
    : hungerRate >= 50
      ? "🛸 Corrélation ÉLEVÉE : suspicion de fascination pour le Double-Double."
      : hungerRate >= 30
        ? "🛰️ Corrélation MODÉRÉE : possible coïncidence spatio-burger."
        : "🔬 Corrélation FAIBLE : les aliens préfèrent peut-être les tacos.";

display(html`
  <div
    class="card"
    style="background:#111726;color:#d8e1ff;border:1px solid #1d2940;"
  >
    <h3 style="margin-top:0;">Conclusion du laboratoire absurde</h3>
    <p>
      <strong>${hungerRate.toFixed(1)}%</strong> des apparitions OVNI en
      Californie se produisent à moins de
      <strong>${hungerRadiusKm} km</strong> dun In-N-Out.
    </p>
    <p style="margin-bottom:0;">${conclusion}</p>
  </div>
`);