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

340 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Paradoxe de lAnimal Style
toc: 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 ?
```js
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>
`);
```
```js
const [inNOut, ufoSightings] = await Promise.all([
FileAttachment("./data/in-n-out-ca-csv.json").json(),
FileAttachment("./data/ufo-ca-sightings-v2.json").json(),
]);
```
```js
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);
```
```js
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 };
}
```
```js
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,
}),
);
```
```js
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);
```
```js
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>
`);
```
```js
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);
```
```js
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);
```
```js
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`,
}),
],
}),
);
```
```js
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>
`);
```