Skip to content

Commit e8397ee

Browse files
committed
feat(structural): genuine audited dataset + m_obs from real data + measured identification
Structural dataset expansion sprint over the already-validated UYAP/KAP/TOKI families (no new source/model/quality abstraction). Make the dataset REAL and MEASURE what it identifies. - Genuine audited seed under validation/structural/ (source_audited=true), derived from the frozen Level-2 records, kept distinct from fixtures: 1 UYAP auction (sold; appraised Q=4.5M -> winning 4.545M; parcel 509 / unit-net 32.5 distinct, gross null; partial legal floor), 1 KAP negotiated disposal (prior-appraisal 5.2M -> sale 5.508M; value_method=negotiation), 1 TOKI disclosure (PMVR3 -> 0 derivable period cohorts: differencing needs >=2 consecutive disclosures). - datasets.py: load_genuine_datasets + dataset_status; CLI 'sold structural dataset' reports genuine audited observations separately from fixtures (UYAP sold/unsold/winning/offer/bidder/exact-floor; KAP eligible/negotiated/appraisal/prior; TOKI disclosures/projects/strata/cohorts/revision-blocked). - moments.build_observed_moments: m_obs rebuilt from genuine data with moment provenance + unavailable (reasons). Single-observation variance is UNAVAILABLE (NaN), never guessed as 0. - KAP negotiated-calibration subset (value_method must explicitly support negotiation; non-related alone does NOT infer negotiation). - identify: moment Jacobian restricted to genuinely OBSERVED moments (simulated-but-unobserved sd moments do not inflate rank); three-way status NOT_IDENTIFIED / WEAKLY_IDENTIFIED (full rank but ill-conditioned or flat FREE-parameter profiles) / IDENTIFIED; no observation-count threshold. - MEASURED result on genuine data: m_obs = 3 single-observation means (uyap_sale_prob, uyap_win/appraisal, kap log(sale/appraisal)); sold structural identify -> NOT_IDENTIFIED (rank 2 / dim 6, condition number ->inf), sensitivity mode. Reported honestly. - Level-2 (KAP 963554, TOKI PMVR3, UYAP 16766356960) unchanged; consumer path frozen; no SaleProbability, no weak supervision, no fourth source, no fabricated fields/floors/counts/cohorts/identification. - 169 tests passing
1 parent 094c098 commit e8397ee

12 files changed

