Skip to content

Repository files navigation

passenger-flow

Информационная система прогнозирования квартального пассажиропотока станций Московского метрополитена. Бэкенд на FastAPI, интерфейс на Streamlit, шесть моделей прогноза за одним интерфейсом.

Работа выполнена как ВКР, защищена в 2026 году.

Задача

Есть открытые данные по входящему пассажиропотоку станций метро: одна точка на станцию в квартал, 2021-Q1 — 2026-Q1. Нужно прогнозировать поток на несколько кварталов вперёд и сравнивать между собой разные подходы к прогнозу на одинаковых входных данных.

Сложность в самих данных. Пять лет квартальных наблюдений — это 21 точка на станцию. Классические модели временных рядов на такой длине работают плохо: сезонность занимает четыре точки из двадцати одной, а на период 2021–2022 приходится провал и восстановление после ковида, который выглядит как структурный сдвиг, а не как сезонное колебание.

Отсюда основное архитектурное решение: модели делятся на два вида. Baseline обучается на одной станции по её собственному ряду. Panel обучается один раз на всех станциях сразу, а станция передаётся признаком или эмбеддингом. Panel-модель компенсирует короткий ряд шириной выборки: 263 станции по 21 точке дают достаточно данных для рекуррентной сети, тогда как на одной станции она бы не обучилась.

Архитектура

backend/
  api/              FastAPI: роутеры, схемы запроса и ответа, обработчики ошибок
  application/      сценарии: forecasting, training
  domain/           сущности, доменные исключения
  infrastructure/   репозитории, SQLAlchemy ORM, сессии
  ml/               модели прогноза, реестр, препроцессинг, метрики
  migrations/       Alembic
  config.py         pydantic-settings
frontend/           Streamlit: страницы, клиент API, тема
  assets/           app.css, motion.js
experiments/        исследовательская часть: прогоны моделей, результаты в json
scripts/            загрузка датасета в базу
docker/             образы и compose
deploy/             развёртывание на один сервер
tests/

Зависимости направлены внутрь: api → application → domain, infrastructure → domain. Слой domain не импортирует ни фреймворк, ни драйвер базы, ни библиотеки моделей — только стандартную библиотеку. Слой application работает с репозиториями и реестром моделей через их интерфейсы и ничего не знает о том, что под ними Postgres и PyTorch.

Модели описаны двумя протоколами в backend/ml/models/base.py: BaselineForecaster принимает ряд одной станции, PanelForecaster принимает панель со всеми станциями и идентификатор нужной. Добавление седьмой модели — это новый класс, реализующий протокол, и одна ветка в backend/ml/registry.py. Ни application, ни api при этом не меняются.

Реестр импортирует реализации лениво: процесс, которому нужна только наивная модель, не тянет PyTorch и XGBoost. Обучение и инференс уходят в asyncio.to_thread, чтобы не блокировать цикл событий FastAPI.

Модели и результаты

Baseline: наивная сезонная, SARIMA, Prophet. Panel: XGBoost, LSTM, GRU.

Тест — 2025-Q1 … 2026-Q1 (5 кварталов), обучение на всём, что раньше. Сводные метрики по всем станциям:

модель n MAE RMSE MAPE
LSTM panel 1046 174 172 262 750 9.84% 0.958
GRU panel 1046 181 933 266 325 10.22% 0.957
XGBoost panel 1078 176 586 289 173 12.38% 0.949

Baseline-модели считались отдельно, на пяти станциях с наибольшим потоком. Там средний MAPE: наивная сезонная 9.27%, Prophet 12.40%, SARIMA 22.92%.

Три вывода, которые стоит читать вместе с таблицей.

Первый. GRU практически не отличается от LSTM (10.22% против 9.84% MAPE). Разница меньше, чем разброс между прогонами, поэтому утверждать, что одна архитектура лучше другой на этих данных, нельзя. Вторая сеть добавлена, чтобы это показать, а не чтобы улучшить результат.

Второй. Наивная сезонная модель на топ-5 станциях не уступает LSTM по MAPE. На коротком ряде с сильной годовой сезонностью «взять значение год назад» — сильный ориентир, и это нормальный результат, а не ошибка эксперимента. Ценность panel-моделей проявляется на всём наборе станций, а не на самых крупных.

