Peildatum 29 april 2026 - WWS 50,2% [49,4-51,0%] op 16.496 listings | WWSO 77,9% [76,9-79,0%] op 6.118 listings

Methodologie van onze probabilistische huurcheck

Technische beschrijving van de probabilistische methode (kernel-bootstrap Monte Carlo met empirische priors) waarmee WoonBusters het wettelijk maximum huurbedrag (WWS/WWSO) schat voor huurwoningen in Nederland. Op 22.614 van ~52.800 gescraapte listings (43%): listings zonder geverifieerd adres of buiten WWS krijgen geen verdict. Empirische CI-coverage: ~78% bij nominaal 90% niveau.

1. Probleem

Het Nederlandse woningwaarderingsstelsel (WWS, Beleidsboek 2026) bepaalt het wettelijke maximum huurbedrag op basis van 11 rubrieken (1 t/m 11) plus rubriek 3b Koeling (toegevoegd 1-7-2024):

RubriekInhoudHoofd-input
1Oppervlakte vertrekkenm² woonkamer + slaapkamers
2Oppervlakte overige ruimtenm² hal/keuken/berging
3Verwarmingtype (cv/stadsverwarming/warmtepomp), aantal verwarmde vertrekken, warmteterugwinning
3bKoeling (sinds 1-7-2024)airco per vertrek (max 2 pt totaal)
4Sanitairaantal badkamers, toiletten, bad/douche/wastafels, bidet, urinoir, vloerverwarming
5Energieprestatieenergielabel (onbekend → per MC-iteratie gesampled uit P(label | bouwjaar, type), zie §10)
6Buitenruimtetuin / balkon / dakterras + afmetingen
7Bergingaanwezig ja/nee, type (privé/gedeeld)
8WOZ-waardeKadaster-waarde en per-m² vergelijking
9Gemeenschappelijke voorzieningengedeelde fietsenstalling, gemeenschappelijke tuin, wasruimte, etc.
10Parkeergelegenheid (sinds 1-7-2024)afgesloten garage (9 pt), overdekt/carport (6 pt), open parkeerplaats (4 pt), + exclusieve laadpaal (+2 pt)
11Bijzondere voorzieningenlift, alarm, video deurbel, EV-laadpunt, woonvoorzieningen gehandicapten
(geen rubriek) Monument-/nieuwbouw-opslagenRent surcharge ná punten→huur conversie. Rijksmonument: +35% op max huur (door ons gemodelleerd via RCE LOD). Niet gemodelleerd: gemeentelijk/provinciaal (+15%), nieuwbouw na 1-7-2024 (+10%), beschermd stadsgezicht pre-1965 (+5%), zorgwoning (+35% op puntensubtotaal van rubrieken 1-11)

In onze parsed representatie bestaan deze 11 rubrieken uit ~36 sub-features- rubriek 4 Sanitair alleen al heeft 10: aantal badkamers, extra toiletten, bad, douche, inloopdouche, wastafels, dubbele wastafel, vloerverwarming bad, bidet, urinoir. Een precieze item-voor-item mapping van WWS-rubrieken naar onze features (we dekken ~25–26 van de 36 items) staat op de data-transparantie pagina.

Het probleem: van een typische huurwoning op WoonBusters kennen we maar een handvol van deze inputs uit de advertentie zelf - oppervlakte, energielabel, tuin, balkon, soms aantal slaapkamers en lift. De overige features - vooral sanitair-details, verwarmingstype en berging - zijn vrijwel nooit beschikbaar. Maar het WWS vereist ze allemaal om tot een exact puntentotaal te komen.

Onze aanpak: bereken een waarschijnlijkheidsverdeling over het puntentotaal door gebruik te maken van empirische kansverdelingen voor de onbekende kenmerken, gebaseerd op echte data van fysiek vergelijkbare woningen. Dat geeft een 90% betrouwbaarheidsinterval op de maximale huur - geen alles-of-niets oordeel.

Wat zegt "X% betaalt te veel" eigenlijk?

Tier-disclosure: niet elke gescoorde woning heeft hetzelfde data-niveau. Een eerlijke huurcheck vereist een geverifieerd adres - alleen dan kunnen we via Kadaster de exacte WOZ van die specifieke woning ophalen. Voor listings zonder huisnummer (alleen lat/lon, of alleen postcode/wijk) tonen we geen verdict: een schatting via stad × m² of via de coördinaten van het gebouw zou systematisch afwijken van de werkelijke WOZ van de individuele woning, en daarmee mensen onnodig laten denken dat hun huur te hoog is. Zie de data-transparantie pagina voor de volledige uitsplitsing.

Per peildatum 2026-04-29: twee aparte cijfers, niet één. We rapporteren WWS en WWSO altijd los van elkaar omdat ze fundamenteel verschillende regimes zijn. Een blended cijfer (57,7%) is mechanisch opwaarts vertekend door WWSO en hoort niet als losse headline gebruikt te worden.

