1. Problem
The Dutch housing valuation system (WWS, Beleidsboek 2026) sets the legal maximum rent based on 11 main categories (surface area, heating, sanitary facilities, energy performance, outdoor space, storage, WOZ value, amenities, parking, monument surcharges) plus category 3b Cooling (added 1 July 2024).
In our parsed representation these expand to ~36 sub-features (sanitary alone has 10). For a typical listing on WoonBusters we only know a handful from the ad itself - area, energy label, garden, balcony, sometimes bedroom count and lift. The rest - especially sanitary details, heating type, and storage - are almost never available. The WWS needs all of them for an exact point total.
Our approach: compute a probability distribution over the point total using empirical priors for the unknown features, conditioned on physically similar listings. That gives a 90% confidence interval on the maximum legal rent - not a worst-case/ best-case range that flips classification on a single coin-flip.
2. Data
2.1 Training data: Funda ground truth
Dataset (peildatum 2026-04-26): 12,162 Funda listings (rent + sale) from 30+ cities, scraped via the reverse-engineered mobile API (pyfunda). Stored in ground_truth_funda (68 columns). The parser extracts 22 sampleable features plus a handful of derived features used as hard conditions. Re-run periodically; UPSERT on global_id keeps it idempotent.
2.2 Quality filter
We require living_area, construction_year, the Badkamervoorzieningen field, and the Verwarming field. Rows marked "Studentenkamer" are excluded (they belong in WWSO). After filtering: 1,116 rent-only rows + 8,243 sale rows = 9,359 mixed training rows. We pool sale data because for most WWS-relevant physical features (sanitary, heating, energy) sale and rent listings of the same dwelling type and era are interchangeable; for 9 features that showed persistent selection bias we restrict sampling to rent-only.
2.3 Kamernet facility data (for WWSO)
Kamernet's mobile API exposes kitchenId, showerId, toiletId, and housematesNumberId. Stored in kamernet_facilities: 2,624 rows with verified facility status (as of 2026-04-26).
3. Probabilistic inference
The WWS score is additive, so we split the calculation into a deterministic part (area, energy label, outdoor, WOZ, monument) and a sampled part (heating type, sanitary features, storage, lift).
For each listing we build a cohort of physically similar Funda rows (area ±25%, year ±25, matching dwelling type) and widen progressively if <25 rows remain. From that cohort we draw empirical per-feature distributions, conditioning on "hard" facts from the listing (garden, balcony, lift, parking, shared kitchen) and relaxing "soft" constraints (energy label) only if the conditional cohort shrinks too much.
We draw each feature independently from its own non-null subset. Joint sampling (picking entire vectors from one Funda row) was empirically rejected: the rows with every field filled skew toward luxury listings (MNAR at row level). The independence assumption costs <1% MAE.
We run N=10,000 Monte Carlo iterations, sum the deterministic and sampled points, apply the monument bonus, and look up max_rent in the WWS table. We report the median, a scaled 90% confidence interval, P(overpaying), and a verdict (fair, uncertain, overpaying, or free_market).
4. Calibration: confidence interval
Raw Monte Carlo intervals are empirically too narrow - bootstrap pool tightness and independent per-feature sampling cause the 5/95 percentile range to under-cover the truth. We calibrate by scaling the half-widths around the median by k=1.15 (range 1.09–1.21 across 5 seeds against a fully deterministic oracle), giving 89.5% empirical coverage on the test set.
5. Accuracy (cross-validation)
Holdout: 10 random seeds × 400 stratified test rows = 4,000 holdout comparisons, bootstrap CI computed across the 10 seed-means.
| Metric | Mean | 95% bootstrap CI |
|---|---|---|
| MAE (points) | 3.55 | [3.46, 3.64] |
| Bias | +0.36 | [+0.18, +0.53] |
| 90% CI coverage | 77.9% | [76.7%, 79.2%] |
| Regulated MAE (oracle <187 pts) | 5.48 | [5.19, 5.85] |
6. WWSO (rooms)
Rooms fall under a different point system (no free-sector threshold; shared facilities are divided by the number of tenants; WOZ uses a 3-tier COROP comparison). When Kamernet API data is available (~43% of rooms) we know the facility split directly; otherwise we use empirical priors by property type and area bin, fit on 2,624 Kamernet rows. An unknown energy label gets the same treatment as under WWS: sampled per Monte Carlo iteration from P(label | construction_year, dwelling_type).
7. Routing: WWS vs WWSO
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, Apartment, House, Flat, ...} → WWS
elif wws_is_onzelfstandig flag from pipeline → WWSO
else → WWS (default)Routing is on property_type, not area. A 14 m² studio is still WWS unless Kamernet explicitly reports shared facilities.
8. Data pipeline summary
1. FUNDA GROUND TRUTH (ad hoc, periodic pyfunda runs)
pyfunda → ground_truth_funda (12,162 rows total)
├─ 2,850 rent (offering_type='rent')
└─ 9,217 sale (offering_type='buy')
parser → parsed_features JSONB
quality filter → 9,359 mixed training rows
(1,116 rent + 8,243 sale; see §2.3 + §2.5)
2. KAMERNET FACILITIES (continuous, grows each scrape cycle)
kamernet_runner.py → kamernet_facilities (2,624 rows)
For WWSO: kitchen / shower / toilet / housemate count
3. PRIORS (fit at session start)
PriorStore.load() from ground_truth_funda
Per-feature kernel bootstrap: rent-only for 9 features with
selection bias, rent + sale for the rest
Continuous Gaussian-kernel weighting on (log-area, year)
4. INFERENCE PER LISTING
route_calculator → wws or wwso
calculate_wws_bayesian / calculate_wwso_bayesian
N=10,000 Monte Carlo iterations, k=1.15 CI scale factor
→ BayesianResult (median, p5, p95, p_overpaying, verdict)
5. BACKFILL STORAGE
bayesian_huurcheck (listing_id PK)
Upserted after each calculation, with data_quality_tier9. Software components
| Module | Purpose |
|---|---|
api/huurcheck/funda_wws.py | Deterministic WWS rubric scoring |
api/huurcheck/priors.py | Empirical priors with cohort matching |
api/huurcheck/calculator_bayesian.py | WWS Monte Carlo calculator |
api/huurcheck/calculator_bayesian_wwso.py | WWSO Monte Carlo calculator |
api/huurcheck/route_calculator.py | Routing WWS vs WWSO |
runners/run_bayesian_backfill.py | Bulk backfill of all listings |
10. Assumptions and limitations
- Construction year via BAG: when missing, falls back to 1975. Production-grade integration would chain
postal_code + house_number → BAG → construction_year. - Exact WOZ: Kadaster WOZ-waardeloket lookup on the verified house number for Tier 1 listings (n=21,377). Listings without a verified house number get no verdict - there is no city × m² or wijk-average fallback anywhere in the published statistics or on the listings pages, because every plausible fallback we tested has a median error of 30%+ versus the ground-truth WOZ.
- Energy label fallback: 91% of Tier 1 listings have a real EP-online label; for the remaining 9% we sample a label per Monte Carlo iteration from the empirical distribution
P(label | construction_year, dwelling_type), so the 90% CI widens honestly instead of pretending to certainty. - Monument status: rijksmonument matched against the RCE Linked Open Data SPARQL endpoint (54,930 monuments loaded, 547 listings matched = 1.0%). Match grants the +35% rent surcharge per Beleidsboek 2026.
- Garden area: Funda populates this in <5% of rows, so we use typical values per
outdoor_type. - Funda sample is mid/high-end biased: ~93% of our Funda rows score in the free sector. We use Funda only as a source of physical structure, never for prices.
- Not modeled: Zorgwoning (+35% bonus on rubrics 1–11), post-July-2024 new builds (+10% under Wet betaalbare huur), and gemeentelijk/ provinciaal monument (+15%) - no public landmark register with BAG-id linkage exists for the latter two.
11. References
- Besluit huurprijzen woonruimte - Annex I, part A (self-contained)
- Besluit huurprijzen woonruimte - Annex I, part B (rooms)
- Beleidsboek Waardering Woonruimte 2026 (Huurcommissie)
- Wet betaalbare huur (Dutch State Gazette 2024, 195) - introduced the 187-point boundary