Третий. Высокий R² в таблице получен по всем станциям сразу и в основном отражает то, что модель различает крупные и мелкие станции. На отдельных станциях R² уходит в минус (на ВДНХ около −0.68 у LSTM) — внутри одной станции модель предсказывает хуже, чем её собственное среднее. Метрика по панели льстит, поэтому в интерфейсе метрики показываются и по станции тоже.

Воспроизведение: python experiments/run_experiment.py и python experiments/run_gru.py, результаты пишутся в experiments/last_run.json и experiments/gru_run.json.

Стек

Python 3.11, FastAPI, Streamlit, SQLAlchemy 2.0 (async), Alembic, PostgreSQL 15, Docker Compose. Модели: PyTorch, XGBoost, statsmodels, Prophet.

Запуск

Нужны Python 3.11+, PostgreSQL 14+ (или Docker), uv, make.

make install
cp .env.example .env

docker compose -f docker/docker-compose.yml up -d db
make migrate
make load CSV=data/passenger_flow_full.csv
make run

API: http://localhost:8000/docs Интерфейс: http://localhost:8501

Таргеты Makefile сами выставляют PYTHONPATH, так что импорты backend.* и frontend.* работают в любом окружении.

Полный стек в контейнерах: docker compose -f docker/docker-compose.yml up --build.

API

метод путь назначение
GET /api/lines линии метро
GET /api/stations станции, фильтр по линии
GET /api/stations/{station_id}/flow исторический ряд станции
GET /api/models обученные модели
GET /api/models/{model_id} модель по идентификатору
POST /api/models/train обучить модель выбранного вида
GET /api/models/{model_id}/metrics метрики модели на тесте
POST /api/predict прогноз на заданные периоды
GET /api/predictions/{station_id} сохранённые прогнозы станции

Схема OpenAPI генерируется из кода и доступна на /docs.

Версии в пути нет: у API один потребитель — собственный интерфейс, и оба выкатываются вместе. Для внешних клиентов пришлось бы вводить /api/v1 и политику совместимости.

Проверки

make test        pytest
make lint        ruff check
make typecheck   mypy backend (strict)

28 тестов на препроцессинг, метрики, реестр моделей и внутренние функции сценария прогноза. Покрыта расчётная часть: разбор панели, оконное кодирование, метрики, границы доверительного интервала, ошибки реестра. HTTP-слой и репозитории тестами не покрыты — при разборе кода стоит смотреть в первую очередь на backend/ml и backend/application.

mypy проходит в режиме strict. CI прогоняет линтер, проверку формата, типы, миграции на живом Postgres и тесты.

Данные

Источник — портал открытых данных Москвы, набор data.mos.ru/opendata/62743. Выгрузка от 14.05.2026: 6142 квартальные записи по 263 станциям 18 линий за 2021-Q1 — 2026-Q1. Файл лежит в data/passenger_flow_full.csv, описание полей — в data/README.md.

Загрузка в базу — scripts/load_dataset.py.

Ограничения

Что система не делает и где проходят её границы:

  • горизонт прогноза ограничен несколькими кварталами. При горизонте больше 8 рекуррентные модели уходят в плато, потому что кормятся собственными предсказаниями
  • доверительный интервал считается как ±1.96σ по остаткам на тесте и предполагает нормальность остатков, которая на этих данных не проверялась. Интервал стоит читать как оценку порядка разброса, а не как строгие 95%
  • переобучение запускается вручную из интерфейса, автоматического расписания нет
  • аутентификации нет: система рассчитана на запуск в закрытом контуре. Выставлять её в интернет как есть нельзя
  • один инстанс, обученные модели лежат на диске рядом с приложением. Горизонтально не масштабируется без вынесения хранилища моделей
  • данные квартальные, поэтому для оперативных задач система не пригодна: суточный и часовой профиль внутри квартала не виден

About

Прогноз квартального пассажиропотока метро: FastAPI, слоистая архитектура, шесть моделей

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages