бэктест: подписи окна приведены к замеру, уверенность — к тому же прогону
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (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 / frontend-checks (pull_request) Successful in 1m21s

Снимает три блокера ревью на этой же ветке.

1. ПОДПИСЬ ОКНА. Числа перевели на представительную выборку, а подпись
осталась от прежнего прогона: «сделки II квартала 2026 года». Автор честно
записал, что квартальный состав пересборки не смотрел, и оставил критерий
снятия — посчитать deal_date по выборке. Посчитал:

    III кв 2025 — 106 (26,5 %)      I кв 2026 — 88 (22,0 %)
    IV кв 2025  — 110 (27,5 %)     II кв 2026 — 96 (24,0 %)

Пересборка покрывает ЧЕТЫРЕ квартала, II квартал 2026 — четверть её. Прежняя
подпись для этих чисел была бы прямым враньём.

2. ЗНАМЕНАТЕЛЬ. Был 5 954 — сделки только II квартала. Для выборки по четырём
кварталам это завышало долю вчетверо: 5,5 % вместо 1,3 %. Замер тем же
фильтром, что у выборки: в окне 24 333, из них II квартал 2026 — 5 954. Оба
числа одним запросом.

3. УВЕРЕННОСТЬ. Плитка «325 из 327» была из прогона 29.08, а лид секции печатал
«замер 31.08» — старому числу приписывалась новая дата, и 325 в лиде совпадало
с 325 в плитке, читаясь как одна выборка. Заменено на «400 из 400» из ТОЙ ЖЕ
пересборки. Знаменатель 400 — запрошенные сделки, а не 325 сматчившихся:
уверенность считается на каждой оценке.

4. Оговорка уверенности не выводилась вовсе: AccuracyV3 собирал оговорки двух
величин из трёх. Плитка стояла без единого слова о том, почему уверенность
низкая. Добавлена третья.

Тесты проверены фальсификацией: возврат знаменатели одного квартала даёт
«expected 5954 to be 24333» и «expected 5.458 to be less than 2»; снятие
оговорки из вывода — «оговорка не выводится». 157 passed.
This commit is contained in:
bot-backend 2026-08-31 12:59:27 +05:00
parent 2864d80854
commit 79ae87019a
3 changed files with 117 additions and 28 deletions

View file

@ -0,0 +1,68 @@
/**
* Подписи бэктеста обязаны описывать ТО окно, по которому числа посчитаны.
*
* 31.08.2026 числа перевели на представительную выборку (медиана трёх
* пересборок), но подписи остались от прежнего прогона: «сделки II квартала
* 2026 года» и знаменатель 5 954 (только этот квартал). Замер показал, что
* пересборка покрывает ЧЕТЫРЕ квартала и II квартал 2026 лишь 24 % её
* сделок. Доля выборки при старом знаменателе выходила 5,5 % вместо 1,3 %,
* то есть вчетверо льстила.
*
* Отдельно: плитка «уверенность низкая» показывалась БЕЗ своей оговорки
* `AccuracyV3` собирал оговорки только двух величин из трёх.
*/
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { AccuracyV3 } from "../_components/v3/AccuracyV3";
import {
BACKTEST,
BACKTEST_PERIOD_LABEL,
BACKTEST_POPULATION,
BACKTEST_SHARE_LABEL,
} from "../landing-facts";
describe("подписи окна бэктеста", () => {
it("окно названо целиком, а не одним кварталом из четырёх", () => {
// Прежняя подпись называла II квартал 2026 — в представительной выборке
// это 24 % сделок, а не все.
expect(BACKTEST_PERIOD_LABEL).not.toMatch(/^сделки II квартала/u);
expect(BACKTEST_PERIOD_LABEL).toMatch(/четыре квартала/u);
});
it("знаменатель — того же окна, что и выборка", () => {
// 5 954 — только II квартал 2026; окно целиком — 24 333 (замер 31.08.2026).
expect(BACKTEST_POPULATION).toBe(24_333);
});
it("доля выборки выводится и получается около 1,3 %, а не 5,5 %", () => {
const share = (BACKTEST.priceError.sampleN / BACKTEST_POPULATION) * 100;
expect(share).toBeGreaterThan(1.0);
expect(share).toBeLessThan(2.0);
expect(BACKTEST_SHARE_LABEL).toBe("1,3 %");
});
it("уверенность посчитана на том же прогоне, что и остальные числа", () => {
// «325 из 327» было из прогона 29.08 и под общей подписью «замер 31.08»
// приписывало старому числу новую дату.
expect(BACKTEST.confidenceLow.text).not.toMatch(/327/u);
});
});
describe("оговорки в интерфейсе", () => {
it("на экране есть оговорки ВСЕХ ТРЁХ величин блока", () => {
render(<AccuracyV3 stats={{}} showcase={null} />);
for (const набор of [
BACKTEST.priceError.caveats,
BACKTEST.coverage.caveats,
BACKTEST.confidenceLow.caveats,
]) {
for (const line of набор) {
expect(
screen.queryByText(line),
`оговорка не выводится: ${line.slice(0, 48)}`,
).not.toBeNull();
}
}
});
});

View file

@ -114,9 +114,14 @@ export function AccuracyV3({
});
}
// Оговорки ВСЕХ ТРЁХ величин блока, а не двух. `confidenceLow` показывается
// отдельной плиткой, но её оговорка сюда не попадала — то есть на экране
// стояло «400 из 400 — уверенность низкая» без единого слова о том, почему.
// Плитка без своей оговорки — ровно то, что этот блок и не должен делать.
const caveats = [
...BACKTEST.priceError.caveats,
...BACKTEST.coverage.caveats,
...BACKTEST.confidenceLow.caveats,
];
const deals = showcase?.deals ?? [];