Segmentn (Tier 1)% te duur95% CI
WWS - zelfstandige woonruimte (appartementen, studio's) op exact-adres Kadaster16.49650,2%49,4-51,0%
WWSO - onzelfstandige woonruimte (kamers) via 3-tier COROP6.11877,9%76,9-79,0%
(afgeleid) Tier 1 blend22.61457,7%57,0-58,3%

De zelfstandige-markt-headline is 50,2% te duur op 16.496 appartementen + studio's. Dat is het cijfer dat het reguliere huurmarkt-debat karakteriseert. WWSO (kamers) zit mechanisch hoger omdat onzelfstandige woonruimte geen vrije-sectorgrens heeft - er is altijd een legaal maximum, dus elke kamer boven die grens telt automatisch als "te duur".

Selectiebias-disclaimer (cruciaal voor representativiteit): Onze populatie is per definitie de openbaar geadverteerde markt. Particuliere verhuur via persoonlijke netwerken, mond-tot-mond circuits en de zwarte markt zit er niet in. De Tier 1 deelpopulatie (22.614 listings) is bovendien biased richting urbane regio's en agency-listed woningen - Amsterdam ~20%, Rotterdam ~10%, Den Haag ~9% - omdat bureaus en grote platforms vaker volledige adressen publiceren dan particulieren. Dit cijfer is dus een betrouwbare representatie van de publiek-zichtbare urbane huurmarkt voor zelfstandige woningen, niet van de Nederlandse huurmarkt als geheel. Voor regionale uitsplitsingen en marktsegmenten met systematisch andere prijsstellingen (sociale verhuur via woningbouwcorporaties, particuliere verhuur in dorpen) zou je een ander dataset nodig hebben.

Wat doen we met listings zonder geverifieerd adres?

Van de ~52.800 gescraapte listings hebben er ~31.400 geen exact adres in een vorm waar we Kadaster mee kunnen bevragen. We splitsen die in drie categorieën, en voor alle drie tonen we geen verdict:

Plus filteren we op contracttype/property_type voor de drie categorieën die wettelijk buiten WWS/WWSO vallen (totaal 32 listings):

Tijdelijke huurovereenkomsten vallen WEL onder WWS/WWSO. Per Wet vaste huurcontracten (1-7-2024) zijn fixed_term en Temporary-contracten alleen toegestaan voor een limitatieve lijst uitzonderingen (jongeren-, studenten-, doelgroepen-, promovendi-, diplomaten- en sloop/renovatiecontracten). Voor al deze uitzonderingen blijft de wettelijke maximumhuur bindend. ~3.121 listings in Tier 1 hebben een expliciet "tijdelijk"-contracttype en die scoren we terecht. Alleen short-stay (verblijfsrecht) en anti-kraak (bruikleenovereenkomst) vallen écht buiten WWS.

Expat-listings vallen ook gewoon onder WWS - er is in de Nederlandse huurwet geen uitzondering op basis van nationaliteit; alleen contractsoort telt.

Eerlijke beperking: contract_term_months is slechts voor 0,13% van Tier 1 geparseerd. Een onbekend klein deel van Tier 1 zou juridisch alsnog short-stay kunnen zijn waar de bron geen expliciete tag heeft meegegeven. Zonder LLM-re-extractie van advertentietekst kunnen we die leak niet dichten.

Voor de live /huurcheck tool is dit géén beperking - gebruikers vullen daar hun eigen adres in waardoor we altijd Tier 1 data hebben. De beperking geldt alleen voor de listings-page badges en de gepubliceerde populatiestatistieken.

2. Data

2.1 Trainingsdata: Funda ground truth

Om te weten welke feature-combinaties bij welk type woning voorkomen, hebben we een trainingsdataset nodig. Funda is daarvoor uitermate geschikt omdat het:

Dataset (peildatum 2026-04-26): 2.850 huurlistings + 9.217 koop-listings = 12.162 Funda-rijen in totaal, uit 30+ steden. De set blijft groeien - we draaien periodiek nieuwe pyfunda-runs en UPSERT op global_id, dus elke run voegt nieuwe advertenties toe en ververst bestaande. Voor de relevantie van de koop-data voor het schatten van een huur-prijs zie §2.5.

StadListings
Amsterdam828
Den Haag371
Rotterdam289
Almere155
Leiden130
Utrecht123
Eindhoven100
Amstelveen76
Haarlem73
Overig (20+ steden)390

Opgeslagen in PostgreSQL-tabel ground_truth_funda (68 kolommen), waaronder global_id, url, title, address, price, living_area, construction_year, energy_label, characteristics (JSONB) en raw_data.

2.2 Feature parsing

Funda's characteristics is een losse dict van vrije-tekst velden. Onze parser (scraper/funda_characteristics_parser.py) extraheert hier 22 sampleable features uit waar de Probabilistische sampler direct uit trekt, plus een aantal afgeleide features (isolatie-score, parkeer-type) die we als hard condition gebruiken.

De volledige lijst van 22 sampleable features (de empirische verdelingen die in priors_v1.json worden opgeslagen):

Daarnaast extraheert de parser ook isolatiekenmerken (has_double_glazing, has_hr_glass, is_fully_insulated) en tuin_m2 als afgeleide gegevens; die gebruiken we als deterministische input of laten we vallen waar Funda ze te zelden vult (tuin_m2 komt voor in <5% van de rows).

2.3 Kwaliteitsfilter

Niet alle Funda-rows zijn bruikbaar als training. We vereisen:

Na filtering: 1.116 huur-trainingsrows (van 2.850 ruwe huurlistings). De ratio (~44%) wordt bepaald door hoe vaak verhuurders het Badkamervoorzieningen- en Verwarming-blok invullen op Funda. Plus daarbij komen nog 8.243 koop-trainingsrows (zie §2.5 voor waarom we koop-data poolen) → eindgebruik 9.359 trainingsrows in totaal, waarvan een deel feature-specifiek wordt gefilterd op offering_type (rent-only voor 9 features met persistente selectie-bias, rent+sale voor de overige).

Is 9.359 rows genoeg voor Monte Carlo? De sampler trekt geen 9.359-rij joint feature-vectoren, maar empirische marginale verdelingen per feature binnen een fysiek vergelijkbaar cohort (zie §3.4). Voor een typische woning resulteert dat in honderden rows per feature waar wel een waarde bekend is - meer dan voldoende voor bootstrap-sampling van categorische features (we schatten frequenties, geen hoog-dimensionale joint distributies). De holdout MAE blijft stabiel onder 3.5 punten ook bij 60% van het trainingsset (zie §5).

2.4 Kamernet faciliteit-data (voor WWSO)

Voor kamers hebben we aanvullend nodig: is de keuken gedeeld of privé? De douche? Het toilet? En met hoeveel huisgenoten? Kamernet's mobile-API geeft deze informatie expliciet via kitchenId, showerId, toiletId, housematesNumberId.

Opgeslagen in tabel kamernet_facilities. Na backfill: 2.624 rows met geverifieerde faciliteit-data (peildatum 26-04-2026).

2.5 Rent vs. Sale: zijn de datasets poolbaar?

Naast huurlistings hebben we ook 9.217 koop-listings uit Funda (peildatum 26-04-2026). Een logische vraag: kunnen we die koop-data direct meenemen in de priors voor onze huurcheck? De fysieke kenmerken (sanitair, verwarming, isolatie) hangen niet af van of een woning verhuurd of verkocht wordt - een bad is een bad. Maar er is selectiebias: koop-woningen zijn voornamelijk eengezinswoningen, huur-woningen voornamelijk appartementen.

Om dit te kwantificeren hebben we voor elk van de 22 sampleable features een per-feature statistische vergelijking gedraaid (chi-squared voor booleans en categorieën, Kolmogorov-Smirnov voor numeriek), met als oordelen:

Aggregaat: rent vs. sale (alle dwelling types)

Cohort: 20–200 m², bouwjaar 1900–2026. n_rent = 2.470, n_sale = 7.871.

VerdictAantal features
similar7
moderate2
different13

Op het eerste gezicht ziet het er slecht uit: 13 van de 22 features verschillen significant. Maar de grootste gaten zijn één-op-één een afspiegeling van het dwelling-type-verschil:

Featurerentsaleverklaring
has_lift42%13%huur is appartement, koop is huis
has_parking_garage32%8%idem
has_tuin72%92%idem (huis = tuin)
has_private_parking12%29%idem

Sample-compositie: rent is 88% appartement / 12% huis; sale is 44% appartement / 56% huis. Het echte antwoord vereist een like-for-like vergelijking binnen dwelling type.

Within-type: alleen appartementen

Cohort: appartementen, 20–200 m², 1900–2026. n_rent = 2.195, n_sale = 3.508.

VerdictAantal features
similar9
moderate2
different11

Beter, maar nog steeds 11 features verschillen. Deze gaten zijn echte selectie-bias: rent-appartementen zijn nieuwer / vaker gerenoveerd, met meer luxe-voorzieningen.

FeaturerentsaleΔ
has_lift47%31%+16 pt
has_parking_garage36%16%+19 pt
has_dual_sink29%14%+15 pt
has_bath35%24%+12 pt
has_walk_in_shower44%39%+5 pt

Within-type: alleen huizen

Cohort: eengezinswoningen, 20–200 m², 1900–2026. n_rent = 275, n_sale = 4.363.

VerdictAantal features
similar15
moderate2
different5

Veel beter: 15 van de 22 features statistisch gelijk. De vijf afwijkingen zijn praktisch verwaarloosbaar (has_lift 2% vs 0% - huizen hebben gewoon geen lift) of betreffen parkeer-features die alsnog enige selectiebias hebben.

Liggen de verschillen aan oppervlakte of bouwjaar?

Rent en sale verschillen mogelijk niet alleen op dwelling type, maar ook op oppervlakte en bouwjaar - bijvoorbeeld als rental investors systematisch nieuwere appartementen targeten. We hebben gemiddelde en mediane waarden vergeleken:

Cohortnarea medyear med
RENT-Apartment2.37082 m²2008
SALE-Apartment3.89881 m²1980
RENT-House285122 m²1995
SALE-House4.538124 m²1979

Oppervlakte is bijna identiek, maar bouwjaar verschilt dramatisch: rent-appartementen zijn gemiddeld 28 jaar nieuwer dan sale-appartementen. Dat verklaart een groot deel van de selectie-bias: rental investors kopen / bouwen nieuwere complexen voor de verhuurmarkt; sale-listings bestrijken de hele woningvoorraad terug tot 1900.

Test: appartementen-only én bouwjaar 2000+. n_rent = 1.375, n_sale = 1.073:

VerdictAantal features
similar13 (was 9)
moderate2
different7 (was 11)

Door ook op bouwjaar te matchen convergeren de sanitair-features (bad, inloopdouche, vloerverwarming, warmteterugwinning) van "different" naar "similar". De resterende verschillen zitten bijna allemaal in parkeer- en lift-features:

Featurerent (2000+)sale (2000+)
has_lift56%48%
has_parking_garage57%41%
has_private_parking12%25%
has_balkon90%80%

Dit is echte selectiebias die niet weggaat met fijner cohort-matchen: rental investors kiezen specifiek modernere appartementenblokken met garages en liften. Sale-data van "dezelfde periode" is daarom nog steeds geen zuivere vervanging voor rent-data op deze features.

Conclusie en gevolgen voor de priors

Onze aanpak: een per-feature-policy. Elke training-row wordt getagd met zijn offering_type (rent of buy). Bij het sampelen van een feature bepaalt een whitelist of we uit beide pools mogen trekken of alleen uit rent:

FeatureSample-poolReden
has_lift, has_parking_garage, has_private_parking, has_balkonrent onlypersistente selectiebias na cohort-matchen
has_dual_sink, outdoor_type, storage_typerent onlyverschillen blijven significant na year-matching
Alle 15 overige features (sanitair, verwarming, energie, tuin, dakterras, num_*)rent + salena cohort-matchen statistisch gelijk
Eerlijk over de pool-grootte per feature: de totale trainingset is 9.359 rows (1.116 rent + 8.243 sale), maar dat aantal wordt niet voor élke feature gebruikt:
FeaturegroepPoolAantal rowsVergroot effectieve cohort
9 rent-only features (lift, parkeren, balkon, dual_sink, walk_in_shower, underfloor_heating_bath, outdoor_type, storage_type)rent only1.116+13%
13 mixable features (sanitair-rest, verwarming, energie, num_*, tuin, dakterras, has_storage, has_heat_recovery)rent + sale9.3598.4× t.o.v. rent-only

De per-feature filter wordt toegepast bij elke Monte Carlo iteratie in sample_feature() in api/huurcheck/priors.py. Voor de rent-only features blijft het effectieve cohort dus klein (1.116 rent rows verdeeld over alle dwelling/area/jaar combinaties); voor de mixable features krijgen we een echte cohort-uitbreiding.

Sale-listings hebben overigens een hogere kwaliteit op de WWS-relevante velden: 89% van de sale-rows passeert het Badkamervoorzieningen + Verwarming kwaliteitsfilter, vs 39% voor rent. Eigenaren vullen op Funda meer detail in dan verhuurders. Deze hoge invul-graad maakt sale-data extra waardevol om sparse cohorten in de rent-set aan te vullen op de features waar het veilig is.

De rauwe vergelijkingsdata staan in docs/comparisons/rent_vs_sale_*.json in onze repo. We draaien de vergelijking opnieuw bij elke materiele uitbreiding van de sale-set zodat we kunnen monitoren of de balans verschuift.

3. Probabilistische inferentie

3.1 Basisformule

De WWS-puntenberekening is additief: elke rubriek draagt los punten bij, het totaal is de som, en de maximale huur is een deterministische functie van het totaal.

totaal_punten = oppervlakte + verwarming + sanitair + energie + buitenruimte
              + berging + WOZ + lift + monument_bonus
max_huur     = WWS_tabel(totaal_punten)

We kunnen de berekening splitsen in een deterministisch deel (wat we exact weten) en een gesampled deel (wat we uit priors moeten trekken).

3.2 Wat we deterministisch berekenen

RubriekInputPunten
1–2 Oppervlaktearea_m2 uit LLMarea_m2 × 1.0
5 Energieenergy_label (EP-online voor 91% van Tier 1; voor de overige 9% sampelen we per Monte Carlo iteratie uit de empirische verdeling P(label | bouwjaar, dwelling_type) - zie §10)WWS-tabel lookup
6 Buitenruimtehas_garden, has_balconyFunctie van bekende status
8 WOZKadaster lookup op exact adres (postcode + huisnummer). Listings zonder huisnummer krijgen géén verdict - er is geen fallback.WOZ-formule
(rent surcharge, geen rubriek)Rijksmonument via RCE LOD lookup (BAG-id koppeling)+35% op maximale huur ná punten→huur conversie

Deze 5 rubrieken zijn identiek in elke Monte Carlo iteratie. Geen sampling.

3.3 Wat we gesampled uit priors

RubriekOnbekendSampled features
3 VerwarmingCV vs stadsverwarming vs warmtepompheating_points_per_room
4 SanitairBad? Aantal wastafels?8 features (zie §2.2)
7 BergingPrivé of gedeeld?has_storage, storage_type
11 Bijzondere voorzieningenLift aanwezig?has_lift

Bij WWSO komt daar nog bij: keuken gedeeld/privé, douche gedeeld/privé, toilet gedeeld/privé, aantal huisgenoten (voor delingsfactor).

3.4 De empirische priors (kernel-bootstrap, v6)

Voor elke sampleable feature bouwen we een empirische verdeling op uit trainingsdata. De kernvraag: "Van huurwoningen die fysiek lijken op dit aanbod, hoe vaak komt feature X voor?"

Sinds v6: we gebruiken een Gaussian-kernel weighting in plaats van discrete cohort-windows. Elke training-row krijgt een continu gewicht op basis van log-area en bouwjaar afstand tot het target:

weight = exp(-0.5 × ((log(area / target_area) / 0.20)²
                   + ((year - target_year) / 15)²))

Bandwidth defaults h_log_area = 0.20 (1σ ≈ ±22% area) en h_year = 15. Een row ver weg krijgt verwaarloosbaar gewicht; een row dichtbij krijgt bijna 1. We trekken vervolgens met random.choices gewogen uit de pool.

Effective sample size (Kish 1965) meet hoeveel "effectieve" rows er in de pool zitten: ESS = (Σw)² / Σ(w²). Bij ESS ≥ 25 sampelen we direct. Bij sparser cohort verbreden we de bandwidth gladsmooth: 1.6× → 2.5× → 4×. Pas daarna laten we soft of hard conditions vallen.

Voorbeeld: voor een 55 m² woning uit 1965 krijgt een 50 m² 1960 row gewicht ≈ 0.96, een 80 m² 1970 row gewicht ≈ 0.27, een 200 m² 1990 row gewicht ≈ 0.001.

3.5 Conditionele sampling

Een WoonBusters-listing kan gedeeltelijke info hebben die de prior moet respecteren:

De sampler filtert het cohort eerst met alle hard+soft conditions. Als er < 25 rows overblijven, laat hij één soft condition tegelijk vallen (greedy: de meest restrictieve eerst). Hard conditions blijven altijd staan.

3.6 Per-feature onafhankelijke sampling

We trekken elke feature onafhankelijk uit zijn eigen subset van rows waar die feature bekend is (niet None).

Waarom niet joint sampling (hele feature-vector uit één Funda-row)? Empirisch getest en afgewezen omdat het een bias introduceert: Funda-rows waar alle features gevuld zijn, zijn systematisch de luxe, gerenoveerde listings waarvan de verhuurder alle velden heeft ingevuld. Die worden oververtegenwoordigd in de joint pool (MNAR op rij-niveau). Empirisch blijkt het accuraatheidsverlies klein: <1% MAE-verschil.

3.7 MNAR-handling

Sommige features zijn "presence features" - Funda vult het veld alleen in als het kenmerk aanwezig is. has_tuin is True als Funda "Tuin" vermeldt, anders None (niet False). Maar in werkelijkheid betekent het ontbreken van het Tuin-veld meestal dat er geen tuin is.

Oplossing: bij fit-tijd zetten we None → False voor deze 7 presence features: has_tuin, has_balkon, has_dakterras, has_storage, has_lift, has_private_parking, has_parking_garage.

3.8 Monte Carlo

Input: area_m2, price, energy_label, build_year, city, has_garden,
       has_balcony, num_bedrooms, property_type

Stap 1 - bepaal cohort
  find Funda rows met area±25%, year±25, same type

Stap 2 - bereken deterministische punten
  area_pts     = area_m2 × 1.0
  energy_pts   = WWS_tabel[energy_label]
  outdoor_pts  = 3.0 if has_balcony else 0 + 3.25 if has_garden else 0
  woz_pts      = WOZ_formule(area_m2, city)
  deterministic_total = area_pts + energy_pts + outdoor_pts + woz_pts

Stap 3 - Monte Carlo loop (N=10.000 iteraties):
  for i in 1..N:
    heating_pts_i = sample(heating_points_per_room) × num_rooms
    sanitair_pts_i = som van 8 gesamplede sanitair features
    storage_pts_i = sample(has_storage) × sample(storage_type)
    lift_pts_i = sample(has_lift) × 1.0

    total_pts_i = deterministic_total + heating_pts_i + sanitair_pts_i +
                  storage_pts_i + lift_pts_i
    max_rent_i = WWS_tabel(total_pts_i)

Stap 4 - aggregeer:
  median_pts = mediaan van total_pts_1..N
  p5_raw, p95_raw = 5% en 95% percentielen
  # Schaalfactor k=1.15 (zie §4, gecalibreerd tegen deterministisch oracle):
  p5  = median - 1.15 × (median - p5_raw)
  p95 = median + 1.15 × (p95_raw - median)

  P(overpaying) = fractie van iteraties waar price > max_rent_i
  verdict =
    'free_market' als >50% van iteraties ≥187 punten
    'overpaying'  als P(overpaying) > 0.7
    'uncertain'   als 0.3 < P(overpaying) ≤ 0.7
    'fair'        anders

4. Calibratie: betrouwbaarheidsinterval

Monte Carlo-sampling geeft van nature intervallen, maar die zijn empirisch te smal: onze "90% CI" bevat de waarheid maar in ~70% van de gevallen. Dit komt door:

  1. Pool tightness: bootstrap uit 18 distinct values comprimeert de staarten.
  2. Independent per-feature sampling: verliest joint correlaties, schuift massa naar het midden.

Calibratieprocedure

We doen een 60/20/20 train/calibrate/test split op de quality-filtered Funda-rows (peildatum 2026-04-26: 9.359 rijen):

  1. Train priors op 60%.
  2. Op 20% calibratie-set: bereken voor elk listing de ratio |true_points − median_points| / gem_half_CI, vind empirisch de scale factor k die 90% coverage oplevert.
  3. Test op 20% held-out data: pas k toe en controleer coverage.

Uitkomst calibratie (5 seeds, deterministisch oracle):

MetricWaarde
Raw 5/95 CI coverage (unscaled, k=1.0)85.2%
Scale factor k (gecalibreerd)1.15 (range 1.09–1.21)
Scaled 90% CI coverage (calibratie-test set, 5 seeds × 400 rows)89.5% (range 86.5–92.5%)
Empirische 90% CI coverage (volledige holdout, 10 seeds × 400 rows)78,0% [76,6%, 79,2%]
Eerlijke disclosure over coverage: de 89,5% calibratie-coverage is in-sample op de calibratie-test split (n=400 per seed, 5 seeds). De empirische coverage op de productie-holdout (n=4.000) zakt naar 78% - dat is het cijfer dat een gebruiker daadwerkelijk ervaart. De gepubliceerde "90% CI" under-cover dus systematisch met ongeveer 12 procentpunten op de staarten van de verdeling. Voor het gereguleerde segment (oracle <187 pt) is dat verschil nog groter: 60,9% empirische coverage. Zie het reliability-diagram in §5 voor de volledige uitsplitsing per CI-niveau.

5. Accuratesse (cross-validation)

Holdout-test: 80/20 random split op de quality-filtered Funda-rows. Priors gefit op train-set, predict op test-set met alleen WB-listing-level info (oppervlakte, energielabel, tuin, balkon, lift, parkeer-flags). De "oracle" gebruikt dezelfde calculator maar krijgt elk parsed sub-feature als override - zodat alle deterministische bijdragen (oppervlakte, WOZ, monument) wegvallen en de MAE puur de prior-driven onzekerheid meet.

Holdout-resultaten met deterministisch oracle

Wat de MAE 3,55 wel en niet meet: De "oracle" krijgt ALLE 22 sampleable features als override (deterministisch waar Funda waarde heeft). De gemeten MAE is dus puur de residuele MC-induced fout gegeven de priors, niet de fout die een eindgebruiker op /huurcheck ervaart. In productie zijn er ook listings waar features ontbreken die het oracle wél heeft. De productie-MAE ligt daarom hoger dan 3,55 pt - we schatten 4-6 pt als realistische bandbreedte (≈ €30–€45/maand) op basis van de extra MC-variantie wanneer je de oracle-overrides één voor één weer als prior laat sampelen. Een exact productie-MAE-cijfer kunnen we niet geven omdat we geen ground-truth WWS- puntentelling hebben voor de productiepopulatie (alleen voor Funda). Lees 3,55 pt als "ondergrens van de fout", niet als de echte productie-MAE.

10 random seeds × 400 stratified test rows = 4.000 holdout-comparisons (data: docs/comparisons/robustness_v6.json). Bootstrap 95% CI berekend over de 10 seed-gemiddelden. Stratificatie: random binnen offering_type (rent vs sale) en within-area-bin om te voorkomen dat één fold disproportioneel uit één segment komt.

MetricMean95% bootstrap CI
MAE (punten, ondergrens-schatting)3,55[3,46; 3,64]
MAE in euro's (≈)~€26/maand[€25; €27]
Bias+0,36 pt[+0,18; +0,53]
90% CI coverage77,9%[76,7%; 79,2%]
Regulated MAE (oracle <187 pt)5,48[5,19; 5,85]
Regulated bias+1,99 pt±0,36 (95%)
Regulated 90% CI coverage60,9%[57,1%; 64,8%]

Richting van de regulated bias: +1,99 pt betekent dat we gereguleerde listings (oracle <187 pt) gemiddeld te hoog scoren - we zien ze als minder gereguleerd dan ze zijn. Dat impliceert dat de gepubliceerde 57,7% te duur op Tier 1 een ondergrens is van het werkelijke percentage; het echte cijfer ligt waarschijnlijk 1-2 pp hoger (rond 60%). Individuele predicties kunnen ±10 pt afwijken; de systematische bias is statistisch significant maar materieel klein.

Root-cause analyse van regulated bias

We hebben drie hypotheses getest om te begrijpen waarom regulated listings überhaupt een bias hebben (n=823, dezelfde holdout):

HypotheseTestResultaat
Cohort-imbalance (93% Funda is vrije sector)Restricteer cohort tot regulated-only training rowsBias gaat van +1.99 → −2.44 (slaat door naar onderkant). Niet de oorzaak.
Eén dominante feature drijft de biasBias-decompositie per featureGeen feature met >0.4 pt bijdrage. Bias accumuleert over kleine effecten.
Hoge variantie maskeert kleine systematische componentstdev 5.32 op 823 rows = SE ~0.18+1.99 ± 0.36 (95%) - significant maar klein. Variantie is het echte verhaal.

Conclusie: er is geen makkelijke fix voor de bias zonder structurele verandering aan de calculator. Voor de gepubliceerde headline (57.7% op Tier 1) kan dit betekenen dat het echte cijfer ~1-2 pp hoger ligt, maar we kunnen het niet exact corrigeren. We rapporteren daarom het "onbevooroordeelde" cijfer en vermelden deze nuance expliciet.

Reliability diagram (calibratie per CI-niveau)

Standaard statistische toets: bij elk nominaal CI-niveau, hoeveel van de waarheden valt binnen het voorspelde interval? Idealiter 50% nominaal = 50% empirisch. Resultaat (n=4.000):

Nominal CIEmpirical95% CI
50%54.4%[52.9%, 56.0%]
70%66.0%[64.5%, 67.4%]
80%71.9%[70.5%, 73.3%]
90%78.0%[76.6%, 79.2%]
95%79.7%[78.4%, 80.9%]

Conclusie: bij 50% is de calibratie correct (54%), maar bij hogere niveaus convergeert de empirische dichtheid bij ~80%. De CI's zijn niet breed genoeg in de staarten. Dit suggereert dat het Monte Carlo sampling de echte verdeling niet helemaal vangt voor outliers - wat een logische beperking is van bootstrap-sampling binnen een eindige cohort.

Bias per cohort - waar zit de afwijking?

De gemiddelde bias op de full-WWS holdout is +0.36 pt (zie tabel boven), maar er zit heterogeniteit onder. Onderstaande sub-cohort-uitsplitsing komt uit een eerdere kleinere holdout-run (n=499, deterministisch oracle, vooral bedoeld om te zien waar de bias zich concentreert; voor de productie-MAE-cijfers zie de tabel hierboven). Bron: docs/comparisons/holdout_extras.json.

CohortnBiasMAECI cov
Pre-1945 buildings157−1.012.5887.9%
1945-1969 *57−1.022.0586.0%
1970-1989 *82−0.071.7193.9%
1990-2009106−0.852.0789.6%
2010+ *97−0.371.9591.8%
Eengezins (huizen)171−0.972.5287.7%
Meergezins (appt)328−0.561.9590.9%
Regulated (<187 pt) *37+0.821.2694.6%
Free sector462−0.822.2289.4%

* Cellen met n < 100: bias/MAE puntschattingen - bootstrap-CI niet gerapporteerd vanwege beperkte sample size. De Regulated-cel met n=37 in het bijzonder is beperkt informatief; de productie-headline blijft de n=4.000 holdout-bias +1,99 (zie tabel boven). Deze sub-cohort tabel is bedoeld om waar de bias zich concentreert te tonen, niet om absolute getallen vast te leggen.

Twee belangrijke patronen:
(1) Vooroorlogse en oude woningen (pre-1969) zijn onderschat met ongeveer 1 punt - sparser cohort, meer spreiding in werkelijke kenmerken.
(2) De bias flipt teken aan de gereguleerd/vrije-sector grens: gereguleerde woningen worden lichtjes overschat (+0.82 pt), vrije-sector woningen onderschat (−0.82 pt). Dit is regression-to-the-mean: de cohort-bootstrap trekt extreme woningen naar het midden. Voor het gereguleerde segment - waar de huurder echt iets aan heeft - is de MAE wel uitstekend (1.26 punten, coverage 94.6%).

Binaire classificatie: gereguleerd vs vrije sector

Op de full-WWS holdout (n=4.000) is de binaire classificatie (predicted <187 pt = gereguleerd) ~94% correct tegen het deterministische oracle. False-negatives (true gereguleerd gemarkeerd als vrije sector) zijn schadelijker voor huurders dan false-positives en concentreren zich aan de bovenrand van het regulated segment (180-186 pt) waar de MC-spreiding rond 187 ligt. Bron: docs/comparisons/robustness_v6.json.

6. WWSO (onzelfstandige woonruimte)

Voor kamers geldt het Woningwaarderingsstelsel Onzelfstandige Woonruimte - een compleet ander puntenstelsel:

Kern-onbekendheden

FeatureImpact op puntenBron
Gedeelde keuken4–10 punten ÷ NKamernet API direct
Gedeelde douche3–6 punten ÷ NKamernet API direct
Gedeeld toilet3 punten ÷ NKamernet API direct
Aantal huisgenotendelingsfactorKamernet API direct

Kamernet's mobile-API (~43% van onze kamerlisting-pool) geeft al deze velden expliciet. Voor andere bronnen vallen we terug op empirische priors:

property_type + area_binP(kitchen shared)P(shower shared)Mean housemates
Kamer <15 m²84%90%12
Kamer 15–25 m²63%85%8
Kamer 25–35 m²65%80%6
Kamer 35+ m²21%50%3
Studio (any)0%12%2
Appartement (any)13%13%2

Deze priors zijn empirisch gefit op 2.624 Kamernet-rows met bekende faciliteit-status.

7. Routing: WWS vs WWSO

Elk listing moet naar de juiste calculator. Beslissingsregel:

if kamernet facility data says "any shared" → WWSO
if kamernet facility data says "all private" → WWS
elif property_type in {Kamer, Room} → WWSO
elif property_type in {Studio, Appartement, Woning, Flat, ...} → WWS
elif wws_is_onzelfstandig flag from pipeline → WWSO
else → WWS (default)

Geïmplementeerd in api/huurcheck/route_calculator.py.

Belangrijke keuze: routing is op property_type, niet op oppervlakte. Een studio is een studio, ongeacht grootte. Een 14 m² "studio" gaat naar WWS (zelfstandig) tenzij Kamernet expliciet gedeelde faciliteiten meldt.

8. Datapijplijn samenvatting

1. FUNDA GROUND TRUTH (ad hoc, periodieke pyfunda runs)
   pyfunda → ground_truth_funda (12.162 rows totaal)
                                  ├─ 2.850 huur (offering_type='rent')
                                  └─ 9.217 koop (offering_type='buy')
   parser → parsed_features JSONB
   kwaliteitsfilter → 9.359 mixed training rows
                      (1.116 rent + 8.243 sale; zie §2.3 + §2.5)

2. KAMERNET FACILITIES (continu - groeit elke scrape-cyclus)
   kamernet_runner.py → kamernet_facilities (2.624 rows)
   Voor WWSO: keuken / douche / toilet / aantal huisgenoten

3. PRIORS (fit bij start van sessie)
   PriorStore.load() vanuit ground_truth_funda
   Per-feature kernel-bootstrap: rent-only voor 9 features
   met selectie-bias, rent + sale voor de overige
   Continue Gaussian-kernel weighting op (log-area, year)

4. INFERENCE PER LISTING
   route_calculator → wws of wwso
   calculate_wws_bayesian / calculate_wwso_bayesian
   N=10.000 Monte Carlo iteraties, k=1.15 CI-schaalfactor
   → BayesianResult (median, p5, p95, p_overpaying, verdict)

5. BACKFILL STORAGE
   bayesian_huurcheck tabel (listing_id primary key)
   Upserted na elke berekening, samen met data_quality_tier

9. Softwarecomponenten

ModuleDoel
api/huurcheck/funda_wws.pyDeterministische features → WWS rubriek-punten
api/huurcheck/priors.pyEmpirische priors met cohort-matching
api/huurcheck/calculator_bayesian.pyWWS Monte Carlo calculator
api/huurcheck/calculator_bayesian_wwso.pyWWSO Monte Carlo calculator
api/huurcheck/route_calculator.pyRouting WWS vs WWSO
scraper/funda_characteristics_parser.pyFunda characteristics → features
scraper/adapters/kamernet_runner.pyKamernet facility extractie
runners/fetch_funda_ground_truth.pypyfunda scraper
runners/parse_funda_characteristics.pyParse-runner voor ground truth
runners/backfill_kamernet_facilities.pyKamernet detail-API backfill
runners/run_bayesian_backfill.pyBulk backfill van alle listings

10. Belangrijke aannames en beperkingen

Wat we expliciet NIET modelleren

Bias-waarschuwingen

Onbekende onbekenden

Voor aggregate marktrapportage is dit verwaarloosbaar (<2% van de populatie is getroffen). Voor individuele huurcheck-verdicts zou dit een apart signaal moeten zijn.

11. Referenties

Vragen, opmerkingen of suggesties? Mail [email protected]. Een Engelstalige versie van deze pagina staat op /en/huurcheck/methodologie.