fix(mera/backtest): свежая фикстура не реплеилась, а разрез по сегментам врал
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
Три связанные правки в харнесе бэктеста. 1. replay_fixture жёстко пинил estimate_dedup_analogs_enabled = False. Это верно только для фикстуры, захваченной до #2173, когда дедуп был выключен по умолчанию. Любая свежая фикстура при этом становилась нереплеибельной: с dedup=True на захвате часть сделок уходит под sufficiency-гейт с median_ppm2<=0, ratio_resolver для них не зовётся, а реплей с dedup=False их оценивает и падает на «control flow diverged from capture». Теперь значение берётся из settings_at_capture; фикстуры без этого поля ведут себя как раньше. 2. Разрез per_segment группирует ошибки по SOLD руб/м2 — по величине, которую движок и предсказывает. Корзины отобраны по шуму исхода, отсюда регрессия к среднему: даже несмещённый предиктор выглядит завышающим в дешёвых корзинах и занижающим в дорогих. На контрольном предикторе без перекоса по построению (13074 сделки ЕКБ) этот разрез даёт бизнес -17.04% и элит -30.06% — больше, чем наблюдавшийся перекос движка. По этому артефакту уже один раз откалибровали estimate_segment_multipliers (#2255). Блок оставлен для сравнимости с историей прогонов, но помечен предупреждением в докстринге и в печатном выводе. 3. Добавлен per_area_bucket — разрез по площади, независимой от исхода, с границами asking_to_sold_ratios (30/44/62/85). Промах конкретного бакета ratio виден сразу. Блок аддитивный: area_rows=None у старых вызывающих даёт пустой словарь, форма ответа не меняется. Фикстура заменена на свежую (1600 сделок ЕКБ с 2025-06, 1269 оценённых, захват после #2620) — прежняя от 2026-07-04 старше перехода ratio на area-бакеты и показывала точность конфигурации, которой в проде уже нет. Baseline перезахвачен: n 269 -> 1269, MAPE 12.63 -> 13.90, bias -3.74 -> -0.78, покрытие 86.13%. Сдвиг метрик — смена измерительной выборки, а не регресс движка. В каверзы модуля добавлен пункт (d): предсказание идёт из центроида улицы, а не из адреса дома — в открытом датасете Росреестра нет номера дома, кадастры все вида 66:41:0000000:0, и 65971 сделка садится в 3137 координат.
This commit is contained in:
parent
2b62d279fe
commit
9516d95277
3 changed files with 220 additions and 84 deletions
|
|
@ -69,6 +69,16 @@ CAVEATS (read these before trusting the numbers)
|
|||
(c) ДКП ≠ TRUE MARKET — a registered ДКП price is what the parties declared
|
||||
to rosreestr; it can diverge from the genuine transaction price (tax
|
||||
optimisation, related-party sales, etc.).
|
||||
(d) ПРЕДСКАЗАНИЕ ИДЁТ ИЗ ЦЕНТРОИДА УЛИЦЫ, НЕ ИЗ АДРЕСА ДОМА (#3251). Открытый
|
||||
датасет Росреестра НЕ содержит номера дома: `deals.address` выглядит как
|
||||
«Екатеринбург, Краснолесья», кадастры приходят как `66:41:0000000:0`
|
||||
(пригодных 0 из 108 623), а геокод сажает 65 971 сделку за 2025+ всего в
|
||||
3 137 различных координат. Значит аналоги ищутся в радиусе от ЦЕНТРА УЛИЦЫ,
|
||||
а `--resolve-house-id` резолвит дом лишь для ~40% выборки. Это раздувает
|
||||
MAPE и бьёт по классам с редкой застройкой сильнее прочих: часть перекоса
|
||||
по 4+ комнатам — цена такой привязки, а не ошибка модели. Любой замер, где
|
||||
сделка связывается с КОНКРЕТНЫМ зданием по геометрии (материал стен,
|
||||
этажность, цена собственного дома), этим скомпрометирован.
|
||||
|
||||
PERFORMANCE
|
||||
-----------
|
||||
|
|
@ -541,6 +551,24 @@ def _segment_label(ppm2: float) -> str:
|
|||
def _segment_metrics(rows: list[tuple[float, float]]) -> dict[str, dict[str, Any]]:
|
||||
"""Per-price-segment signed-error summary, bucketed by the SOLD ₽/m².
|
||||
|
||||
⚠ #3251 — ЧИТАТЬ ПЕРЕД ЛЮБОЙ КАЛИБРОВКОЙ ПО ЭТОМУ РАЗРЕЗУ. Корзина задаётся
|
||||
величиной, которую движок и предсказывает, то есть ИСХОДОМ. Это отбирает корзины
|
||||
по шуму исхода и порождает регрессию к среднему: даже идеально несмещённый
|
||||
предиктор выглядит завышающим в дешёвых корзинах и занижающим в дорогих.
|
||||
Монотонный спуск «чем дороже сегмент, тем сильнее занижаем» — подпись артефакта,
|
||||
а НЕ находка.
|
||||
|
||||
Контрольный опыт (13 074 сделки ЕКБ, 2026-08-29): предиктор без сегментного
|
||||
перекоса по построению даёт в этих корзинах эконом +22.28%, бизнес −17.04%,
|
||||
элит −30.06% — БОЛЬШЕ, чем наблюдавшийся перекос движка. На этом артефакте уже
|
||||
один раз откалибровали `estimate_segment_multipliers` (#2255, флаг с тех пор
|
||||
выключен, см. предупреждение в config.py).
|
||||
|
||||
Блок оставлен для сравнимости с историей прогонов. Для калибровки пользуйся
|
||||
`per_area_bucket` ниже — там ось (площадь) независима от исхода. Настоящий
|
||||
ценовой бэнд требует независимого прокси (медиана объявлений собственного дома,
|
||||
охват 74.6%), которого в офлайн-фикстуре нет.
|
||||
|
||||
Each input row is ``(pred_ppm2, sold_ppm2)``. We bucket by the segment of the
|
||||
SOLD price (ground truth), compute signed_error_pct = 100*(pred-sold)/sold,
|
||||
and run `_errors_summary` per band. Rows with sold<=0 are dropped (can't
|
||||
|
|
@ -635,11 +663,41 @@ def _calibration_metrics(
|
|||
return out
|
||||
|
||||
|
||||
def _area_bucket_metrics(
|
||||
rows: list[tuple[float, float, float]],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""#3251: ошибка по AREA-БАКЕТАМ — ось независима от исхода.
|
||||
|
||||
Каждая строка — ``(expected_sold_ppm2, sold_ppm2, area_m2)``. Бакеты те же, что
|
||||
у `asking_to_sold_ratios` (границы 30/44/62/85, см. `app.tasks.asking_to_sold_ratio
|
||||
.area_bucket`), поэтому разрез напрямую показывает, какой бакет ratio промахивается.
|
||||
|
||||
В отличие от `per_segment` здесь НЕТ регрессии к среднему: площадь известна на
|
||||
входе и не зависит от того, что вышло. Чистая функция, без БД.
|
||||
"""
|
||||
edges = ((30.0, "0 <30"), (44.0, "1 30-44"), (62.0, "2 44-62"), (85.0, "3 62-85"))
|
||||
|
||||
def label_for(area: float) -> str:
|
||||
for upper, label in edges:
|
||||
if area < upper:
|
||||
return label
|
||||
return "4 >=85"
|
||||
|
||||
order = [lbl for _, lbl in edges] + ["4 >=85"]
|
||||
by: dict[str, list[float]] = {lbl: [] for lbl in order}
|
||||
for pred, sold, area in rows:
|
||||
if sold <= 0 or area <= 0:
|
||||
continue
|
||||
by[label_for(area)].append(100.0 * (pred - sold) / sold)
|
||||
return {lbl: _errors_summary(by[lbl]) for lbl in order}
|
||||
|
||||
|
||||
def _expected_sold_metrics(
|
||||
rows: list[tuple[float, float, int]],
|
||||
*,
|
||||
n_no_prediction: int = 0,
|
||||
per_rooms_no_prediction: dict[int, int] | None = None,
|
||||
area_rows: list[tuple[float, float, float]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""expected_sold error block: overall + per-rooms (via _compute_metrics) + per-segment.
|
||||
|
||||
|
|
@ -654,6 +712,9 @@ def _expected_sold_metrics(
|
|||
per_rooms_no_analogs=per_rooms_no_prediction,
|
||||
)
|
||||
m["per_segment"] = _segment_metrics([(pred, sold) for pred, sold, _ in rows])
|
||||
# #3251: независимая ось. area_rows=None (старые вызывающие) → блок пустой,
|
||||
# форма ответа не ломается.
|
||||
m["per_area_bucket"] = _area_bucket_metrics(area_rows or [])
|
||||
return m
|
||||
|
||||
|
||||
|
|
@ -688,12 +749,14 @@ def _compute_full_metrics(
|
|||
cov_by_conf: dict[str, list[tuple[float, float, float]]] = {b: [] for b in conf_order}
|
||||
sharp_rows: list[tuple[float, float, float]] = [] # (point, range_low, range_high)
|
||||
calib_rows: list[tuple[str, float | None, bool | None]] = []
|
||||
es_area_rows: list[tuple[float, float, float]] = [] # #3251 (pred, sold, area)
|
||||
|
||||
for p in predictions:
|
||||
signed: float | None = None
|
||||
if p.expected_sold_ppm2 is not None and p.sold_ppm2 > 0:
|
||||
signed = 100.0 * (p.expected_sold_ppm2 - p.sold_ppm2) / p.sold_ppm2
|
||||
es_rows.append((p.expected_sold_ppm2, p.sold_ppm2, p.rooms))
|
||||
es_area_rows.append((p.expected_sold_ppm2, p.sold_ppm2, p.area_m2))
|
||||
|
||||
covered: bool | None = None
|
||||
if p.range_low is not None and p.range_high is not None:
|
||||
|
|
@ -712,6 +775,7 @@ def _compute_full_metrics(
|
|||
es_rows,
|
||||
n_no_prediction=n_no_prediction,
|
||||
per_rooms_no_prediction=per_rooms_no_prediction,
|
||||
area_rows=es_area_rows,
|
||||
),
|
||||
"range_coverage": {
|
||||
"overall": _range_coverage(cov_rows),
|
||||
|
|
@ -882,6 +946,9 @@ def _render_full_table(metrics: dict[str, Any]) -> str:
|
|||
lines.extend(_render_metrics_block("[EXPECTED_SOLD] per-rooms", es, no_col="no_pred"))
|
||||
lines.append("")
|
||||
lines.extend(_render_segment_block(es["per_segment"]))
|
||||
if es.get("per_area_bucket"):
|
||||
lines.append("")
|
||||
lines.extend(_render_area_bucket_block(es["per_area_bucket"]))
|
||||
|
||||
conf_order = metrics.get("confidence_order") or list(CONFIDENCE_BUCKETS)
|
||||
lines.append("")
|
||||
|
|
@ -909,6 +976,9 @@ def _render_segment_block(per_segment: dict[str, Any]) -> list[str]:
|
|||
header = f" {'segment':<10} {'n':>5} {'bias%':>8} {'MAPE%':>8} {'p25%':>8} {'p75%':>8}"
|
||||
out: list[str] = [
|
||||
"[EXPECTED_SOLD] per price-segment (band by SOLD ₽/m²):",
|
||||
" ⚠ корзина задана ИСХОДОМ → регрессия к среднему. Монотонный спуск здесь",
|
||||
" появляется и у несмещённого предиктора. НЕ калибровать по этому блоку",
|
||||
" (#3251) — бери per area-bucket ниже.",
|
||||
header,
|
||||
" " + "-" * (len(header) - 2),
|
||||
]
|
||||
|
|
@ -923,6 +993,25 @@ def _render_segment_block(per_segment: dict[str, Any]) -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
def _render_area_bucket_block(per_area: dict[str, Any]) -> list[str]:
|
||||
"""#3251: таблица ошибки по area-бакетам (ось независима от исхода)."""
|
||||
header = f" {'area bucket':<12} {'n':>5} {'bias%':>8} {'MAPE%':>8} {'p25%':>8} {'p75%':>8}"
|
||||
out: list[str] = [
|
||||
"[EXPECTED_SOLD] per area-bucket (ось НЕЗАВИСИМА от исхода — калибруй здесь):",
|
||||
" бакеты = границы asking_to_sold_ratios (30/44/62/85), промах видно",
|
||||
" сразу по конкретному бакету ratio.",
|
||||
header,
|
||||
" " + "-" * (len(header) - 2),
|
||||
]
|
||||
for label, m in per_area.items():
|
||||
out.append(
|
||||
f" {label:<12} {m.get('n', 0):>5} "
|
||||
f"{_fmt_pct(m.get('median_bias_pct')):>8} {_fmt_pct(m.get('mape_pct')):>8} "
|
||||
f"{_fmt_pct(m.get('p25_pct')):>8} {_fmt_pct(m.get('p75_pct')):>8}"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# #2255 --calibrate-segments — per-segment multiplier proposal (PRINT-ONLY)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -1575,8 +1664,18 @@ def replay_fixture(fixture: dict[str, Any]) -> dict[str, Any]:
|
|||
# вызовов разъехалась бы. Пин ЗДЕСЬ, а не только в CI-гейте: гейт монкипатчил флаг
|
||||
# сам, а документированная регенерация baseline (--from-fixture --update-baseline)
|
||||
# — нет, и с #2173 писала baseline, который тест не совпал бы НИКОГДА.
|
||||
# #3251: РАНЬШЕ здесь стоял жёсткий `= False`. Это верно только для фикстуры,
|
||||
# захваченной ДО #2173 (когда дедуп был выключен по умолчанию), и делает
|
||||
# НЕРЕПЛЕИБЕЛЬНОЙ любую свежую фикстуру: при dedup=True на захвате часть сделок
|
||||
# уходит под sufficiency-гейт с median_ppm2<=0 и ratio_resolver для них не
|
||||
# вызывается, а реплей с dedup=False их оценивает и упирается в
|
||||
# «control flow diverged from capture». Берём значение ИЗ ФИКСТУРЫ (fallback
|
||||
# False — для старых фикстур без settings_at_capture поведение прежнее).
|
||||
_dedup_capture = bool(
|
||||
(fixture.get("settings_at_capture") or {}).get("estimate_dedup_analogs_enabled", False)
|
||||
)
|
||||
_dedup_saved = m.settings.estimate_dedup_analogs_enabled
|
||||
m.settings.estimate_dedup_analogs_enabled = False
|
||||
m.settings.estimate_dedup_analogs_enabled = _dedup_capture
|
||||
try:
|
||||
for rec in deals:
|
||||
kw = dict(rec["kwargs"])
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
{
|
||||
"calibration": {
|
||||
"high": {
|
||||
"coverage_pct": null,
|
||||
"mape_pct": null,
|
||||
"n": 0,
|
||||
"n_covered": 0
|
||||
"coverage_pct": 50.0,
|
||||
"mape_pct": 27.87,
|
||||
"n": 8,
|
||||
"n_covered": 4
|
||||
},
|
||||
"low": {
|
||||
"coverage_pct": 82.09,
|
||||
"mape_pct": 12.67,
|
||||
"n": 276,
|
||||
"n_covered": 220
|
||||
"coverage_pct": 86.68,
|
||||
"mape_pct": 13.76,
|
||||
"n": 1562,
|
||||
"n_covered": 1067
|
||||
},
|
||||
"medium": {
|
||||
"coverage_pct": 100.0,
|
||||
"mape_pct": 6.99,
|
||||
"n": 1,
|
||||
"n_covered": 1
|
||||
"coverage_pct": 73.33,
|
||||
"mape_pct": 17.91,
|
||||
"n": 30,
|
||||
"n_covered": 22
|
||||
}
|
||||
},
|
||||
"confidence_order": [
|
||||
|
|
@ -26,130 +26,167 @@
|
|||
],
|
||||
"expected_sold": {
|
||||
"overall": {
|
||||
"mape_pct": 12.63,
|
||||
"median_bias_pct": -3.74,
|
||||
"n": 269,
|
||||
"mape_pct": 13.9,
|
||||
"median_bias_pct": -0.78,
|
||||
"n": 1269,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": -15.17,
|
||||
"p75_pct": 8.67
|
||||
"p25_pct": -13.39,
|
||||
"p75_pct": 14.46
|
||||
},
|
||||
"per_area_bucket": {
|
||||
"0 <30": {
|
||||
"mape_pct": 14.55,
|
||||
"median_bias_pct": 7.59,
|
||||
"n": 161,
|
||||
"p25_pct": -4.16,
|
||||
"p75_pct": 29.4
|
||||
},
|
||||
"1 30-44": {
|
||||
"mape_pct": 13.12,
|
||||
"median_bias_pct": -0.51,
|
||||
"n": 503,
|
||||
"p25_pct": -11.98,
|
||||
"p75_pct": 14.88
|
||||
},
|
||||
"2 44-62": {
|
||||
"mape_pct": 12.99,
|
||||
"median_bias_pct": -2.82,
|
||||
"n": 379,
|
||||
"p25_pct": -14.44,
|
||||
"p75_pct": 10.26
|
||||
},
|
||||
"3 62-85": {
|
||||
"mape_pct": 15.32,
|
||||
"median_bias_pct": -5.33,
|
||||
"n": 179,
|
||||
"p25_pct": -18.99,
|
||||
"p75_pct": 7.16
|
||||
},
|
||||
"4 >=85": {
|
||||
"mape_pct": 23.21,
|
||||
"median_bias_pct": -21.36,
|
||||
"n": 47,
|
||||
"p25_pct": -29.97,
|
||||
"p75_pct": 6.3
|
||||
}
|
||||
},
|
||||
"per_rooms": {
|
||||
"0": {
|
||||
"label": "студия",
|
||||
"mape_pct": 16.96,
|
||||
"median_bias_pct": 16.96,
|
||||
"n": 35,
|
||||
"mape_pct": 14.55,
|
||||
"median_bias_pct": 7.59,
|
||||
"n": 161,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": 1.5,
|
||||
"p75_pct": 38.82
|
||||
"p25_pct": -4.16,
|
||||
"p75_pct": 29.4
|
||||
},
|
||||
"1": {
|
||||
"label": "1к",
|
||||
"mape_pct": 11.06,
|
||||
"median_bias_pct": -4.61,
|
||||
"n": 93,
|
||||
"mape_pct": 13.12,
|
||||
"median_bias_pct": -0.51,
|
||||
"n": 503,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": -14.43,
|
||||
"p75_pct": 8.15
|
||||
"p25_pct": -11.98,
|
||||
"p75_pct": 14.88
|
||||
},
|
||||
"2": {
|
||||
"label": "2к",
|
||||
"mape_pct": 17.39,
|
||||
"median_bias_pct": -11.71,
|
||||
"n": 74,
|
||||
"mape_pct": 12.99,
|
||||
"median_bias_pct": -2.82,
|
||||
"n": 379,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": -23.07,
|
||||
"p75_pct": -0.36
|
||||
"p25_pct": -14.44,
|
||||
"p75_pct": 10.26
|
||||
},
|
||||
"3": {
|
||||
"label": "3к",
|
||||
"mape_pct": 7.79,
|
||||
"median_bias_pct": -4.05,
|
||||
"n": 43,
|
||||
"mape_pct": 15.32,
|
||||
"median_bias_pct": -5.33,
|
||||
"n": 179,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": -10.26,
|
||||
"p75_pct": 3.82
|
||||
"p25_pct": -18.99,
|
||||
"p75_pct": 7.16
|
||||
},
|
||||
"4": {
|
||||
"label": "4+",
|
||||
"mape_pct": 16.38,
|
||||
"median_bias_pct": -1.32,
|
||||
"n": 24,
|
||||
"mape_pct": 23.21,
|
||||
"median_bias_pct": -21.36,
|
||||
"n": 47,
|
||||
"n_no_analogs": 0,
|
||||
"p25_pct": -14.91,
|
||||
"p75_pct": 15.41
|
||||
"p25_pct": -29.97,
|
||||
"p75_pct": 6.3
|
||||
}
|
||||
},
|
||||
"per_segment": {
|
||||
"бизнес": {
|
||||
"mape_pct": 13.65,
|
||||
"median_bias_pct": -10.54,
|
||||
"n": 46,
|
||||
"p25_pct": -27.15,
|
||||
"p75_pct": -1.31
|
||||
"mape_pct": 11.85,
|
||||
"median_bias_pct": -9.98,
|
||||
"n": 144,
|
||||
"p25_pct": -20.69,
|
||||
"p75_pct": -1.72
|
||||
},
|
||||
"комфорт": {
|
||||
"mape_pct": 10.14,
|
||||
"median_bias_pct": -5.01,
|
||||
"n": 101,
|
||||
"p25_pct": -15.49,
|
||||
"p75_pct": 4.37
|
||||
"mape_pct": 12.71,
|
||||
"median_bias_pct": -2.39,
|
||||
"n": 425,
|
||||
"p25_pct": -16.15,
|
||||
"p75_pct": 9.46
|
||||
},
|
||||
"премиум": {
|
||||
"mape_pct": 68.92,
|
||||
"median_bias_pct": -68.92,
|
||||
"n": 1,
|
||||
"p25_pct": -68.92,
|
||||
"p75_pct": -68.92
|
||||
"mape_pct": null,
|
||||
"median_bias_pct": null,
|
||||
"n": 0,
|
||||
"p25_pct": null,
|
||||
"p75_pct": null
|
||||
},
|
||||
"эконом": {
|
||||
"mape_pct": 14.2,
|
||||
"median_bias_pct": 3.33,
|
||||
"n": 115,
|
||||
"p25_pct": -8.69,
|
||||
"p75_pct": 27.3
|
||||
"mape_pct": 14.98,
|
||||
"median_bias_pct": 2.46,
|
||||
"n": 697,
|
||||
"p25_pct": -9.54,
|
||||
"p75_pct": 26.95
|
||||
},
|
||||
"элит": {
|
||||
"mape_pct": 33.2,
|
||||
"median_bias_pct": -33.2,
|
||||
"n": 6,
|
||||
"p25_pct": -42.75,
|
||||
"p75_pct": -22.4
|
||||
"mape_pct": 18.45,
|
||||
"median_bias_pct": -18.45,
|
||||
"n": 3,
|
||||
"p25_pct": -40.32,
|
||||
"p75_pct": -15.7
|
||||
}
|
||||
}
|
||||
},
|
||||
"headline": {
|
||||
"ask_median_ppm2": 145883.6593586467,
|
||||
"deal_median_ppm2": 125063.0,
|
||||
"spread_pct": 16.65
|
||||
"ask_median_ppm2": 136721.80831284,
|
||||
"deal_median_ppm2": 115770.5,
|
||||
"spread_pct": 18.1
|
||||
},
|
||||
"range_coverage": {
|
||||
"overall": {
|
||||
"coverage_pct": 82.16,
|
||||
"n": 269,
|
||||
"n_covered": 221
|
||||
"coverage_pct": 86.13,
|
||||
"n": 1269,
|
||||
"n_covered": 1093
|
||||
},
|
||||
"per_confidence": {
|
||||
"high": {
|
||||
"coverage_pct": null,
|
||||
"n": 0,
|
||||
"n_covered": 0
|
||||
"coverage_pct": 50.0,
|
||||
"n": 8,
|
||||
"n_covered": 4
|
||||
},
|
||||
"low": {
|
||||
"coverage_pct": 82.09,
|
||||
"n": 268,
|
||||
"n_covered": 220
|
||||
"coverage_pct": 86.68,
|
||||
"n": 1231,
|
||||
"n_covered": 1067
|
||||
},
|
||||
"medium": {
|
||||
"coverage_pct": 100.0,
|
||||
"n": 1,
|
||||
"n_covered": 1
|
||||
"coverage_pct": 73.33,
|
||||
"n": 30,
|
||||
"n_covered": 22
|
||||
}
|
||||
}
|
||||
},
|
||||
"sharpness": {
|
||||
"median_rel_width": 0.743,
|
||||
"n": 269
|
||||
"n": 1269
|
||||
},
|
||||
"unrecorded_lookup_calls": 0
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Reference in a new issue