View file

@ -97,14 +97,20 @@ export const BACKTEST: Readonly<Record<"priceError" | "coverage" | "confidenceLo
],
},
confidenceLow: {
text: "325 из 327",
sampleN: 327,
// Тот же прогон, что и остальные числа блока: пересборка «mera» от
// 31.08.2026. Раньше здесь стояло «325 из 327» из прогона 29.08 — под
// общей подписью «замер 31.08» это приписывало старому числу новую дату.
// Знаменатель 400 — это ЗАПРОШЕННЫЕ сделки, а не 325 сматчившихся:
// уверенность считается на каждой оценке, включая те, что не дошли до
// сверки с ценой ДКП.
text: "400 из 400",
sampleN: 400,
source: BACKTEST_CONFIDENCE_SOURCE,
caveats: [
"Оценки «уверенность высокая» в выборке не встретилось ни разу.",
"Число — из прогона 29.08.2026 по 327 кластеризованным сделкам; на " +
"представительных пересборках 31.08.2026 уверенность не пересчитывалась, " +
"поэтому знаменатель здесь свой, а не 325 головных величин.",
"«Уверенность высокая» за три пересборки по 400 оценок встретилась один " +
"раз, «средняя» — 0, 6 и 9 раз. Это не скромность: порог требует " +
"разброса цен среди сопоставимых объявлений не выше 24 %, а на замере " +
"31.08.2026 по 60 случайным адресам его медиана — 29 %.",
],
},
};
@ -131,37 +137,47 @@ export const BACKTEST_CORRIDOR_LABEL = "±37 %";
* 01.04.2026). Поэтому «квартал» предельная точность, которую данные вообще
* позволяют назвать; «сделки за апрель» было бы вторым враньём.
*/
export const BACKTEST_PERIOD_LABEL = "сделки II квартала 2026 года";
export const BACKTEST_PERIOD_LABEL =
"сделки за четыре квартала — с III квартала 2025 по II квартал 2026";
/**
* ЧЕГО МЫ НЕ ЗНАЕМ ПРО ОКНО ПРЕДСТАВИТЕЛЬНЫХ ПЕРЕСБОРОК (записано 31.08.2026).
* КВАРТАЛЬНЫЙ СОСТАВ ПРЕДСТАВИТЕЛЬНОЙ ВЫБОРКИ ЗАМЕРЕН 31.08.2026.
*
* Подпись выше проверена на прогоне 29.08 (`ORDER BY id DESC`): там у всех
* 327 сделок стояло `deal_date = 2026-04-01`. Пересборки 31.08 разносят
* выборку по адресам, и квартальный состав их 325 сделок НИКТО не смотрел
* если разнесение идёт по всему окну `--since`, в выборку попадают и другие
* три метки квартала, а тогда и знаменатель `BACKTEST_POPULATION` (5 954
* только II квартал) назван не тот.
* Здесь стояла оговорка «никто не смотрел» с критерием снятия: посчитать
* `deal_date` по сделкам выборки. Посчитано, и ответ поменял подпись.
*
* КРИТЕРИЙ СНЯТИЯ, чтобы это не осталось вечной оговоркой: одним запросом
* посчитать `deal_date` по сделкам представительной выборки. Один квартал
* подпись и знаменатель остаются как есть; больше одного подпись назвать
* окном целиком и взять знаменатель того же окна. До этого числа окна не
* трогались: врать в другую сторону та же ошибка, что чинилась здесь.
* Прогон 29.08 (`ORDER BY id DESC`) брал последние ЗАГРУЖЕННЫЕ строки, и у
* всех 327 сделок стояло `deal_date = 2026-04-01` один квартал. Пересборка
* вразброс покрывает ВСЁ окно `--since`, и её 400 сделок легли так:
*
* III кв 2025 106 (26,5 %) I кв 2026 88 (22,0 %)
* IV кв 2025 110 (27,5 %) II кв 2026 96 (24,0 %)
*
* То есть II квартал 2026 лишь четверть выборки. Прежняя подпись «сделки
* II квартала 2026 года» для этих чисел была бы прямым враньём, а знаменатель
* 5 954 (только II квартал) завышал бы долю вчетверо.
*
* Поэтому подпись называет окно целиком, а знаменатель взят того же окна.
*/
/**
* Сколько сделок было В ТОМ ЖЕ окне чтобы «мы сверили 327 сделок» не
* читалось как «столько их и было». Замер 30.08.2026 на проде: годных под те
* же фильтры выборки (`source='rosreestr'`, geom, ppm² 30 000..600 000, rooms,
* area) сделок Екатеринбурга с `deal_date = 2026-04-01` 5 954.
* Сколько сделок было В ТОМ ЖЕ окне чтобы «мы сверили 325 сделок» не
* читалось как «столько их и было».
*
* ЗНАМЕНАТЕЛЬ БЕРЁТСЯ ИЗ ТОГО ЖЕ ОКНА, что и выборка. Соблазн подставить сюда
* 24 333 (все сделки ЕКБ с июня 2025) даёт долю красивее 1,3 % вместо
* 5,5 %, но это ровно та же ошибка, что чинится выше: доля от окна, которое
* замер не покрывал.
* Замер 31.08.2026 на проде, ровно те же фильтры, что у выборки бэктеста
* (`source='rosreestr'`, geom, ppm² 30 000..600 000, rooms, area > 0,
* `deal_date >= 2025-06-01`, город Екатеринбург): **24 333** сделки.
*
* ЗНАМЕНАТЕЛЬ БЕРЁТСЯ ИЗ ТОГО ЖЕ ОКНА, ЧТО И ВЫБОРКА правило не изменилось,
* изменилось окно. Раньше здесь стояло 5 954 (только II квартал 2026), и это
* было верно для прогона 29.08, который в один квартал и укладывался. Для
* пересборок вразброс, покрывающих четыре квартала, тот же знаменатель дал бы
* долю вчетверо больше реальной 5,5 % вместо 1,3 %.
*
* Обе величины проверены одним запросом: в окне 24 333, из них с
* `deal_date = 2026-04-01` 5 954.
*/
export const BACKTEST_POPULATION = 5_954;
export const BACKTEST_POPULATION = 24_333;
/**
* Доля выборки. ВЫВОДИТСЯ, а не вписывается: разъехаться с `sampleN` не может.