Hoe de score wordt berekend
Een precieze, licht nerdy uitleg van elke invoer, formule, drempelwaarde en realitycheck die de 0–10 van vanavond produceert. Voor mensen die de bonnetjes willen zien.
De vorm ervan
De score is één getal van 0 tot 10 dat één vraag beantwoordt: "is de hemel vanavond de moeite waard om naar buiten te gaan?" Twee fysieke realiteiten tellen voor sterren-kijken met het blote oog — wolken die het zicht blokkeren, en een felle maan die de zwakkere sterren wegspoelt. Alles wat verder telt is daar afgeleid van.
We berekenen de score in zes kleine stappen op basis van een weersvoorspelling, en doen daarna twee realitychecks: één tegen meerdere onafhankelijke modellen, één tegen daadwerkelijke vliegveldwaarnemingen na zonsondergang. Een aparte gebruikersfeedback-loop kalibreert regionale afwijkingen op de lange termijn.
1. Wolkenvoorspelling (Open-Meteo)
We halen een uurlijkse 8-daagse voorspelling op bij Open-Meteo voor jouw exacte lat/lng. Belangrijk: we vragen de bewolking opgesplitst per hoogte op, niet alleen het samengevatte "totaal" dat de meeste weer-apps tonen.
Elke band is een percentage in [0, 100] en ze overlappen — een hemel met 60% lage wolken en 40% hoge cirrus rapporteert als beide, niet als 100%. Door de split kunnen we hoogtes anders wegen in stap 2.
// Open-Meteo, per location, every hour: cloud_cover_low // 0–100% (< 6500 ft / ~2 km AGL) cloud_cover_mid // 0–100% (6500–20000 ft / 2–6 km) cloud_cover_high // 0–100% (> 20000 ft / 6+ km, mostly cirrus)
2. Laag-weging (laag > midden > hoog)
Voor sterren-kijken met het blote oog zijn lage wolken de vijand. Stratus en stratocumulus blokkeren de hele hemel erachter. Middenwolken (altostratus, altocumulus) dimmen de meeste sterren. Hoge dunne cirrus daarentegen vervaagt slechts licht — je ziet Orion er nog doorheen.
weightedCloud =
cloud_low × 0.5 // blocks everything
+ cloud_mid × 0.3 // blocks most stars
+ cloud_high × 0.2 // thin cirrus dims, doesn't block
solidDeck = min( cloud_total, max(cloud_low, cloud_mid) )
effectiveCloud = max( weightedCloud, solidDeck )
// 100% low stratus counts as 100%, not 50%Dus wegen we op hoogte voordat we middelen. Een hemel vol cirrus en een hemel vol stratus geven heel andere kijkervaring; het ongewogen "totale bewolking"-getal dat de meeste apps gebruiken kan dat niet onderscheiden. Eén correctie erbovenop (september 2026): weging alleen maakte van een gesloten lage wolkenlaag "50% bewolkt", dus de effectieve bewolking is nooit lager dan de dikste lage of middenlaag. Een volledig bewolkte nacht leest nu ook als bewolkt.
3. Tijds-weging (piekuren > late nacht)
Sterrenkijkers beslissen op wat ze om 22:00 zullen zien, niet om 03:30. Dus wanneer we de nacht middelen, wegen we de "piekuren" (21:00 → 00:59 lokaal) 2× zwaarder dan de late-nacht-staart. Een heldere 04:00 kan een bewolkte 22:00 niet meer omhoogtrekken met een halve punt.
darkWindow = sun below −18° (astronomical dusk → dawn)
else sun below −12°, labelled "nautical" — score capped at 7.5
else none: "No true darkness tonight" — score capped at 2
headlineCloud = Σ ( effectiveCloud(h) × w(h) ) / Σ w(h)
w(h) = darkMinutes(h) / 60 // share of the hour inside darkWindow
× 2 if 21:00 ≤ h < 01:00 // prime hours
× 1 otherwise // late-night tailDe uren die we middelen vormen het astronomische-duisternis-venster — de zon minstens 18° onder de horizon, berekend met astronomy-engine voor jouw exacte locatie en tijdzone. Uren die maar deels donker zijn tellen alleen voor het donkere deel. Waar de zon nooit −18° haalt (bijv. Amsterdam eind juni) vallen we terug op nautische schemering (−12°), zeggen we dat op de pagina en begrenzen we de score op 7,5: een heldere nautische nacht kan nog goed zijn voor planeten en heldere sterren, maar nooit geweldig, en de pagina zegt dat de Melkweg niet te zien is. Waar zelfs −12° niet gehaald wordt (Tromsø in juni) tonen we "Vannacht wordt het niet echt donker" en begrenzen we op 2. Het bewolkingspercentage op de pagina is precies dit gemiddelde over precies dit venster.
4. Maan-penalty (verlichting × echte hoogte)
Een felle maan maakt de hemel niet donkerder — maar overstemt wel alles behalve de helderste sterren. We passen een penalty toe evenredig met verlichting, in drie niveaus:
- 0% penalty als verlichting < 25% (nieuwe maan en sikkel).
- verlichting × 0,5 bij 25–75% (rond eerste/laatste kwartier).
- verlichting × 1,0 boven 75% (afnemend gibbous en vol).
Daarna kijken we waar de maan echt staat. Voor elk donker uur berekenen we haar hoogte met astronomy-engine: onder de horizon kost ze niets, laag aan de horizon een beetje, en vanaf 30° de volle trede. De penalty voor de nacht is het gemiddelde over dezelfde gewogen donkere uren als de bewolking. Een halve maan die om 23:30 ondergaat drukt dus alleen het eerste deel van de nacht. (Tot september 2026 gebruikten we een vuistregel — "na middernacht gaat een felle maan waarschijnlijk onder" — die ook een nacht kon bestraffen waar de maan al weg was.)
penalty(h) = tier(illumination) × altitudeFactor(moonAltitude at h)
altitudeFactor = 0 if the Moon is below the horizon
= min(1, sin(alt) / sin 30°) otherwise5. Zicht-gate
Zelfs met een perfecte nieuwe-maan-hemel mag een bewolkte nacht niet boven nul scoren. De zicht-helling forceert dat: van volledig bewolkt (helderheid 0%) tot 30% helder schaalt de score lineair naar volle waarde. Onder 30% helderheid wordt zelfs een perfecte donkere hemel met minder dan 1 vermenigvuldigd.
clearness = 100 − headlineCloud visibility = min(1, clearness / 30) // clearness 0–30% → visibility ramps 0 → 1 // clearness 30%+ → visibility = 1
Zonder deze gate scoorde een 100%-bewolkte nacht met nieuwe maan ~4/10 alleen al uit de maan-donkerheid-term — fysiek onmogelijk. Nu scoort dat correct 0,0.
6. De koplijn-formule
moonPenalty = Σ ( penalty(h) × w(h) ) / Σ w(h) // same weights as cloud
darkness = 100 − moonPenalty
score = visibility × ( clearness × 0.6 + darkness × 0.4 ) / 10
// clamped to 0–10, rounded to 1 decimal
verdict: < 3 terrible · < 5 poor · < 7 decent
≥ 7 good · ≥ 8 great · ≥ 9 exceptional // alerts default to 7Dat is de koplijn — het ene getal, identiek op de stadspagina, in alert-mails, op deelafbeeldingen en in embeds. Bewolking krijgt 60% van het gewicht, maan-donkerte 40%, en de zicht-gate vetoot het geheel als er te veel wolken zijn. Dezelfde formule scoort elk uur van de tijdlijn, de beste twee opeenvolgende donkere uren ("Beste 2 uur") en het avondblok 21:00–01:00 — maar die twee zijn secundair, liggen altijd binnen het donkere venster en vervangen de koplijn nooit. Elk oordeel, emoji en label komt alleen uit de koplijn: onder de 7 heet het nooit een goede nacht.
Twee realitychecks erbovenop
Eén enkele voorspelling kan met overtuiging fout zitten. Twee lagen boven de koplijn vangen dat op.
Geen van de checks tilt de score omhoog. Ze vlaggen alleen onzekerheid (multi-model) of trekken de score richting waargenomen werkelijkheid (nowcast).
Realitycheck 1: meningsverschil tussen modellen (met voorzichtige blend)
Open-Meteo geeft graag dezelfde voorspelling apart uit drie verschillende globale weermodellen. Wij vragen alle drie en vergelijken wat ze zeggen over het volledige donkere venster — elk uur tussen astronomische schemering en dageraad, niet alleen de piekuren in de vroege avond. Alert-mails gaan door precies dezelfde check, dus het getal in je inbox is het getal op de site.
// Three independent global numerical weather models:
// ECMWF IFS (European Centre, broadly best in Europe)
// GFS (NOAA, best US coverage)
// ICON-EU (Deutscher Wetterdienst regional)
nightSpread = max pairwise | model_i − model_j |
averaged over the full darkness window
(≈ 21:00 → 04:00, varies by season + latitude)
if nightSpread > 25:
pessimistic = max( model_avg ) // cloudiest model
delta = max(0, pessimistic − seamless) // capped at 30 pts
if delta ≥ 10:
// Bias the headline toward the cloudier model.
weightedCloud += delta × 0.4 // assume mid/low layer
score recomputed — but capped at unblended − 1.5
confidence = { level: "low", pessimisticBlend: true }
// badge: "Cautious estimate"
else:
confidence = { level: "low", pessimisticBlend: false }
// badge: "Low confidence" — warning onlyVerschillen ze meer dan 25 procentpunt over het volledige duisternis-venster (meestal ~21:00 tot ~04:00, afhankelijk van seizoen + breedtegraad), dan waarschuwen we niet alleen — we leunen de score richting het bewolktste model. De redenering: een vals-positief ("topavond, ga naar buiten") kost meer vertrouwen dan een vals-negatief ("matige avond, blijf binnen"), dus bij twijfel beloven we minder. De blend is afgekapt op 1,5 score-punt, zodat één afwijkend model je avond niet ruïneert. Als de blend wordt toegepast, verandert het label van "Lage zekerheid" in "Voorzichtige schatting" — zodat je weet dat we al bijgesteld hebben, niet alleen gewaarschuwd. Eerdere versies van deze check keken alleen naar de piekuren (21:00–01:00) — daardoor werden late-night meningsverschillen gemist, dus is het venster verbreed in mei 2026.
Realitycheck 2: nowcast-clamp na zonsondergang
Zodra de zon onder is, is "vanavond" geen voorspelling meer — het wordt waarneembare werkelijkheid. We halen de meest recente METAR (verplichte uurlijkse waarneming van elk commercieel vliegveld) binnen ~65 km van jou, en parseren de FEW/SCT/BKN/OVC laagcodes naar dezelfde laag/midden/hoog split die we intern gebruiken.
Als de waargenomen gewogen bewolking flink hoger is dan wat de voorspelling middelde voor vanavond, verlagen we de koplijn-score. De drempels zijn afhankelijk van de bron: METAR is echte meting dus vertrouwen we kleine afwijkingen; Open-Meteo current is model + stationmix (hetzelfde model dat de voorspelling produceerde), dus daar houden we een conservatievere drempel om model-fout niet dubbel te tellen.
// Post-sunset only.
// Source priority:
// 1. METAR via aviationweather.gov bbox (~65 km, 130 km fallback)
// 2. Open-Meteo current (model + station blend, last resort)
// Trigger thresholds depend on source trust:
// METAR: Δ > 15 pts AND observed > 35%
// Open-Meteo cur.: Δ > 30 pts AND observed > 50%
// If triggered:
score = min( forecastScore, observationScore )
// never raised — only loweredCruciaal: de clamp verlaagt alleen, nooit verhogen. Als de werkelijkheid er beter uitziet dan de voorspelling tillen we niet op — de voorspelling had haar kans, en overbeloven schaadt vertrouwen meer dan onderbeloven.
Kalibratie: elke alert-mail is een leerlus
Modellen drijven. Specifieke steden hebben specifieke bugs (zeebries-nachten in Amsterdam, late inversies in Salt Lake, marine layer in kust-Californië). De enige manier om de lokale afwijking van een model te leren is voorspelling vs. werkelijkheid vergelijken.
Dus elke alert-mail bevat drie knoppen: ✨ Helder, 🌤 Deels, ☁️ Bewolkt. Elk is een HMAC-getekende link met de hele voorspellingscontext — geen login, geen DB-lookup, één tik. We slaan de klik op in starsout_feedback.
// Each alert email contains 3 buttons. Each button is an
// HMAC-SHA256 signed link carrying the entire prediction context:
{
v: 1,
sub: <subscriber_id>,
d: "2026-05-07",
s: 8.2, // predicted score
c: 18, // predicted cloud cover %
lat, lng,
conf: "high",
src: "metar",
city, locale,
exp: <unix epoch + 7 days>,
o: "clear" | "partly" | "cloudy"
}
// Token is stand-alone: no DB lookup needed to validate.
// One click → upsert into starsout_feedback.
// Per-city rollup (view starsout_feedback_calibration):
hit_rate(city, month) =
100 × clear_or_partly_alerts_with_score≥7 / total_alerts_with_score≥7Een wekelijkse job rolt feedback per stad per maand op. Bij ≥4 weken feedback per stad hebben we genoeg signaal om per-stad bias-correctie live in de score toe te passen. Tot dan publiceren we alleen de kalibratie-view intern en houden we systemische missers in de gaten.
Wat niet in de score zit (en waarom)
- Lichtvervuiling (Bortle-klasse): bepaalt de "Vanavond zichtbaar"-deep-sky-lijst — welke sterrenstelsels en nevels haalbaar zijn vanaf jouw plek — maar verschuift de koplijn niet. Het getal is "werkt de hemel mee?", niet "is jouw hemel ongerept?".
- Wind, vochtigheid, neerslag: zichtbaar in de tijdlijn als je scrollt, maar veranderen de score niet. Ze beïnvloeden comfort en seeing, niet of de hemel ondoorzichtig is voor sterlicht.
- Atmosferische seeing (turbulentie): telt voor telescoopgebruikers (planetair detail, dubbelster-splits) maar niet voor blote-oog-kijken. Vage proxies uit oppervlakteweer zijn niet betrouwbaar genoeg voor de koplijn.
- Aerosolen / bosbrandrook: echte driver van stermagnitude tijdens rookseizoen, maar nog niet in het model. Open-Meteo levert een deel hiervan; meenemen in de zicht-gate staat op de roadmap.
Bronnen
- Open-Meteo — uurlijkse bewolking + zonsopkomst + multi-model spread. Gratis, geen auth, naamsvermelding vereist.
- Aviation Weather Center — METAR-waarnemingen van elk commercieel vliegveld, gratis.
- astronomy-engine — posities van zon en maan, op- en ondergang, schemering, fase en verlichting, lokaal berekend voor elke stad en nacht (MIT-licentie, geen externe API).
- Voorspellingen verversen ongeveer elke 30 minuten; METAR elk uur; multi-model fetch op elk score-verzoek, 10 minuten gecachet.
Waarom het werkt zoals het werkt
Een sterren-kijk-score moet eerlijk falen. De meeste weer-apps tonen "30% bewolking" als de hemel bewolkt is omdat hun model fout zit; hun UI suggereert dan een vertrouwen dat de data niet verdient. Wij tonen liever 6,5 met een "voorzichtige schatting"-pil — een getal dat het meningsverschil al meeneemt — dan 8,5 zonder voorbehoud.
De score wordt nooit perfect — atmosferische wetenschap is moeilijk en de meeste sterren-kijk-failmodes zijn hyper-lokaal. Maar elke laag hier bestaat omdat we de score op een specifieke manier hebben betrapt op liegen, ten minste één keer. De kalibratie-loop is hoe we het blijven leren.