Веб-сервис для мониторинга состояния сельскохозяйственных полей по спутниковым данным: выбор региона и полигона на карте, автоматический сбор спутниковых и метеоданных, восстановление пропусков во временном ряде NDVI и детекция аномальных периодов вегетации.
Запуск всего продукта одной командой: docker compose up --build → http://localhost:8000/
Стек. Бэкенд: Python 3.14, FastAPI, LightGBM, PyTorch, odc-stac/rasterio, pandas. Интерфейс: React 19,
Vite, TypeScript, MUI X Charts, AntV L7 (карта на тайлах OpenStreetMap), GSAP, TanStack Query.
Окружение и зависимости: uv (pyproject.toml + uv.lock) и npm (web/package-lock.json), образ — Docker.
Ключи, регистрации и обучение моделей не нужны: веса лежат в репозитории, внешние каталоги данных открытые.
docker compose up --build # 1. весь продукт → http://localhost:8000/
docker compose exec app uv run --no-sync python -m gapfill.predict_improved --output artifacts/submission.csv --device cpu # 2. batch-инференсБез Docker: uv sync --group service --group torch, затем cd web && npm ci && npm run build && cd .. и
uv run uvicorn service.app:app --port 8000. Проверить метрику задачи 1 за доли секунды —
uv run --no-sync python -m gapfill.metrics. Что смотреть по шагам: экран «Новая территория» (карта →
контуры OSM или свой полигон → сбор данных), затем экран «Поле» (ряд NDVI, восстановленные точки, эпизоды и
объяснения). Слайды защиты — presentation/index.html.
Полное ТЗ, критерии оценки, чек-лист сдачи и отчёты — в папке docs/. Рабочие заметки для команды и Claude Code — в CLAUDE.md.
- Постановка задачи
- Теория и глоссарий
- Данные и формат submission
- Метрика оценки
- Технические требования
- Критерии оценки
- Чек-лист сдачи
- Отчёт EDA, обзор open-source, сравнение двух EDA, заметки первого EDA
- Модель восстановления пропусков, детекция аномалий, исследовательский отчёт, вопросы к экспертам
- Улучшенная модель: эксперименты, проверка и отдельные веса
uv sync # Python 3.14 + зависимости из pyproject.toml / uv.lock
uv run python -m eda.run_all # графики в reports/eda/figures/, числа в reports/eda/summary.jsonВыводы и графики — в docs/08-eda-report.md. Скрипты первого прохода анализа и сборка дашборда — в eda/v1/.
Интерактивный дашборд «NDVI-атлас полей»: reports/dashboard/dashboard.html (самодостаточный файл, открывается двойным кликом; пересборка uv run python eda/v1/build_dashboard_data.py).
Пакет gapfill/: признаки по ряду полигона, циклам съёмки и «шуму дня» других полигонов,
LightGBM + нейросеть по сезонной сетке (SeasonNet), смесь 0.5/0.5. Метод, валидация и результаты —
в docs/12-gapfill-model.md, журнал экспериментов — в experiments/.
Входные данные. data/test_features_new.csv — вторая версия private_features.csv (обновление организаторов
2026-09-05: 20 полигонов, история 2010–2024, 2 323 контрольные точки); по ней считается метрика. Первая версия
data/test_dataset.csv больше не оценивается и используется только как дополнительные известные точки
(параметр --extra; чтобы отключить — --extra без значений). Сравнение версий — docs/03.
Выход — submission.csv (anon_polygon_id,date,primary_ndvi_true — так требует платформа проверки, в ТЗ было primary_ndvi_pred, только строки is_synthetic_gap = True).
Инференс сданной модели (ничего обучать не нужно, веса models/improved/ лежат в репозитории):
uv sync --frozen --no-default-groups --group infer --group torch
uv run --no-sync python -m gapfill.predict_improved --output submission.csv --device cpu # 2 323 строки
uv run --no-sync python -m gapfill.metrics # RMSE и GapScore за 0.05 сОбучение финальной модели с нуля (exp-008; на RTX 5060 Ti около полутора часов, подробности и промежуточные проверки — docs/16):
UV_TORCH_BACKEND=cu130 uv sync --group ml --group dl
# 1. подбор и проверка на отложенной маске 777
uv run --no-sync python -m gapfill.research.train --val-seed 777 --n-masks 30 --modes all --kriging --rounds 8500 --threads 5 --out kriging30_777
uv run --no-sync python -m gapfill.research.nn --kind residual --epochs 260 --schedule-epochs 400 --fixed-epoch 260 --hidden 96 --val-seed 777 --out nn_residual777
uv run --no-sync python -m gapfill.research.uncertainty --model artifacts/research/kriging30_777/all/model.txt --n-masks 30 --val-seed 777 --kriging --out uncertainty_kriging777
uv run --no-sync python -m gapfill.research.blend --model artifacts/research/kriging30_777/all/model.txt --nn nn_residual777 --val-seed 777 --weights 0.6 --sigma 0.04 --calibration posterior --uncertainty artifacts/research/uncertainty_kriging777/model.txt --out posterior_blend777
# 2. обучение на всех известных точках и сборка пакета весов models/improved/
uv run --no-sync python -m gapfill.research.final --source artifacts/research/kriging30_777/all/result.json --n-masks 30 --rounds 8500 --seeds 42 137 --threads 5 --out final_kriging --uncertainty
uv run --no-sync python -m gapfill.research.nn --kind residual --epochs 260 --schedule-epochs 400 --hidden 96 --final --seed 0 --out final_nn_s0
uv run --no-sync python -m gapfill.research.package --lgb final_kriging --nn final_nn_s0 --calibration posteriorПредыдущая конфигурация exp-007 (смесь LightGBM + SeasonNet 0.5/0.5, веса в models/) обучается так:
uv run python -m gapfill.train --n-masks 20 --clip -0.1 1.0 --out lgb_v4 # валидация LightGBM (RMSE 0.055)
uv run python -m gapfill.nn_model --epochs 600 --dropout 0.25 --out nn_v4 # валидация SeasonNet (RMSE 0.059, GPU)
uv run python -m gapfill.ensemble lgb_v4 nn_v4 # смесь на валидации (RMSE 0.054)
uv run python -m gapfill.predict --input data/test_features_new.csv --output submission.csv --n-masks 30 --rounds 5500 --seeds 0 1 2 --out final_lgb
for s in 0 1 2 3 4; do uv run python -m gapfill.nn_model --final --epochs 600 --dropout 0.25 --seed $s --out final_nn; done
uv run python -m gapfill.make_submission final_lgb:0.5 final_nn:0.5 # → submission.csvСвой файл private_features.csv. Калибровка привязана к опубликованным организаторами историческим
агрегатам и проверяет контрольные суммы исходных train/extra, поэтому на другом наборе данных запускайте
инференс без неё, иначе будет ValueError:
uv run --no-sync python -m gapfill.predict_improved --no-calibration --input <ваш_файл>.csv --output submission.csv --device cpuSeed 42 (LightGBM — дополнительно 137), гиперпараметры финала: LightGBM l2, lr 0.03, 63 листа,
min_data_in_leaf 40, feature_fraction 0.6, 8 500 итераций, 427 признаков; ResidualSeasonNet — 96 каналов,
260 эпох, расписание на 400. Проверка окружения — uv run pytest tests -q; полный набор из 182 тестов проходит на окружении
uv sync --group infer --group serve --group geo --group agent --group torch (без группы geo падают
тесты сбора, без agent — тесты агента, без собранного web/dist — тест клиентских маршрутов).
Готовый submission.csv лежит в корне — это предсказания финальной модели exp-008
(для первой версии test — reports/gapfill/submission_v1_test_dataset.csv,
предыдущая версия — reports/gapfill/submission_prev_exp007.csv).
Результаты на отложенной выборке (15 % известных точек скрыты как контрольные, 9 199 точек; это не приватный лидерборд):
| Вариант | RMSE | GapScore |
|---|---|---|
| baseline «среднее двух соседей» (ТЗ) | 0.093 | 2.1 |
| ансамбль exp-007 (LightGBM + SeasonNet, 0.5/0.5) | 0.0553 | 13.41 |
| финал exp-008 (kriging-признаки + остаточная сеть + калибровка) | 0.0437 | 16.88 |
Разброс по трём маскам (seed 2026 / 777 / 999) — 0.0036. Пересчёт метрики — uv run python -m gapfill.metrics.
Отдельный пакет models/improved/ содержит LightGBM с 427 признаками и ResidualSeasonNet.
Исходные models/ и submission.csv сохранены; веб-сервис продолжает использовать прежние артефакты.
Разбор обучения, контрольных масок и ограничений — отчёт.
uv sync --frozen --no-default-groups --group infer --group torch
uv run --no-sync python -m gapfill.predict_improved --output submission.csv --model-only-output submission_model.csv
# Только обычный ML, без опубликованных исторических агрегатов:
uv run --no-sync python -m gapfill.predict_improved --no-calibration --output submission_model.csvКакой файл сдаётся. Конкурсный — submission.csv (с калибровкой, GapScore 16.88 на отложенной выборке).
Рядом лежит submission_model.csv — тот же ансамбль без калибровки (13.41): его отправляем, если организаторы
сочтут калибровку недопустимой. Калибровка использует опубликованные ими же исторические mean/std из первой
версии data/test_dataset.csv; эти агрегаты несут информацию о скрытых значениях второй версии, поэтому
приём калибровки — вопрос к организаторам (docs/15), а не техническое
ограничение. На новые поля она не переносится: инференс на чужом файле запускается с --no-calibration.
Про «невозможные» значения. В калиброванном файле есть два предсказания вне диапазона [−0.1, 1]
(минимум −0.15, максимум 2.03). Это не ошибка формата: в самих данных кейса есть истинные primary_ndvi
до 1.84 и до −2.13 (артефакты съёмки, попавшие в разметку), а калибровка по агрегатам как раз и восстанавливает
такие выбросы. Обрезка проверена на отложенной выборке и только ухудшает метрику: [−0.5, 1.0] даёт 16.80,
[−0.1, 1.0] — 16.67 против 16.88 без обрезки, потому что рядом с этими прогнозами стоят такие же
выбросы в ответах. Локальные метрики и контрольные прогнозы — в
reports/gapfill/improvement/.
На macOS, если LightGBM не находит libomp.dylib, перед запуском:
export DYLD_LIBRARY_PATH="$PWD/.venv/lib/python3.14/site-packages/torch/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"Пакет anomaly/: гармонизированная кривая сезона, норма по истории полигона (или по культуре),
эпизоды «устойчивого и/или сильного» отклонения (Z < −1), причина по правилам с погодой ERA5, фенологией и
региональным контекстом, текст на русском по правилам. Интерфейс показывает эпизоды и объяснения по правилам.
Метод и проверка — в docs/13-anomaly-detection.md, результаты — в
reports/anomalies/ (episodes.csv, seasons.csv, figures/).
uv run python -m anomaly.run # все полигоны → reports/anomalies/
uv run python -m anomaly.evaluate reports/anomalies # прокси-метрики детектора
uv run python -m anomaly.plots AOI-0065:2024 AOI-0043:2019 # графики сезонов с эпизодамиdocker compose up --build # соберёт интерфейс и бэкенд, поднимет http://localhost:8000/Образ собирается в два этапа: node:24-alpine собирает интерфейс (web/), затем образ uv с Python 3.14
ставит зависимости (infer, geo, serve плюс CPU-сборка torch) и получает готовую статику. Поля,
добавленные пользователем, лежат в именованном томе и переживают перезапуск. Нужен только интернет для внешних
каталогов данных; ключи и регистрация не требуются.
Batch-инференс в том же контейнере:
docker compose exec app uv run --no-sync python -m gapfill.predict_improved --output artifacts/submission.csv --device cpuНа экране поля есть панель «Спросить про поле»: вопрос своими словами, ответ — по тем же числам,
что показаны на экране. С ключом OLLAMA_API_KEY (uv sync --group agent) отвечает языковая модель Ollama Cloud,
у которой есть три инструмента: сводка сезона, периоды снижения и метод. Ключ кладётся в .env
(см. .env.example), модель по умолчанию — gemma4:31b. Без ключа отвечает разбор
по правилам — теми же фактами, только без связного текста. Модель не считает NDVI и не видит сырых данных.
Кнопка «Отчёт для печати» открывает GET /api/report/{pid} — самодостаточный HTML без внешних ссылок,
с графиком сезона в SVG. Он свёрстан под печать, поэтому PDF получается прямо из браузера:
«Печать» → «Сохранить как PDF».
Объяснения периодов снижения тоже пишет модель, но не в момент анализа: она отвечает
десятками секунд на эпизод, поэтому анализ отдаёт текст по правилам сразу, а объяснения
догружаются фоном (GET /api/explanations/{pid}?year=) и подменяются в карточках, когда готовы.
Те же данные доступны любому клиенту MCP — например, чтобы спрашивать про поля прямо из Claude Code:
claude mcp add vegetation -- uv run --no-sync python -m mcp_serverШесть инструментов: list_fields, field_summary, find_episodes, ask_about_field,
field_report, solution_metrics. Сбор данных по новой территории в MCP намеренно не вынесен:
он идёт минуты и требует сети, для него есть POST /api/analyze.
uv run --no-sync python -m gapfill.metricsСчитает RMSE и GapScore по сохранённым предсказаниям отложенной выборки
(reports/gapfill/improvement/validation_*.csv) — без обучения и инференса, за доли секунды.
Обновляет reports/gapfill/validation.json, откуда числа берёт интерфейс.
Заголовочная цифра — модель с калибровкой историческими агрегатами, рядом всегда показывается
результат чистого ансамбля. Это отложенная выборка, а не приватный лидерборд.
uv sync --group ml --group geo --group service --group torch # FastAPI, STAC-клиенты, rasterio, Open-Meteo, torch (CPU)
uv run python -m anomaly.run # один раз: эпизоды для полигонов кейса
cd web && npm ci && npm run build && cd .. # интерфейс (Node 20+); без этого шага откроется резервный HTML
uv run uvicorn service.app:app --host 127.0.0.1 --port 8000Открыть http://127.0.0.1:8000/. Для разработки интерфейса отдельно: cd web && npm run dev (порт 5173,
запросы /api проксируются на 8000). Если сборки web/dist нет, сервис отдаёт простой резервный интерфейс
на одном HTML-файле (он же всегда доступен по адресу /legacy).
React 19 + Vite, графики — MUI X Charts, карта — AntV L7 (WebGL) на тайлах OpenStreetMap, анимации — GSAP.
Иконочных шрифтов нет: все иллюстрации нарисованы в проекте (web/src/components/art/). Экраны:
| Экран | Что показывает |
|---|---|
| Обзор | Метрики решения, причины угнетения по всем полям, список полей кейса с поиском и фильтром |
| Поле | Сезон по годам: наблюдения по сенсорам, восстановленная кривая, норма ±1σ, восстановленные контрольные точки, Z-score, погода ERA5, эпизоды с объяснением |
| Новая территория | Карта, поиск контуров OSM, рисование полигона, сбор данных, набор «Мои поля» |
| Аномалии | Мои поля / поля кейса / все: сезон, поиск по имени, компактный список полей с раскрытием эпизодов и переходом к периоду на графике |
| Как это работает | Пайплайн, метрики обеих задач, источники данных |
Альтернативный запуск бэкенда — uv run --locked --group service python -m service: тот же сервер плюс
проверка зависимостей автосбора при старте, готовность — GET /api/health. Повторный запуск без синхронизации —
uv run --no-sync python -m service. Эпизоды полигонов кейса уже включены в репозиторий; пересчёт при
необходимости — uv run --no-sync python -m anomaly.run.
Резервный интерфейс берёт графики из установленного пакета Plotly через /vendor/plotly.min.js.
Поиск контуров OSM не импортирует спутниковый сборщик. Для Sentinel-2 выбираются публичные HTTPS COG;
JP2-дубликаты и сцены, доступные только через S3 с авторизацией, исключаются до чтения.
При отказе Overpass поиск переключается между публичными серверами VK Maps, Private.coffee и overpass-api.de;
успешные ответы по рамке карты кэшируются до пяти минут. При слишком широком обзоре кнопка поиска
сама приближает карту вокруг её центра до допустимого размера области.
На /anomalies по умолчанию открываются Мои поля из «Новой территории» и последний доступный
сезон. Один ряд — одно поле; внутри можно выбрать эпизод, прочитать возможную причину и рекомендацию.
Числа и методика скрыты под раскрытием. «Показать период на графике» открывает правильный отчёт,
год и временное окно. Источник, сезон, поиск и фильтр сохраняются в URL и восстанавливаются после «Назад».
Поля без найденных отклонений остаются в списке; отсутствие сезона или оценки обозначается отдельно.
GET /api/anomaly-fields?source=mine|case|all&year=2025 возвращает {years, year, fields}.
Год необязателен (последний доступный для источника), источник по умолчанию mine. Каждое поле
содержит имя, pid, uid, источник, доступные годы, has_season, статус level и эпизоды выбранного
года. Сохранённые отчёты читаются из того же набора, что карта, с удалением дублей по pid;
спутниковый сбор и пересчёт модели не запускаются. Статусы: high, medium, data, context,
unknown, clear. Историческая аномалия не означает текущую угрозу; причина остаётся гипотезой.
В «Новая территория» введите город и улицу, полный адрес, населённый пункт, область, название объекта
или координаты 47.22, 39.72 (широта, долгота), затем нажмите Enter или «Найти». Выберите один из
результатов: карта покажет место и его окрестности, в адресе видны район и регион. Улица может
состоять из нескольких участков и проходить через разные районы. После перехода выберите готовый
контур поля или нарисуйте его. Найденный адрес не превращается в сельскохозяйственный полигон.
API: GET /api/places?q=..., от 2 до 200 символов. Ответ — список {id, label, center, bbox, address, kind};
center — [долгота, широта], bbox — [запад, юг, восток, север] или null для координат.
Ошибки: 400/422 — некорректный ввод, 429 — повторить через две секунды, 502 — источник недоступен.
Поиск работает через Nominatim на бэкенде; координаты обрабатываются локально. Согласно
правилам публичного Nominatim, запросы
отправляются только по действию пользователя, без автодополнения; общий SQLite-лимит — один запрос
за 1.1 секунды, кэш — до 512 запросов на 24 часа. Переменные NOMINATIM_URL (полный URL /search)
и GEOCODING_USER_AGENT позволяют заменить провайдера и идентификацию без изменения кода;
они также передаются через Docker Compose. GEOCODING_CACHE задаёт путь к SQLite, по умолчанию
artifacts/service/geocoding.sqlite. Все процессы должны использовать один файл кэша;
для нескольких машин нужен собственный провайдер или общий прокси с ограничением запросов.
Проверки и запуск e2e — tests/e2e/README.md.
Два сценария из ТЗ:
- Готовый полигон: в «Новая территория» кнопка «Найти поля OSM в видимой области» запрашивает
landuse=farmlandиз OpenStreetMap (Overpass API), клик по контуру выбирает поле. - Произвольный полигон: контур рисуется на карте (
@antv/l7-draw).
После выбора «Собрать данные и проанализировать» сервис сам получает Sentinel-2 L2A, Landsat 8/9 C2 L2 и MODIS MOD13Q1
(Planetary Computer STAC; для Sentinel-2 маска облаков по SCL, запасной каталог — Earth Search), ERA5 (Open-Meteo,
по центроиду поля), строит ряд primary_ndvi по приоритету S2 → Landsat → MODIS, восстановленную кривую,
норму по собственной истории поля, находит эпизоды угнетения и объясняет их. Четыре источника грузятся
одновременно, внутри каждого — по восемь сезонов параллельно; из каталогов берутся сцены с облачностью
до 60 % (более облачные всё равно не проходят порог чистых пикселей). GDAL настроен под чтение COG из облака
(configure_rio(cloud_defaults=True): без листинга каталога на каждую сцену, с объединением диапазонов),
это главное ограничение скорости: сотни HTTP-запросов к хранилищам в США. Ход сбора виден в интерфейсе:
клиент задаёт идентификатор задания (job) и опрашивает GET /api/analyze/progress/{job}, где сервер
отдаёт число загруженных сезонов и сцен по каждому источнику, этап и последние строки журнала
(файл artifacts/service/progress/<job>.json, который пишет процесс сборщика). Если источник недоступен,
ряд строится по остальным, а в интерфейсе выводится, что именно собрано.
Проанализированные поля попадают в набор «Мои поля» (artifacts/service/polygons/*.json, не в git): их можно
открыть без повторного сбора данных и удалить; на карте контур раскрашен по состоянию последнего сезона
(зелёный — норма, оранжевый — умеренное угнетение, красный — критическое), клик по контуру открывает поле.
Полигоны кейса (78 анонимных AOI-xxxx, координат нет) показываются списком: исходные наблюдения по сенсорам,
восстановленные контрольные точки из submission.csv (и из reports/gapfill/submission_*.csv для первой версии test),
кривая, норма ±1σ, Z-score, погода ERA5 и эпизоды с причинами.
API: GET /api/health, GET /api/polygons, GET /api/polygon/{pid}, GET /api/episodes?year=&cause=&severity=, GET /api/summary,
GET /api/meta, GET /api/fields?bbox=юг,запад,север,восток, POST /api/analyze ({"geometry": <GeoJSON Polygon>, "name": "...", "start_year": 2019, "end_year": 2025}), набор пользователя GET /api/user-polygons,
GET|DELETE /api/user-polygons/{uid}. Объяснение эпизодов языковой моделью включается переменной окружения
OLLAMA_API_KEY (uv sync --group agent); без неё текст формируется по правилам.
Экран поля начинается со «светофора»: зелёная, жёлтая или красная плашка с заголовком («Всё в норме»,
«Стоит присмотреться», «Поле отстаёт») и одной фразой-выводом, например «Поле сильно отстаёт с 15 авг
по 28 сен: скорее всего из-за засухи или жары». Под ней три вопроса: «Как поле сейчас?», «Что было в этом
сезоне?», «Что делать?». Карточки показателей пишут крупно фразу («Дождей за месяц вдвое меньше нормы»,
«Сезон опережает обычный примерно на 8 дней»), а число и отклонение от нормы — мелким шрифтом.
Карточка эпизода начинается с фразы, сравнения с соседями словами и совета; Z, уверенность в процентах,
аргументы и технический текст свёрнуты в «подробности для агронома». Все формулировки собираются на
клиенте из уже посчитанных данных (web/src/lib/plain.ts, плашка — components/panels/FieldVerdict.tsx);
детектор и модель не меняются. Уверенность переводится в слова: ≥ 0.8 «почти наверняка», ≥ 0.6 «скорее
всего», иначе «возможно». Разница накопленного тепла переводится в дни делением на 20 °C·дней в сутки.
На экране поля слева — графики NDVI и погоды с переключателем, справа — периоды, когда поле отставало. Сверху — светофор поля, последнее наблюдение NDVI, осадки, накопленное тепло и сухой период. Панели прокручиваются внутри экрана; карта Sentinel-2 раскрывается под NDVI. Выбор контура и список сохранённых полей доступны на вкладке «Новая территория», включая поля прежнего интерфейса. Наведение связывает даты двух графиков и показывает значения в карточках. Кнопка в эпизоде приближает соответствующий период на обоих графиках; «Весь сезон» возвращает исходный масштаб.
Режимы погодного графика:
- Осадки за 30 дней, мм, со сравнением по прошлым годам. Под графиком — самый длинный сухой период сезона.
- Накопленное тепло: сумма
max(Tср − 10 °C, 0)от 1 апреля, °C·дни. Это общий показатель тепла сезона, без автоматического определения культуры или даты посева. Форма не требуется.
Сухой период — дни подряд с осадками менее 1 мм; пропуски разрывают серию. Если собрана ET₀, первый график можно переключить на осадки минус испарение за 30 дней.
Серая линия — среднее предыдущих лет (до 30), диапазон — 10–90-й процентили. Текущий год исключён, сравнение идёт по календарным датам. Показатели не изменяют детектор аномалий и модель NDVI. ET₀ учитывает температуру, радиацию, влажность и ветер; водный баланс не равен влажности почвы или потребности в поливе.
Источник: ERA5 через Open-Meteo, полные годы,
средняя/минимальная/максимальная температура, осадки и ET₀. Пропуски остаются неизвестными.
Для анонимных AOI используются имеющиеся средняя температура и осадки. ET₀ и Tmin/Tmax не выдумываются.
Если погоды нет вообще, погодная часть скрывается. Z-score, исходные спутниковые точки, контрольные
восстановления модели и температура/осадки по дням доступны в «Подробности для агронома: данные и расчёт». Основные наблюдения NDVI
приведены к шкале Sentinel-2; исходные значения доступны при наведении и в подробностях.
Объяснения эпизодов свёрнуты и формулируются как гипотезы. Формулы и сам детектор не изменены.
Собранные поля доступны в прежнем списке по названию; отчёты и параметры графиков сохранены в
artifacts/fields/, погодный кэш — в artifacts/weather/.
API графиков: GET /api/polygon/{pid}/agro?year=2025, POST /api/polygon/{pid}/agro
(year, profile, sowing_date, base), POST /api/polygon/{pid}/weather-refresh.
Формулы и границы изменений — в плане дополнения графиков.
E2E-проверки через Playwright: tests/e2e/README.md.
Дек защиты — presentation/index.html: 14 слайдов (проблематика, данные и EDA,
baseline и его ограничения, эксперименты, результат задачи 1, логика детекции аномалий, три показательных
сезона, пользовательский путь, автосбор, архитектура, ограничения и итог). Открывается в браузере без сборки
и без интернета, ходит стрелками/пробелом, P — печать в PDF (один слайд = одна страница A4-landscape).
uv run python -m http.server 8899 # затем http://127.0.0.1:8899/presentation/index.htmlЧисла на слайдах взяты из reports/gapfill/validation.json и reports/anomalies/; графики сезонов —
копии из reports/anomalies/figures/ в presentation/img/.
Базовые зависимости ставятся uv sync. Остальное разбито на группы в pyproject.toml (все версии актуальны на сентябрь 2026 и имеют wheels под Python 3.14):
| Группа | Что внутри | Команда |
|---|---|---|
ml |
scikit-learn, LightGBM, CatBoost, XGBoost, statsmodels, whittaker-eilers, optuna, shap | uv sync --group ml |
infer |
только LightGBM и scikit-learn — минимум для инференса и детекции, эту группу берёт образ Docker | uv sync --group infer |
dl |
torch, PyPOTS, pygrinder, chronos-forecasting (эксперименты) | uv sync --group dl |
torch |
только torch — хватает для инференса SeasonNet из models/ |
uv sync --group torch |
geo |
pystac-client, odc-stac, stackstac, planetary-computer, rasterio, rioxarray, xarray, geopandas, shapely, earthengine-api, openmeteo-requests, osmnx, overpy | uv sync --group geo |
openeo |
клиент Copernicus Data Space (конфликтует с geo по xarray) |
uv sync --group openeo |
serve |
только веб-слой: FastAPI, uvicorn, pydantic, httpx, Plotly, duckdb — эта группа идёт в образ Docker | uv sync --group serve |
service |
всё для разработки сервиса: serve + geo + ml |
uv sync --group service |
agent |
pydantic-ai, anthropic, mcp | uv sync --group agent |
dev |
ruff, pytest, pytest-cov, mypy | ставится по умолчанию |
Индекс сборки torch задаётся переменной UV_TORCH_BACKEND: UV_TORCH_BACKEND=cu130 uv sync --group dl — сборка
под CUDA 13.0 (RTX 5070, для обучения нейросети), UV_TORCH_BACKEND=cpu uv sync --group torch — CPU. В образе
Docker torch ставится из индекса CPU отдельной строкой, чтобы в контейнер не попадали пакеты nvidia-*.
Обзор моделей, источников данных и обоснование выбора — в docs/09-open-source-landscape.md.
Системные зависимости: только uv (сам ставит Python 3.14 по .python-version); GDAL/PROJ приходят внутри wheels
rasterio/pyproj, отдельная установка не нужна. GPU не обязателен: инференс gapfill.predict_saved, сервис и
детекция аномалий работают на CPU; обучение SeasonNet на GPU (CUDA 13.0) занимает ~8 минут на seed, на CPU — дольше.
Внешние сервисы (Planetary Computer, Earth Search, Open-Meteo, Overpass) — без ключей; единственная необязательная
переменная окружения — OLLAMA_API_KEY.
.
├── README.md
├── CLAUDE.md # рабочие заметки по проекту, окружению и данным
├── data/
│ ├── train_dataset.csv # обучающий датасет (99 955 строк)
│ ├── test_features_new.csv # тестовый датасет = private_features.csv, вторая версия (49 190 строк, 2 323 контрольные точки)
│ └── test_dataset.csv # первая версия test (57 185 строк): только дополнительные известные точки
├── docs/ # ТЗ, критерии, чек-лист, отчёты 08–19, исходные PDF в source/
├── eda/ # модули разведочного анализа (uv run python -m eda.run_all)
│ └── v1/ # скрипты первого прохода EDA и сборка дашборда
├── gapfill/ # восстановление primary_ndvi: признаки, LightGBM, SeasonNet, смесь, submission
│ карта модулей — gapfill/README.md
├── anomaly/ # детекция и интерпретация аномалий: кривые, нормы, эпизоды, погода, причины
│ карта модулей — anomaly/README.md
├── service/ # бэкенд: FastAPI (app.py), данные (data.py), сбор (collect.py), набор полигонов (polygons.py), сводка (meta.py), резервный UI (static/)
│ карта модулей — service/README.md
├── mcp_server/ # MCP-инструменты поверх service/facts.py (claude mcp add vegetation)
├── web/ # интерфейс: React 19 + Vite, MUI X Charts, AntV L7, GSAP (src/pages, src/components)
├── presentation/ # дек защиты: index.html (14 слайдов), style.css, deck.js, img/
├── scripts/ # вспомогательные скрипты (замер скорости автосбора)
├── experiments/ # журнал экспериментов (exp-000…008 задача 1, exp-100…105 задача 2, сервис и интерфейс)
├── tests/ # pytest: ядро gapfill, детектор аномалий, набор полигонов сервиса
├── models/ # готовые веса (~103 МБ): LightGBM (gzip) + SeasonNet (.pt), инференс без обучения
│ └── improved/ # веса финальной модели exp-008 (gapfill.predict_improved)
├── Dockerfile # образ всего продукта: сборка интерфейса + сервис (uv, CPU)
├── docker-compose.yml # запуск одной командой: docker compose up --build
├── submission.csv # предсказания контрольных точек test (задача 1, вторая версия test)
├── reports/
│ ├── eda/ # графики и summary.json, генерируются EDA
│ ├── dashboard/ # template.html + собранный dashboard.html
│ ├── gapfill/ # submission для первой версии test (сверка с ответами организаторов)
│ └── anomalies/ # эпизоды, фенометрики сезонов, графики (генерирует anomaly.run)
├── artifacts/ # модели и кэш признаков (не в git)
├── pyproject.toml # зависимости по группам (uv)
└── uv.lock
Структура и статистика датасетов описаны в docs/03-data.md.