340 lines
8.6 KiB
Markdown
340 lines
8.6 KiB
Markdown
---
|
||
title: Paradoxe de l’Animal Style
|
||
toc: false
|
||
---
|
||
|
||
# 🛸 Le Paradoxe de l’Animal 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> d’un In-N-Out.
|
||
</p>
|
||
<p style="margin-bottom:0;">${conclusion}</p>
|
||
</div>
|
||
`);
|
||
```
|