Lines changed: 587 additions & 71 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Data refresh](https://github.com/onatozmenn/sold/actions/workflows/kfe-refresh.yml/badge.svg)](https://github.com/onatozmenn/sold/actions/workflows/kfe-refresh.yml)
55
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
66
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7-
[![Tests](https://img.shields.io/badge/tests-161%20passing-brightgreen.svg)](tests/)
7+
[![Tests](https://img.shields.io/badge/tests-169%20passing-brightgreen.svg)](tests/)
88
[![Data](https://img.shields.io/badge/data-TCMB%20%C2%B7%20T%C3%9C%C4%B0K-informational.svg)](#data-sources)
99

1010
> Infer the **realized transaction price** of a Turkish home from its **asking** price — a provenance-aware valuation engine.
@@ -131,7 +131,7 @@ Each public source enters as **structural moments under its own mechanism**, nev
131131

132132
For an ordinary listing, **asking price is a noisy strategic signal of the seller reservation** (not ground truth, not a ceiling): `S` is conditioned on asking, fair value and tightness, `B`/`S` are drawn, trades (`B ≥ S`) retained, and the **conditional-on-trade** closing distribution returned — median, mean, an **80% structural interval**, trade probability, and **mechanism-transfer sensitivity**. This is a **structural inference, never an observed closing price or a measured ordinary-resale accuracy**.
133133

134-
**Identification before estimation.** Optimizer convergence is *not* identification. `sold structural identify` inspects the actual dataset and computes a numerical moment Jacobian `J(θ)=∂m_sim/∂θ'` (central differences, common random numbers) reporting rank, singular values, condition number and weakly-identified directions, plus per-parameter profile diagnostics for `θ` and the mechanism shifts. If `rank(J) < dim(θ)` the status is **`NOT_IDENTIFIED`** and prediction runs in **sensitivity mode**. Until an identified fit exists, `sold structural value` is labelled a **structural-method prototype using provisional parameters** — not a measured ordinary-resale model. See [`src/sold/structural/`](src/sold/structural/) and `sold structural value` / `identify` / `estimate`.
134+
**Identification before estimation.** Optimizer convergence is *not* identification. `sold structural identify` inspects the **actual audited dataset** (`sold structural dataset` reports genuine audited observations separately from fixtures) and computes a numerical moment Jacobian `J(θ)=∂m_sim/∂θ'` (central differences, common random numbers) **restricted to the moments that are genuinely observed** — reporting available vs unavailable moments (with reasons), moment provenance by source, rank, singular values, condition number, weakly-identified directions, and per-parameter profile diagnostics. The status is three-way: `NOT_IDENTIFIED` (`rank(J) < dim(θ)`), `WEAKLY_IDENTIFIED` (full rank but severely ill-conditioned or flat profiles), or `IDENTIFIED`; the first two run prediction in **sensitivity mode**. No observation-count threshold is used as the criterion. Until an identified fit exists, `sold structural value` is labelled a **structural-method prototype using provisional parameters** — not a measured ordinary-resale model. See [`src/sold/structural/`](src/sold/structural/) and `sold structural dataset` / `identify` / `value` / `estimate`.
135135

136136
## Broker Data Flywheel
137137

@@ -301,7 +301,7 @@ tests/ # offline unit / end-to-end tests
301301
## Testing
302302

303303
```bash
304-
pytest -q # 161 tests, fully offline (no network or API key required)
304+
pytest -q # 169 tests, fully offline (no network or API key required)
305305
```
306306

307307
## Methodology & References
@@ -325,7 +325,8 @@ Negotiation-margin figures from Turkish market reporting: İstanbul ≈ 10%, Ank
325325
- [x] **Direct-label quality gate (pre-ML)** — mandatory `origin` (`consumer_submission` / `test_fixture` / `demo_seed` / `manual_import`) so `asking_to_closing_labels()` **excludes test/demo by default** (opt-in `include_non_production=True`) and fixtures never inflate the genuine count; `quality_status` (`accepted`/`flagged`/`rejected`) that **hard-rejects only structurally impossible values** (non-positive price, closing-before-listing) and merely **flags** unusual ratios (extreme close-to-ask, final-above-initial, suspicious duration, duplicate) while preserving the original self-reported values; a privacy-preserving duplicate-candidate **fingerprint** (one-way SHA-256 over bucketed canonical non-personal fields that flags submissions collapsing to the **same canonical transaction fingerprint** — a canonical-fingerprint collision, **not** general near-duplicate similarity detection, and it does **not** identify a property or seller); genuine vs test/demo reported as **separate counts**
326326
- [ ] **First genuine real-world label** — exactly one *actual* seller-submitted completed residential sale passing through the product path + quality gate. **Current genuine direct-label count: 0** — the end-to-end test proves the acquisition *path* works, not that a real-world label has been acquired
327327
- [x] **Structural econometric core** — mechanism-aware generalized **Nash bargaining** (`P = ηB+(1−η)S`, `η` estimated, not hard-coded) fit by **Simulated Method of Moments**; TCMB-anchored hedonic fair value (relative premiums only, no listing intercept as level); structural UYAP auctions with the **statutory legal floor** (`muhammen_bedel` preserved as appraised value `Q`, never the reserve; partially-observed floors not fabricated); KAP `η`-calibration moments with a corporate mechanism shift; TOKİ cumulative-disclosure differencing into room-type cohort moments. Replaces weak-label aggregation as the core; the provenance registry and validated KAP/TOKİ/UYAP Level-2 records are kept; the consumer path is frozen as an optional future *validation* channel. No SaleProbability model yet
328-
- [x] **Statutory-floor fix, TCMB double-count audit & identification diagnostics** — corrected the İİK acceptance floor to `max(0.5·Q, priority_claims) + realization_costs`; audited the fair-value anchor so a contemporaneous TL/m² level is not trend-adjusted twice (KFE ratio only when rolling an older anchor); expanded the UYAP/KAP/TOKİ structural dataset schemas + observed-moment constructors (area semantics preserved, `log(sale/appraisal)` KAP moments, revision-guarded TOKİ cohorts); added `sold structural identify` (Jacobian rank / singular values / condition number / weak directions / profile diagnostics → `NOT_IDENTIFIED` → sensitivity mode). **Next: public structural dataset expansion → observed moments → identification → only then real SMM estimation**
328+
- [x] **Statutory-floor fix, TCMB double-count audit & identification diagnostics** — corrected the İİK acceptance floor to `max(0.5·Q, priority_claims) + realization_costs`; audited the fair-value anchor so a contemporaneous TL/m² level is not trend-adjusted twice (KFE ratio only when rolling an older anchor); expanded the UYAP/KAP/TOKİ structural dataset schemas + observed-moment constructors (area semantics preserved, `log(sale/appraisal)` KAP moments, revision-guarded TOKİ cohorts); added `sold structural identify` (Jacobian rank / singular values / condition number / weak directions / profile diagnostics → `NOT_IDENTIFIED` → sensitivity mode)
329+
- [x] **Genuine structural dataset (measured, not synthetic)** — wired the three validated Level-2 records as the genuine audited seed under [`validation/structural/`](validation/structural/) (`source_audited=true`, kept distinct from fixtures): **1 UYAP auction** (sold; appraised `Q`=4.5M → winning 4.545M; parcel/net areas distinct, gross null; partial legal floor), **1 KAP negotiated disposal** (prior-appraisal 5.2M → sale 5.508M; `value_method=negotiation`), **1 TOKİ disclosure** (PMVR3 → **0** derivable period cohorts: differencing needs ≥2 consecutive disclosures). `m_obs` is rebuilt from this genuine data — **3 single-observation means** (`uyap_sale_prob`, `uyap_win/appraisal`, `kap log(sale/appraisal)`); variances/quantiles reported **unavailable** at n=1 (never guessed). Measured result: `sold structural dataset` + `sold structural identify` → **`NOT_IDENTIFIED`** (rank 2 / dim 6, condition number →∞), sensitivity mode; a three-way `WEAKLY_IDENTIFIED` status and moment-provenance/unavailability reporting were added. **Next: genuine repeated TOKİ disclosures + more audited UYAP/KAP observations → rerun identify (data, not architecture)**
329330
- [ ] **SaleProbability** model (`P(sold ≤ N days)`) trained on collected outcomes
330331
- [ ] Live, ToS-reviewed fetchers for the public label sources
331332
- [ ] Broker-vs-benchmark analytics over an aggregate anonymized dataset

src/sold/cli.py

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,17 +1483,59 @@ def structural_estimate_cmd(
14831483
)
14841484

14851485

1486+
@structural_app.command("dataset")
1487+
def structural_dataset_cmd() -> None:
1488+
"""GERÇEK denetlenmiş yapısal gözlem durumu — fixture/illustratif kayıtlardan AYRI."""
1489+
from .structural import dataset_status
1490+
1491+
st = dataset_status()
1492+
g = st["genuine"]
1493+
typer.secho("Yapısal veri kümesi durumu (GERÇEK denetlenmiş)", fg=typer.colors.CYAN, bold=True)
1494+
u = g["uyap"]
1495+
typer.echo(
1496+
f" UYAP: denetlenmiş açık artırma {u['total_audited_auctions']} "
1497+
f"(satılan {u['sold']} · satılmayan {u['unsold']})"
1498+
)
1499+
typer.echo(
1500+
f" kazanan teklif {u['winning_bids_observed']} · teklif sayısı {u['offer_counts_observed']} "
1501+
f"· artıran {u['bidder_counts_observed']} · tam yasal-taban {u['exact_legal_floors_observed']}"
1502+
)
1503+
k = g["kap"]
1504+
typer.echo(
1505+
f" KAP: uygun elden çıkarma {k['audited_eligible_disposals']} "
1506+
f"(müzakere-kalibrasyon {k['negotiated_calibration_observations']}) · "
1507+
f"appraisal {k['appraisal_observations']} · prior-appraisal {k['prior_appraisal_observations']}"
1508+
)
1509+
t = g["toki"]
1510+
typer.echo(
1511+
f" TOKİ: denetlenmiş açıklama {t['audited_disclosures']} · proje {t['projects_represented']} "
1512+
f"· oda-tipi kümülatif strata {t['room_type_cumulative_strata']}"
1513+
)
1514+
typer.echo(
1515+
f" geçerli dönem kohortu {t['valid_derived_period_cohorts']} "
1516+
f"· revizyonla-bloklanan {t['revision_blocked_cohorts']}"
1517+
)
1518+
na = st["non_audited_records"]
1519+
typer.secho(
1520+
f" Denetlenmemiş (fixture/illustratif, GERÇEK sayıma KATILMAZ): "
1521+
f"UYAP {na['uyap']} · KAP {na['kap']} · TOKİ {na['toki']}",
1522+
fg=typer.colors.YELLOW,
1523+
)
1524+
1525+
14861526
@structural_app.command("identify")
14871527
def structural_identify_cmd(
1488-
auctions_file: Optional[Path] = typer.Option(None, "--auctions", help="UYAP açık artırma kayıtları (JSON)"),
1489-
kap_file: Optional[Path] = typer.Option(None, "--kap", help="KAP elden çıkarma kayıtları (JSON)"),
1528+
auctions_file: Optional[Path] = typer.Option(None, "--auctions", help="UYAP kayıtları (JSON) — verilirse gerçek seti geçersiz kılar"),
1529+
kap_file: Optional[Path] = typer.Option(None, "--kap", help="KAP kayıtları (JSON)"),
14901530
toki_file: Optional[Path] = typer.Option(None, "--toki", help="TOKİ açıklamaları (JSON)"),
1491-
demo: bool = typer.Option(False, "--demo", help="Sentetik veriyle diagnostiği göster"),
1531+
demo: bool = typer.Option(False, "--demo", help="Sentetik veriyle diagnostiği göster (gerçek değil)"),
14921532
) -> None:
1493-
"""Yapısal KİMLİKLENDİRME raporu: dataset sayıları + Jacobian rank/SVD/koşul + profiller.
1533+
"""Yapısal KİMLİKLENDİRME raporu — VARSAYILAN olarak GERÇEK denetlenmiş veri kümesinden.
14941534
1495-
Optimizer yakınsaması KİMLİKLENDİRME DEĞİLDİR. rank(J) < dim(θ) ise NOT_IDENTIFIED ve
1496-
tahmin sensitivity moduna geçer. Epistemik katı: veri yoksa 0 raporlanır (uydurma yok).
1535+
Dataset sayıları + kullanılabilir/eksik momentler (+ neden) + moment provenance +
1536+
Jacobian rank/SVD/koşul + zayıf yönler + eta/kayma profilleri + 3'lü durum. Optimizer
1537+
yakınsaması KİMLİKLENDİRME DEĞİLDİR. rank(J)<dim → NOT_IDENTIFIED; tam rank ama ağır
1538+
kondisyon bozukluğu/düz profil → WEAKLY_IDENTIFIED; ikisi de değilse IDENTIFIED.
14971539
"""
14981540
import json
14991541

@@ -1503,20 +1545,22 @@ def structural_identify_cmd(
15031545
DEFAULT_FREE,
15041546
MomentContext,
15051547
StructuralParams,
1548+
build_observed_moments,
15061549
context_from_datasets,
15071550
difference_disclosures,
15081551
identification_report,
1509-
kap_observed_moments,
15101552
load_auctions,
1553+
load_genuine_datasets,
15111554
load_kap_disposals,
15121555
observed_moments,
15131556
simulate_negotiations,
1514-
uyap_observed_moments,
15151557
)
15161558

15171559
auctions_df = kap_df = None
15181560
toki_res = None
15191561
m_obs: dict = {}
1562+
provenance: dict = {}
1563+
unavailable: list = []
15201564

15211565
if demo:
15221566
theta0 = StructuralParams(eta=0.6)
@@ -1525,48 +1569,52 @@ def structural_identify_cmd(
15251569
neg = simulate_negotiations(rng, V, theta0, 20000, mechanism="kap")
15261570
tr = neg["traded"]
15271571
m_obs = observed_moments(kap_realized=neg["price"][tr], kap_appraisal=V[tr])
1572+
provenance = {k: "synthetic" for k in m_obs}
15281573
ctx = MomentContext(
15291574
auction_appraised=np.array([]), auction_floors=np.array([]),
15301575
kap_appraisal=np.ones(60), reps=300,
15311576
)
15321577
else:
1533-
if auctions_file:
1534-
auctions_df = load_auctions(
1535-
json.loads(Path(auctions_file).read_text(encoding="utf-8"))
1536-
)
1537-
m_obs.update(uyap_observed_moments(auctions_df))
1538-
if kap_file:
1539-
kap_df = load_kap_disposals(
1540-
json.loads(Path(kap_file).read_text(encoding="utf-8"))
1541-
)
1542-
m_obs.update(kap_observed_moments(kap_df))
1543-
if toki_file:
1544-
toki_res = difference_disclosures(
1545-
json.loads(Path(toki_file).read_text(encoding="utf-8"))
1546-
)
1578+
if auctions_file or kap_file or toki_file:
1579+
if auctions_file:
1580+
auctions_df = load_auctions(json.loads(Path(auctions_file).read_text(encoding="utf-8")))
1581+
if kap_file:
1582+
kap_df = load_kap_disposals(json.loads(Path(kap_file).read_text(encoding="utf-8")))
1583+
if toki_file:
1584+
toki_res = difference_disclosures(json.loads(Path(toki_file).read_text(encoding="utf-8")))
1585+
else:
1586+
genuine = load_genuine_datasets() # VARSAYILAN: gerçek denetlenmiş veri
1587+
auctions_df, kap_df, toki_res = genuine["uyap"], genuine["kap"], genuine["toki_result"]
1588+
built = build_observed_moments(auctions_df, kap_df, toki_res)
1589+
m_obs, provenance, unavailable = built["moments"], built["provenance"], built["unavailable"]
15471590
ctx = context_from_datasets(auctions_df, kap_df)
15481591

15491592
rep = identification_report(
15501593
ctx, StructuralParams(), DEFAULT_FREE, m_obs=m_obs,
15511594
auctions=auctions_df, kap=kap_df, toki_result=toki_res,
1595+
provenance=provenance, unavailable=unavailable,
15521596
)
15531597
ds = rep["dataset"]
15541598
typer.secho("Yapısal kimliklendirme raporu", fg=typer.colors.CYAN, bold=True)
15551599
typer.echo(
1556-
f" UYAP: toplam {ds['uyap_total']} · satılan {ds['uyap_sold']} · "
1557-
f"satılmayan {ds['uyap_unsold']}"
1558-
)
1559-
typer.echo(
1560-
f" teklif gözlemli {ds['uyap_offer_count_observed']} · artıran gözlemli "
1561-
f"{ds['uyap_bidder_count_observed']} · tam yasal-taban {ds['uyap_exact_legal_floor_observed']}"
1600+
f" UYAP: toplam {ds['uyap_total']} · satılan {ds['uyap_sold']} · satılmayan {ds['uyap_unsold']} "
1601+
f"· teklif göz. {ds['uyap_offer_count_observed']} · artıran {ds['uyap_bidder_count_observed']} "
1602+
f"· tam-taban {ds['uyap_exact_legal_floor_observed']}"
15621603
)
15631604
typer.echo(f" KAP müzakereli elden çıkarma: {ds['kap_negotiated_disposals']}")
1564-
typer.echo(f" TOKİ geçerli proje-dönem strata: {ds['toki_valid_project_period_strata']}")
1605+
typer.echo(f" TOKİ geçerli proje-dönem kohortu: {ds['toki_valid_project_period_strata']}")
15651606
typer.echo(
1566-
f" Yapısal parametre (dim θ): {rep['n_structural_parameters']} · "
1567-
f"gözlenen moment: {rep['n_observed_moments']}"
1607+
f" dim(θ): {rep['n_structural_parameters']} · kullanılabilir moment: {rep['n_observed_moments']}"
15681608
)
1569-
color = typer.colors.GREEN if rep["status"] == "IDENTIFIED" else typer.colors.RED
1609+
if rep.get("moment_provenance"):
1610+
typer.echo(f" Moment provenance: {rep['moment_provenance']}")
1611+
for un in rep.get("unavailable_moments", []):
1612+
typer.echo(f" Eksik moment: {un['moment']} [{un['source']}] — {un['reason']}")
1613+
color = {
1614+
"IDENTIFIED": typer.colors.GREEN,
1615+
"WEAKLY_IDENTIFIED": typer.colors.YELLOW,
1616+
"NOT_IDENTIFIED": typer.colors.RED,
1617+
}.get(rep["status"], typer.colors.RED)
15701618
typer.secho(
15711619
f" DURUM: {rep['status']} (rank {rep['rank']} / dim {rep['n_structural_parameters']}) "
15721620
f"· mod: {rep['prediction_mode']}",
@@ -1577,16 +1625,17 @@ def structural_identify_cmd(
15771625
sv = ", ".join(f"{x:.2e}" for x in rep["singular_values"])
15781626
typer.echo(f" Tekil değerler: [{sv}] · koşul sayısı: {rep['condition_number']:.2e}")
15791627
for wd in rep.get("weakly_identified_directions", []):
1580-
typer.echo(f" Zayıf-kimliklendirilmiş yön (s={wd['singular_value']:.2e}): {wd['direction']}")
1628+
typer.echo(f" Zayıf yön (s={wd['singular_value']:.2e}): {wd['direction']}")
15811629
for pname, pr in (rep.get("profiles") or {}).items():
15821630
rr = pr.get("relative_range")
15831631
flag = "ZAYIF" if pr.get("weakly_identified") else "belirgin"
15841632
rr_txt = f"{rr:.2e}" if isinstance(rr, (int, float)) else "—"
15851633
typer.echo(f" Profil {pname}: göreli hedef aralığı {rr_txt}{flag}")
15861634
if rep["status"] != "IDENTIFIED":
15871635
typer.secho(
1588-
" Not: rank < dim → optimizer sonucu NOKTA TAHMİNİ olarak SUNULMAZ; sensitivity "
1589-
"mode. (Optimizer yakınsaması ≠ kimliklendirme.) Sonraki iş: veri kümesi genişletmesi.",
1636+
" Not: rank < dim veya zayıf kimliklendirme → optimizer sonucu NOKTA TAHMİNİ olarak "
1637+
"SUNULMAZ; sensitivity/prototip mode. (Optimizer yakınsaması ≠ kimliklendirme.) "
1638+
"Sonraki iş: gerçek UYAP/KAP/tekrarlı TOKİ gözlem genişletmesi.",
15901639
fg=typer.colors.YELLOW,
15911640
)
15921641

src/sold/structural/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
simulate_negotiations,
3030
trade_mask,
3131
)
32+
from .datasets import (
33+
GENUINE_DIR,
34+
dataset_status,
35+
load_genuine_datasets,
36+
load_kap_records,
37+
load_toki_records,
38+
load_uyap_records,
39+
)
3240
from .hedonic import HedonicPremium, roll_unit_price, tcmb_fair_value
3341
from .identify import (
3442
dataset_summary,
@@ -45,6 +53,7 @@
4553
from .moments import (
4654
MomentContext,
4755
align,
56+
build_observed_moments,
4857
context_from_datasets,
4958
observed_moments,
5059
simulated_moments,
@@ -88,12 +97,20 @@
8897
"MomentContext",
8998
"observed_moments",
9099
"simulated_moments",
100+
"build_observed_moments",
91101
"context_from_datasets",
92102
"align",
93103
"estimate_smm",
94104
"SMMResult",
95105
"smm_objective",
96106
"nelder_mead",
107+
# gerçek veri kümesi (denetlenmiş)
108+
"load_genuine_datasets",
109+
"dataset_status",
110+
"load_uyap_records",
111+
"load_kap_records",
112+
"load_toki_records",
113+
"GENUINE_DIR",
97114
# kimliklendirme (identification)
98115
"moment_jacobian",
99116
"identification_report",

src/sold/structural/auction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def auction_moments(sold, win_over_appraisal) -> dict:
180180
return {
181181
"uyap_sale_prob": sale_prob,
182182
"uyap_win_over_appraisal_mean": float(ratios.mean()) if ratios.size else float("nan"),
183-
"uyap_win_over_appraisal_sd": float(ratios.std()) if ratios.size > 1 else 0.0,
183+
"uyap_win_over_appraisal_sd": float(ratios.std()) if ratios.size > 1 else float("nan"),
184184
}
185185

186186

0 commit comments

Comments
 (0)