Wind Power Forecasting
Развёрнутый сервис почасового прогноза мощности двух турбин на 48 часов по архивному прогнозу ECMWF.
О проекте
Команда: Health Core
Кейс HackAlem.ai: Энергетика — Самрук-Казына
Стадия: Доведённый продукт
Задача кейса: AI-агент, прогнозирующий выработку ветровой электростанции по погодным данным
Суть
Развёрнутый сервис почасового прогноза мощности двух турбин на 48 часов по архивному прогнозу ECMWF. Модели версионируются в PostgreSQL или Supabase, интерфейс на Vue показывает график, карту ветра на Google Maps и выгрузку. Реализован последовательный прогон 1–28 февраля.
Как устроено
CSV загружаются в БД без дублей с сохранением исходных строк и агрегируются почасово по каждой турбине. HistGradientBoostingRegressor обучается на 7 признаках: ветер, температура, sin/cos часа, sin/cos дня года, id турбины. Для прогноза на 00:00 UTC+5 берётся выпуск ECMWF IFS за 12:00 UTC предыдущего дня через Open-Meteo Single Runs API. Сервер отклоняет ревизию модели, если её обучающие данные не предшествуют моменту выпуска. Качество проверяется на временном holdout из последних 10% данных, прогнозы и погодные признаки сохраняются в БД.
Ключевые решения
- HistGradientBoostingRegressor
- циклическое кодирование времени
- временной holdout
- защита от утечки будущих данных
- ECMWF IFS через Open-Meteo Single Runs
- версионирование ревизий модели
Метрики
MAE 0.02637 (шкала 0–1) на историческом holdout против 0.32101 у прогноза постоянной средней; прогон февраля: 2688 точек без пропусков
Особенности
Строгая гигиена против утечки данных: выбор погодного выпуска, опубликованного до момента решения, и серверная проверка cutoff ревизии модели. Команда честно отделяет holdout-оценку от недоступной оценки февраля. Демо развёрнуто на healthcore.ych.kz.
Стек
Python · FastAPI · PostgreSQL · Supabase · SQLite · Vue 3 · Vite · Google Maps · nginx · systemd
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-ed58da2d-health-core.
README
README.mdWind Power Forecasting — энергия ветра на 48 часов вперёд
Какой профиль мощности покажет каждая турбина завтра, если изменится ветер? Команда HEALTH CORE превращает архивный прогноз погоды для координат турбин в почасовой прогноз мощности в долях от номинала. Оператор видит не только результат, но и дату погодного выпуска, версию обученной модели и исходные данные расчёта.
Открыть работающее демо → · Трек «Энергетика» · Кейс HackAlem AI «Agentic AI для прогнозирования выработки ветровой электростанции (ВЭС)».
- Практический результат: 48 почасовых значений для выбранных турбин, график, карта ветра и выгрузка; можно посмотреть первые 24 часа.
- Воспроизводимость: исходные измерения сохраняются без потери полей, модель обучается из базы данных, каждая ревизия и каждый прогноз остаются доступными для проверки.
- Проверенный масштаб: последовательный прогон 1–28 февраля для двух турбин создал 2 688 прогнозных точек без пропусков. Это проверка работы расчёта, не заявленная точность февраля: фактической выработки за этот период в выданных данных нет.

Скриншот публичного демо: сохранённый прогноз, а не тестовый макет. Мощность показана в долях от номинала (0–1), не в мегаваттах.
Как продукт получает результат
- Загружает историю измерений турбин из конкурсных файлов в PostgreSQL и обучает версионируемую модель на данных, доступных до момента прогноза.
- Для координат каждой выбранной турбины берёт архивный прогноз погоды, выпущенный Европейским центром среднесрочных прогнозов погоды (ECMWF) до момента решения, а не фактическую погоду задним числом.
- Рассчитывает 48 часов, сохраняет погодные признаки и результат в базе данных, показывает их в интерфейсе и позволяет выгрузить. Новые измерения можно загрузить и обучить новую ревизию модели.

Карта публичного демо: скорость и направление ветра показаны для точек турбин и выбранного часа; между турбинами сплошное поле ветра не моделируется.
Историческая проверка модели дала среднюю абсолютную ошибку 0,02637 на шкале мощности 0–1 против 0,32101 у простого прогноза постоянной средней. Это сравнение на отложенных исторических измерениях, не оценка качества прогноза за февраль: в феврале модель получает прогнозный ветер, а фактической выработки для сравнения нет. Методика и ограничения приведены ниже.
Подробности: соответствие кейсу · формулы и проверка · зависимости · установка · серверный деплой · словарь сокращений.
Соответствие кейсу
| Требование кейса | Состояние |
|---|---|
| Модель по историческим данным ВЭС | Реализовано; разбор CSV, почасовая агрегация, обучение из БД и внешний временной holdout. |
| Архивный прогноз погоды по координатам на дату решения | Реализовано через Open-Meteo Single Runs; фактическое время публикации выпуска API не подтверждает. |
| Почасовой прогноз на 24–48 часов | Реализовано: расчёт 48 часов, просмотр и экспорт первых 24 либо всех 48. |
| Последовательный тестовый период 1–28 февраля | Реализован возобновляемой CLI-командой для двух турбин; 28 × 2 × 48 точек. В интерфейсе нет кнопки общего запуска. |
| Автономный цикл и повторный расчёт при обновлении входных данных | Цикл запускается пользователем; после загрузки данных нужно вручную переобучить модель и повторить прогноз. Автоматическая очередь/триггер обновления пока не реализованы. |
| Оценка ошибки февральского прогноза | Невозможна: фактических значений февраля в выданных CSV нет; ниже приведён исторический holdout без утечки. |
Что решаем
На каждый день с 31 января по 28 февраля 2026 года система должна воспроизвести решение, которое могло быть принято в тот день: получить доступный на тот момент прогноз погоды, подготовить признаки, применить обученную модель и сохранить результат на 24–48 часов. Отдельный расчёт запускается через API, а воспроизводимый последовательный прогон февраля — командой python -m backend.forecast_range. Оценка по фактическим данным февраля невозможна: их нет в предоставленных CSV.
Для расчёта на 00:00 времени Казахстана (UTC+5) используется выпуск ECMWF IFS за 12:00 UTC предыдущего календарного дня UTC. До момента решения остаётся около 7 часов на публикацию модели. Время выпуска, версия модели и входные погодные параметры сохраняются вместе с прогнозом. Прогнозная мощность — нормализованное значение 0–1 каждой турбины; перевод в МВт невозможен без номинальной мощности.
Исходные CSV из папки TZ/ содержат 291 859 строк с 11 марта 2023 года по 31 января 2026 года, хотя в имени файлов фигурирует 28 февраля. CSV входят в закрытый Git-репозиторий. Импорт сохраняет исходные поля каждой строки в measurements.original и отдельно числовые поля для вычислений. Обучение всегда читает БД, а не файлы CSV. По умолчанию оно исключает записи начиная с 31 января 2026 года, чтобы ревизию можно было использовать уже в прогнозе 31 января в 00:00. Для выпусков начиная с 1 февраля можно явно выбрать отсечение 1 февраля; тестовый февраль в обучение не допускается. Сервер отклоняет ревизию, если её последняя обучающая запись не предшествует моменту выпуска прогноза. Проверка модели использует последние 10% почасовых данных по времени. Фактическую ошибку февральского прогноза пока оценить нельзя. Модель обучается на измеренном ветре у турбины, а при прогнозе получает ветер ECMWF на высоте 100 м; эта разница распределений и неизвестное время публикации погодного выпуска ограничивают достоверность проверки.
Состав и архитектура
backend/app/db.py: PostgreSQL (включая Supabase) или локальная SQLite; исходные измерения, турбины, ревизии моделей, бинарные артефакты, запуски и точки прогноза.init-dbсоздаёт таблицы, ограничения, индексы и включает RLS в PostgreSQL.backend/app/pipeline.py: импорт без дублирования по(turbine_id, source_id), почасовая агрегация, обучениеHistGradientBoostingRegressor, расчёт и простой анализ диапазона результата.backend/app/weather.py: архив отдельного выпуска ECMWF IFS через Open-Meteo Single Runs API.backend/app/main.py: FastAPI для просмотра и загрузки измерений, регистрации турбин, обучения, выбора ревизии и запуска прогноза.frontend/: интерфейс на Vue 3 с графиком, таблицей, выбором турбин и ревизии, загрузкой CSV, обучением и экспортом прогноза.
Раздел «География ВЭС» показывает на Google Maps турбины и интерактивный точечный слой ветра. Для выбранной даты браузер отдельно запрашивает из Open-Meteo Single Runs API почасовые wind_speed_100m и wind_direction_100m того же архивного выпуска ECMWF IFS, который соответствует правилу выбора погоды для прогноза. Стрелки показывают, куда дует ветер; при выборе часа меняются значения у каждой турбины. Слой носит справочный характер: эти дополнительные данные не записываются в БД и не меняют обученную модель или сохранённый прогноз. Направление не содержится в исходных измерениях, а данные у двух турбин нельзя считать непрерывным полем ветра между ними.
Цикл агента в текущей версии выполняется по запросу: выбор доступного выпуска погоды → загрузка → подготовка признаков → модель → контроль допустимого диапазона → запись результата. Автоматический повторный расчёт при обновлении входных данных, журнал состояний и обработка сбоев — задачи следующего этапа; они пока не заявляются как готовые.
Как получаются данные, прогноз и показатели
Исходные измерения и почасовая выборка
Загрузка принимает CSV в UTF-8 (допускается BOM) ровно с пятью колонками из ТЗ: ID, статистическое время, средняя скорость ветра (м/с), нормализованная активная мощность и средняя температура воздуха (°C). Вся строка сохраняется без изменения значений в measurements.original; отдельно сохраняются её числовые поля и время для расчётов. Для пары (turbine_id, source_id) действует уникальность: повторная загрузка пропускает эту пару. В ответе rows — число прошедших разбор строк файла, imported — число новых строк в БД, skipped = rows - imported. Некорректный файл не импортируется частично.
Для обучения из БД выбираются измерения с observed_at < cutoff_date 00:00 по времени Казахстана. По умолчанию cutoff_date = 2026-01-31; позже 2026-02-01 задать нельзя. Значения каждого признака и цели усредняются отдельно для каждой турбины и каждого календарного часа:
wind_hour = sum(wind_speed_ms в этом часе) / число имеющихся измерений ветра
temp_hour = sum(temperature_c в этом часе) / число имеющихся измерений температуры
power_hour = sum(normalized_power в этом часе) / число имеющихся измерений мощности
Это среднее по имеющимся строкам, а не сумма выработанной энергии: если 10-минутные записи отсутствуют, час всё равно может участвовать. Часы без полного набора трёх агрегированных значений удаляются; отсутствующие часы не интерполируются. Цель модели — power_hour в шкале 0–1. Номинальная мощность турбины в МВт не предоставлена, поэтому МВт, МВт·ч и суммарная энергия не вычисляются.
Обучение и проверка модели
Для почасовой строки строится вектор из семи признаков в фиксированном порядке:
x = [wind_hour, temp_hour,
sin(2π · hour/24), cos(2π · hour/24),
sin(2π · day_of_year/365.25), cos(2π · day_of_year/365.25),
turbine_id]
hour и day_of_year берутся непосредственно из метки времени CSV; её часовой пояс поставщик не указал. Циклическое кодирование делает соседними 23:00 и 00:00, а также конец и начало года. В прогнозе за 2026 год время явно переводится в UTC+5. До 1 марта 2024 года Алматы жил в UTC+6; CSV не содержат повторного часа при переводе часов назад, поэтому абсолютную временную привязку старых записей нельзя подтвердить без ответа поставщика. Обучается одна общая модель для турбин, где turbine_id — числовой признак, а не отдельная модель и не категориальная one-hot переменная. Используется HistGradientBoostingRegressor: ансамбль деревьев, обучаемый по квадратичной ошибке. Явные параметры: max_iter=100 (верхний предел итераций), max_leaf_nodes=20, l2_regularization=0.1, random_state=42. Параметры библиотеки, не заданные кодом, в зафиксированной версии scikit-learn 1.9.1 остаются по умолчанию, в том числе learning_rate=0.1, loss='squared_error' и early_stopping='auto'. При выборке более 10 000 строк автоматическая ранняя остановка использует внутренний проверочный срез; поэтому фактическое число итераций может быть меньше 100.
После почасовой агрегации все строки сортируются по времени. Пусть q90 — 90-й процентиль почасовых меток времени. Внешняя обучающая часть — строки с observed_at < q90, внешняя проверочная — с observed_at >= q90; одинаковые часы двух турбин остаются по одну сторону границы. Это примерно последние 10% часов, но не обязательно ровно 10% строк. Новая ревизия обучается заново на внешней обучающей части; это не инкрементальное обновление старой модели. При включённой автоматической ранней остановке scikit-learn дополнительно выделяет 10% из внешней обучающей части для своей внутренней проверки. Внешняя проверочная часть не передаётся в model.fit.
На внешней проверочной части вычисляется pred_i = min(1, max(0, model.predict(x_i))), затем MAE:
validation_mae = (1 / N_validation) · Σ |power_hour_i - pred_i|
training_rows и validation_rows — количества почасовых строк после агрегации во внешних частях, не исходных CSV-строк; training_rows включает строки, которые библиотека могла отложить для внутренней ранней остановки. training_max_at в API — последний час всей подготовленной выборки, включая внешнюю проверочную часть; это консервативная граница доступности данных ревизии, а не последний час, переданный в model.fit. Без явного revision_id сервер выбирает последнюю ревизию, у которой training_max_at < issued_at; явно выбранная несовместимая ревизия отклоняется. MAE выражена в единицах нормализованной мощности (0–1), а не в процентах или МВт; она проверяет модель на измеренном ветре, а не качество февральского прогноза с прогнозным ветром.
Каждое обучение создаёт новый revision_id и уникальную 12-символьную метку model_version. Метаданные и бинарный артефакт сохраняются в БД; локальный MODEL_DIR хранит копию. Выбор пустой ревизии при расчёте означает последнюю совместимую с датой выпуска по revision_id, а не «самую точную» по MAE. Архивные расчёты сохраняют использованную model_version.
Фактически проверенная точность
Команда python -m backend.evaluate 2 повторно оценивает сохранённую ревизию 301a08f545e8 на строго более позднем временном отрезке 21.10.2025 10:00 — 30.01.2026 23:00, который не передавался в model.fit. Это 4 873 почасовые строки двух турбин после агрегации. Базовый прогноз для сравнения — постоянная средняя мощность соответствующей турбины, вычисленная только по обучающей части. MAE и RMSE выражены в шкале нормализованной мощности 0–1:
| Выборка | Часы | MAE модели | RMSE модели | MAE базового прогноза |
|---|---|---|---|---|
| Обе турбины | 4 873 | 0,02637 | 0,06288 | 0,32101 |
| Турбина 1 | 2 438 | 0,02575 | 0,05594 | 0,32115 |
| Турбина 2 | 2 435 | 0,02699 | 0,06913 | 0,32088 |
MAE модели ниже MAE этого простого baseline на 91,8%. Это не точность февральского прогнозирования: в holdout модель получает измеренный ветер, а при расчёте февраля — прогноз ECMWF. Фактические мощности за февраль отсутствуют; поэтому февральские MAE, RMSE и «процент точности» не заявляются.
Команда python -m backend.forecast_range --revision-id 1 --turbine-ids 1 2 рассчитала 1–28 февраля 2026 года без пропусков: 28 выпусков × 2 турбины × 48 часов = 2 688 прогнозных точек. Все сохранены в PostgreSQL, нормализованная мощность лежит в 0…0,98942. Повторный запуск пропускает уже имеющиеся полные выпуски той же версии и набора турбин; он не проверяет точность без фактической мощности. Ревизия №1 обучена на данных не позднее 31 января, то есть доступна для выпуска 1 февраля.
Погодный выпуск и прогноз для выбранных турбин
Для даты D момент решения issued_at = D 00:00 UTC+5, то есть 19:00 UTC предыдущего дня. Код выбирает отдельный выпуск ECMWF IFS, инициализированный в 12:00 UTC того же UTC-дня: между инициализацией и решением около семи часов. По сохранённым координатам каждой выбранной турбины Open-Meteo Single Runs API запрашивает run=<время выпуска>, wind_speed_100m (м/с) и temperature_2m (°C) с часовыми метками UTC. Это значения архивного прогноза погоды, а не измеренная или реанализированная погода. Если хотя бы один необходимый час отсутствует, расчёт завершается ошибкой; подмены фактической погодой нет. Open-Meteo не возвращает момент публикации выпуска, поэтому семичасовой запас — допущение, а не доказательство доступности прогноза в тот момент.
Для каждого выбранного turbine_id и k = 1…48 строится valid_at_UTC = issued_at_UTC + k часов. Погодные wind_speed_100m и temperature_2m занимают первые два места того же вектора x; час и день года для синусов и косинусов берутся из valid_at после перевода в UTC+5, а последнее место — ID турбины. Итоговая почасовая мощность:
forecast_power(turbine, k) = min(1, max(0, model_revision.predict(x_turbine,k)))
Это результат обученной модели, не формула пересчёта скорости ветра в энергию. Коэффициенты и деревья определяются обучением, поэтому результат на иных погодных данных получается повторным вычислением тех же семи признаков и вызовом predict, без добавления этих погодных данных в обучающую выборку. Прогнозные ветер и температура вместе с результатом сохраняются в forecast_points. На одну выбранную турбину сохраняется 48 точек; на T турбин — 48 × T. Переключатель 24/48 часа в интерфейсе меняет только отображение, не сам расчёт и не запись в БД.
Показатели интерфейса и результата API
В интерфейсе сначала выбираются точки одной просматриваемой турбины, сортируются по valid_at и ограничиваются первыми H строками, где H = 24 или 48. Если P — набор точек с конечным числовым значением normalized_power, а W — набор точек с конечной скоростью ветра, то:
| Показатель | Точное вычисление | Единица и смысл |
|---|---|---|
| Средняя мощность | sum(p.normalized_power for p in P) / len(P) | Нормализованная мощность 0–1 за показанный горизонт; не сумма энергии. |
| Пиковая мощность и время | Последовательно сравниваются точки: следующая заменяет текущий максимум при next.power >= current.power; берётся её valid_at | Последнее максимальное значение при равенстве; время показывается в UTC+5. |
| Минимальная мощность и время в «Источниках данных» | Следующая точка заменяет текущий минимум при next.power <= current.power; берётся её valid_at | Последнее минимальное значение при равенстве. |
| Средний ветер | sum(p.wind_speed_ms for p in W) / len(W) | м/с, среднее прогнозных погодных значений показанного горизонта. |
| Покрытие горизонта | Показываются два числа: len(rows) из ожидаемых H | Например, 24 / 48 ч; это не вычисленный процент или результат деления. |
| График и таблица | Каждая сохранённая точка без дополнительного усреднения; переключение между normalized_power, wind_speed_ms, temperature_c | Мощность 0–1, ветер м/с, температура °C; в таблице значения только округляются для показа. |
analysis.points | 48 × число выбранных турбин | Число точек всего сохранённого расчёта. |
analysis.min, analysis.max, analysis.mean | min, max, sum/N по всем предсказаниям расчёта до сохранения | Нормализованная мощность за полные 48 часов и все выбранные турбины; может отличаться от карточек одной турбины и 24-часового просмотра. |
При пустом наборе показатель не выдаётся за ноль: интерфейс показывает «—» или отсутствие результата. Проверки интерфейса отмечают неполные 24/48 часов, значения мощности вне 0–1, неверную последовательность часов и статус invalid_lookahead; при этих ошибках экспорт отключён. Индикатор «API подключён» проверяет только GET /health и соединение с БД, не точность модели. Проверка «погодный выпуск не позже решения» сравнивает weather_run_at <= issued_at, но сама по себе не доказывает фактическое время публикации погоды.
График мощности имеет шкалу 0–1. Для ветра и температуры нижняя граница шкалы — min(0, минимум показанных значений) - 1, верхняя — максимум показанных значений + 1; пять подписей оси делят этот диапазон на четыре равные части. Полоса мощности в таблице имеет ширину min(100, max(0, normalized_power × 100))%. Эти преобразования касаются только изображения: экспорт CSV отдаёт исходные сохранённые числа без экранного округления, с valid_at в UTC.
Словарь сокращений
| Сокращение | Значение в проекте |
|---|---|
| ВЭС, ТЗ, БД, SQL | Ветровая электростанция; техническое задание; база данных; Structured Query Language — язык запросов к реляционной БД. |
| ECMWF | European Centre for Medium-Range Weather Forecasts — Европейский центр среднесрочных прогнозов погоды. Официальный словарь ECMWF. |
| IFS | Integrated Forecasting System — интегрированная система прогнозирования ECMWF. В коде Open-Meteo модель обозначена ecmwf_ifs. Описание ECMWF. |
| HRES | High-Resolution Forecast — прежнее название детерминированного прогноза высокого разрешения ECMWF. В актуальной номенклатуре он называется IFS Medium-range Control forecast (formerly HRES); сокращение HRES не обозначает отдельную версию модели 2026 года. Каталог ECMWF. |
| AI, Agentic AI | Artificial Intelligence — искусственный интеллект; «агентный ИИ» здесь означает последовательный цикл получения погоды, подготовки признаков, применения модели, проверки и сохранения результата. Автоматический перезапуск при обновлении данных пока не реализован. |
| CSV, UTF-8, BOM | Comma-Separated Values — текстовый табличный файл; Unicode Transformation Format, 8-bit — кодировка файла; Byte Order Mark — необязательная сигнатура в его начале. |
| API, CLI, URL, HTTPS | Application Programming Interface — программный интерфейс; Command-Line Interface — командная строка; Uniform Resource Locator — адрес ресурса; Hypertext Transfer Protocol Secure — защищённый протокол сайта. |
| UTC | Coordinated Universal Time — всемирное координированное время. UTC+5 — местное время, на пять часов опережающее UTC. |
| MAE, RMSE | Mean Absolute Error — средняя абсолютная ошибка; Root Mean Squared Error — корень из средней квадратичной ошибки. Здесь обе измеряются в шкале нормализованной мощности 0–1. |
| RLS, DDL, JSON, ID | Row Level Security — защита строк таблицы; Data Definition Language — команды изменения схемы БД; JavaScript Object Notation — формат структурированных данных; Identifier — идентификатор записи. |
| CC BY 4.0 | Creative Commons Attribution 4.0 International — лицензия данных с обязательным указанием источника. |
| MIT, BSD, LGPL | Краткие названия лицензий программных компонентов: Massachusetts Institute of Technology License, Berkeley Software Distribution License и Lesser General Public License. Точные лицензии зависимостей перечислены ниже. |
| МВт, МВт·ч | Мегаватт — единица мощности; мегаватт-час — единица энергии. Прогноз 0–1 нельзя перевести в них без номинальной мощности турбины и длительности периода. |
Зависимости и внешние источники
Backend зависимости с точными версиями зафиксированы в requirements.txt: FastAPI, Uvicorn, SQLAlchemy, Psycopg 3, pandas, NumPy, scikit-learn, joblib, HTTPX, python-dotenv, python-multipart. Frontend зависимости находятся в frontend/package.json, а точные версии Vue 3, Vite и их транзитивных пакетов — в frontend/package-lock.json. Для локальной сборки нужны Python 3.11, Node.js 22 и npm; для серверного варианта — Debian 12, PostgreSQL 17 или Supabase PostgreSQL, nginx и Certbot. Прямые зависимости перечислены ниже; транзитивные Python-зависимости разрешаются pip из зафиксированного файла верхнего уровня, а транзитивные JS-версии закреплены lock-файлом.
Внешний погодный сервис: Open-Meteo Single Runs API, модель ecmwf_ifs, доступ к конкретным архивным выпускам. Это исторические прогнозы, не реанализ и не фактическая погода. Источник данных — Open-Meteo с данными ECMWF; лицензия API-данных CC BY 4.0, атрибуция обязательна. Бесплатный API разрешён для некоммерческого использования и имеет лимиты запросов; для коммерческого применения нужен соответствующий тариф. Две исходные координаты взяты из ссылок Google Maps в ТЗ; Google Maps также используется для раскрытия короткой ссылки при добавлении турбины. Supabase предоставляет PostgreSQL; приложение не использует его пользовательскую авторизацию или Storage.
Прямые зависимости и лицензии по метаданным установленных пакетов:
| Компонент | Источник | Лицензия |
|---|---|---|
| FastAPI | fastapi/fastapi | MIT |
| Uvicorn | encode/uvicorn | BSD-3-Clause |
| SQLAlchemy | sqlalchemy/sqlalchemy | MIT |
| Psycopg 3 | psycopg/psycopg | LGPL-3.0-only |
| pandas | pandas-dev/pandas | BSD-3-Clause |
| NumPy | numpy/numpy | BSD-3-Clause и лицензии отдельных частей, см. пакет |
| scikit-learn | scikit-learn/scikit-learn | BSD-3-Clause |
| joblib | joblib/joblib | BSD-3-Clause |
| HTTPX | encode/httpx | BSD-3-Clause |
| python-dotenv | theskumar/python-dotenv | BSD-3-Clause |
| python-multipart | Kludex/python-multipart | Apache-2.0 |
| Vue 3 | vuejs/core | MIT |
| Vite | vitejs/vite | MIT |
Развёртывание с нуля на Windows
Требуются Git, Python 3.11+ (python --version), Node.js 22+ и npm (node --version, npm --version), доступ к Open-Meteo. Команды выполнять в PowerShell из корня проекта. Для рабочей установки задайте PostgreSQL в Supabase либо собственный PostgreSQL; SQLite допустима только для локальной проверки. Если репозитория ещё нет: git clone https://github.com/BAITC-Hacks/hack-ed58da2d-health-core.git, cd hack-ed58da2d-health-core, git switch dev.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
Copy-Item .env.example .env
python -m backend.cli init-db
python -m unittest discover -s backend/tests -v
Перед init-db при использовании Supabase замените DATABASE_URL в .env по инструкции ниже. Без этого параметра либо с примером из .env.example используется локальная SQLite; публичный production-запуск с SQLite запрещён. Команда init-db не удаляет записи и её можно запускать повторно. Инициализация схемы также выполняется при старте API, если DB_INIT_ON_STARTUP=true. В уже существующей БД create_all не меняет столбцы старых таблиц: последующие изменения схемы потребуют отдельной миграции.
Поместите оба конкурсных CSV в локальную папку TZ/ перед первым запуском API. Файлы должны заканчиваться на turbine 1.csv и turbine 2.csv; они не включаются в Git. При AUTO_BOOTSTRAP=true (значение по умолчанию) backend в фоне проверит их, сохранит исходные строки в выбранной БД и обучит модель с отсечением 31 января 2026 года. Интерфейс покажет «Загрузка данных», затем «Обучение модели» и после завершения — «Прогноз доступен»; при ошибке покажет причину. GET /readiness возвращает те же состояния. Уже существующая ревизия модели не обучается повторно при рестарте. Если файлы добавлены после запуска, перезапустите backend. Путь можно изменить через TZ_DIR в .env.
Ручные импорт и переобучение через интерфейс либо CLI остаются доступны, если нужно обновить данные:
python -m backend.cli import-csv 1 'TZ/Dataset HackAlemAI для участников 11.03.2023-28.02.2026 - turbine 1.csv'
python -m backend.cli import-csv 2 'TZ/Dataset HackAlemAI для участников 11.03.2023-28.02.2026 - turbine 2.csv'
python -m backend.cli train
Чтобы использовать весь январь для прогноза с 1 февраля, укажите python -m backend.cli train --cutoff-date 2026-02-01. Для прогноза, выпущенного 31 января в 00:00, такая ревизия не подходит и будет отклонена сервером.
Для проверки установки без конкурсных файлов доступен генератор явно синтетических данных: python -m backend.generate_demo_csv demo-data, затем python -m backend.cli import-csv 1 demo-data/demo-turbine-1.csv, python -m backend.cli import-csv 2 demo-data/demo-turbine-2.csv, python -m backend.cli train. Это только smoke test: его метрики не сопоставимы с конкурсным набором. Импорт проверяет весь файл до записи, отклоняет нечисловые и недопустимые значения и повторяем: существующие пары (turbine_id, source_id) пропускаются. Для каждой новой турбины сначала добавьте координаты, затем её CSV и обучите новую ревизию. SQLite и локальный кэш модели не коммитятся; бинарный артефакт каждой новой ревизии сохраняется также в общей БД.
Запустите API в первом терминале (с активированным .venv):
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 8000
Во втором терминале, из корня проекта:
cd frontend
npm ci
npm run dev
Открыть http://127.0.0.1:5173. Для проверки API доступны /docs, /health и /readiness на http://127.0.0.1:8000: /health должен содержать "database":"ready", а /readiness после подготовки — "can_forecast":true. До готовности кнопка расчёта заблокирована, причина видна в панели прогноза. Для проверки выберите 31 января 2026 года, запустите агента и убедитесь, что получено по 48 строк на каждую выбранную турбину. Прямой запрос для первой турбины с последней совместимой ревизией:
Invoke-RestMethod -Method Post http://127.0.0.1:8000/forecasts/2026-01-31 -ContentType 'application/json' -Body '{"turbine_ids":[1]}'
База данных и доступы
Supabase-проект и сама база postgres создаются в кабинете Supabase; приложение не создаёт облачный проект. python -m backend.cli init-db в существующей базе создаёт таблицы measurements, turbines, model_revisions, model_artifacts, forecast_runs, forecast_points, ограничения и индексы, а также идемпотентно регистрирует две турбины из ТЗ. В PostgreSQL для всех шести таблиц включается RLS без политик для anon/authenticated: фронтенд работает только через backend, прямой доступ через Supabase Data API закрыт. На общей БД после обновления кода запускайте init-db явно: DB_INIT_ON_STARTUP=false пропускает DDL при обычном старте API. Bootstrap не изменяет столбцы существующих таблиц.
В локальном .env рядом с .env.example укажите DATABASE_URL. Файл .env игнорируется Git. В панели Supabase откройте Connect → Connection string и выберите Direct connection для ПК/постоянного сервера с работающим IPv6 либо Session pooler (порт 5432) для IPv4. Если Direct-хост db.<project-ref>.supabase.co не подключается на ПК без IPv6, используйте Session pooler, не Transaction pooler на порту 6543. Замените схему URL postgresql:// на postgresql+psycopg:// (приложение также делает это автоматически). Пример без реальных реквизитов:
DATABASE_URL=postgresql+psycopg://USER:[скрыто]@HOST:5432/postgres?sslmode=require
Для повседневной работы используйте отдельную роль с минимальными правами на таблицы; первичная инициализация требует права создавать объекты в public и включать RLS. Пароль с @, :, / и другими служебными символами необходимо URL-кодировать. Не отправляйте строку подключения в чат или репозиторий. Supabase API key и URL проекта для текущей архитектуры не нужны: backend использует PostgreSQL-соединение. Проверьте подключение командами python -m backend.cli init-db и Invoke-RestMethod http://127.0.0.1:8000/health после запуска API. При ошибке соединения старт API завершится с ошибкой, а не переключится молча на SQLite. Текущая версия создаёт начальную схему, но не содержит версионированных миграций последующих изменений. Альтернатива Supabase — собственный PostgreSQL 17; команды установки, создания роли и БД даны в deploy/README.md.
Перенос существующей SQLite в Supabase
Остановите локальный backend перед переносом, чтобы новые записи не появились в SQLite после снимка. Фронтенд останавливать необязательно. Для сохранности сделайте копию локальной БД (файл игнорируется Git):
Copy-Item .\healthcore.sqlite3 .\healthcore-before-supabase.sqlite3
В .env добавьте SUPABASE_DATABASE_URL=postgresql+psycopg://... или замените существующий DATABASE_URL на полный URL из Connect → Connection string в Supabase. Если заданы обе переменные, SUPABASE_DATABASE_URL имеет приоритет. После успешного переноса и перезапуска backend приложение будет читать PostgreSQL. Не добавляйте URL в код, README или чат. Используйте роль с правами на создание таблиц и запись; целевые таблицы measurements, forecast_runs, forecast_points должны быть пустыми. Скрипт не смешивает данные с существующими строками и не удаляет их.
.\.venv\Scripts\python.exe -m backend.migrate_sqlite_to_postgres
.\.venv\Scripts\python.exe -m backend.cli init-db
.\.venv\Scripts\python.exe -m uvicorn backend.app.main:app --host 127.0.0.1 --port 8000
Скрипт копирует строки трёх исходных таблиц порциями, сохраняя ID, JSON и связи прогнозов; загрузка выполняется одной транзакцией. Он сверяет количество, максимальный ID и сумму ID каждой таблицы до и после коммита и обновляет PostgreSQL-последовательности. Новые таблицы турбин и моделей создаются bootstrap-командой, но скрипт не переносит в них ревизии из более новой SQLite. Если перенос не завершён, записи целевых таблиц откатываются и локальная SQLite остаётся нетронутой. Проверьте http://127.0.0.1:8000/health, затем откройте сохранённый прогноз по ID. Старый model.joblib — локальный артефакт; после переноса рекомендуется обучить новую ревизию, чтобы она сохранилась в model_artifacts и была доступна всей команде. Не удаляйте резервную SQLite, пока не проверите данные в Supabase.
API и пополнение данных
GET /turbines— список турбин и координат;POST /turbines— добавить{name, latitude, longitude}либо{name, maps_url}. Для коротких ссылок поддерживается только HTTPS Google Maps.GET /measurements?turbine_id=1&limit=100&offset=0— исходные строки для просмотра.POST /measurements— добавить одно измерение сturbine_id,source_id,observed_at,wind_speed_ms,normalized_power,temperature_c.POST /measurements/upload?turbine_id=1— загрузить CSV multipart-полемfile, максимум 50 МБ; ответ{rows, imported, skipped}.POST /models/train— создать ревизию по данным БД до 31 января 2026 года (по умолчанию); тело{"cutoff_date":"2026-02-01"}разрешает использовать весь январь для выпусков не раньше 1 февраля.GET /models— список ревизий и метрик.GET /readiness— готовность прогнозирования, стадия фонового импорта/обучения, причина недоступности (в том числе БД) и активная ревизия.POST /forecasts/2026-01-31с JSON{"turbine_ids":[1,2],"revision_id":1}— рассчитать 48 часов для указанных турбин выбранной ревизией. Безrevision_idиспользуется последняя ревизия, совместимая с датой выпуска.GET /forecasts/{id}— результат, погодные признаки и анализ.GET /forecasts— до 100 последних сохранённых расчётов для истории интерфейса.
Для Supabase Session Pooler количество соединений ограничено тарифом проекта. Backend по умолчанию держит не более двух соединений (DB_POOL_SIZE=2, DB_MAX_OVERFLOW=0); если одновременно запускаются локальный backend, сервер и CLI, сумма их пулов должна оставаться ниже лимита Supabase.
Ревизия модели содержит метаданные и бинарный артефакт в PostgreSQL; локальный файл в MODEL_DIR — кэш и резервная копия. Другой ПК с доступом к той же БД может выбрать ревизию без копирования файлов. Модель не предсказывает для турбины без обучающих строк; сначала импортируйте её CSV и переобучите модель. Все операции просмотра и записи доступны без ключа оператора как локально, так и на публичном сайте.
Публичная запись сейчас открыта намеренно: любой посетитель может добавить турбину, загрузить CSV, переобучить модель и создать прогноз. Это демо-режим; для долгосрочной эксплуатации нужны аутентификация, ограничения запросов, защита загрузок и отдельные права БД.
Серверный запуск
Публичная версия: healthcore.ych.kz. API размещается в /opt/healthcore/api, собранный frontend — в /opt/healthcore/ui. На Debian 12 устанавливаются Python 3.11, Node.js 22, nginx, Certbot и выбранная БД. Сервисный файл deploy/healthcore-api.service запускает Uvicorn от отдельного пользователя healthcore только на loopback; nginx проксирует /api/ и обслуживает статический сайт по HTTPS. Полный повторяемый процесс с выбором Supabase либо собственного PostgreSQL — в deploy/README.md.
Для Supabase на сервере строку подключения размещают только в /opt/healthcore/api/.env, заменяя DATABASE_URL; для собственного PostgreSQL указывают локальный URL. После переключения БД нужна повторная инициализация схемы и импорт данных. Содержимое .env нельзя отправлять в Git. Переключение с SQLite на PostgreSQL не переносит данные автоматически.
Ограничения
Нет февральских фактических значений для итоговой метрики. Нет автоматической очереди/периодического пересчёта через API, версионируемых миграций, калибровки смещения измеренного ветра к ECMWF. Полный февральский прогон доступен как возобновляемая CLI-команда, не как фоновая задача интерфейса. Open-Meteo возвращает выбранный архивный выпуск, но не подтверждает точный момент его публикации. Выбор выпуска за 12:00 UTC предыдущего дня оставляет около семи часов запаса до решения; для строгой проверки доступности требуется независимый журнал публикации. Метрика MAE на измеренном ветре не является оценкой точности реальных февральских прогнозов. Часовой пояс исторических CSV не подтверждён поставщиком.
В текущей общей БД ранее сохранённые прогоны за 31 января с прежними моделями помечены invalid_lookahead; их точки сохранены для аудита, но интерфейс запрещает экспорт как достоверного результата. Прогноз с новой ревизией, обученной строго до 31 января, сохранён отдельно.
Команда и вклад
HEALTH CORE — команда проекта Wind Power Forecasting.
- Андрей: backend, БД, импорт, модель, погодный API, сервер, воспроизводимый запуск и README.
- Михаил: UX/UI, frontend, интеграция с API, сценарий демонстрации и продуктовая проверка соответствия кейсу.
План команды — в PLAN.md, контракт интерфейса и сервера — в INTEGRATION.md. Исходный кейс и данные в TZ/, справочные материалы в docs/ включены в Git для воспроизводимости установки; репозиторий должен оставаться закрытым и доступным только команде и организаторам. Локальный .env, ключи и HACKATHON_TZ.md не публикуются.
Авторы коммитов
История коммитов
- 2026-09-23 17:55Объединить правки README и карту рельефаUssenko Andrey
- 2026-09-23 17:54Показать рельеф и обе турбины на карте и в READMEUssenko Andrey
- 2026-09-23 17:46docs: remove outdated Git note from READMEMG-Trener
- 2026-09-23 17:43Обновить README для жюри и добавить скриншоты демоUssenko Andrey
- 2026-09-23 17:25Упорядочить разделы бокового менюUssenko Andrey
- 2026-09-23 17:14Добавить исходные данные и материалы хакатона в закрытый репозиторийUssenko Andrey
- 2026-09-23 17:14Убрать ключ оператора из API и интерфейсаUssenko Andrey
- 2026-09-23 17:06docs: добавить ссылку на демо в начало READMEUssenko Andrey
Все коммиты (49)
- 2026-09-23 17:03fix: интерфейс получает причину недоступности PostgreSQLUssenko Andrey
- 2026-09-23 17:02fix: повторная подготовка при неполной ревизии моделиUssenko Andrey
- 2026-09-23 17:00merge: объединены автоматическая подготовка модели и карта ветраUssenko Andrey
- 2026-09-23 16:59feat: интерфейс показывает готовность прогноза и причины блокировкиUssenko Andrey
- 2026-09-23 16:59feat: автоматический импорт CSV и обучение модели при первом запускеUssenko Andrey
- 2026-09-23 16:49Add interactive archived wind overlay to turbine mapMG-Trener
- 2026-09-23 16:46perf: история прогнозов загружается одним запросомUssenko Andrey
- 2026-09-23 16:44fix: ограничен пул соединений с SupabaseUssenko Andrey
- 2026-09-23 16:41feat: история прогнозов сохраняется между сессиямиUssenko Andrey
- 2026-09-23 16:35test: изолирована проверка API от серверного ключа записиUssenko Andrey
- 2026-09-23 16:31docs: описаны тестовый диапазон и развёртывание двух вариантов PostgreSQLUssenko Andrey
- 2026-09-23 16:23feat: добавлены проверка модели, прогон февраля и защита записиUssenko Andrey
- 2026-09-23 16:16Brand site as Wind Power ForecastingMG-Trener
- 2026-09-23 16:04Fix Google Maps initialization raceMG-Trener
- 2026-09-23 15:59Fix geography page and document Maps setupMG-Trener
- 2026-09-23 15:53Integrate geography view with current dev APIMG-Trener
- 2026-09-23 15:49Merge remote-tracking branch 'origin/dev' into devMG-Trener
- 2026-09-23 15:45Add Google Maps geography viewMG-Trener
- 2026-09-23 15:45Описать формулы показателей и путь обучения моделиUssenko Andrey
- 2026-09-23 15:39Исключена утечка будущих данных и обновлены инструкцииUssenko Andrey
- 2026-09-23 15:27Добавлены интерфейс данных и общие ревизии моделейUssenko Andrey
- 2026-09-23 15:18Добавлены загрузка CSV, турбины и ревизии модели в APIUssenko Andrey
- 2026-09-23 15:00Согласовать запуск Supabase с обновлением интерфейсаUssenko Andrey
- 2026-09-23 14:59fix: добавлен запуск API с готовой БД Supabase без повторного DDLMG-Trener
- 2026-09-23 14:59Объединить интерфейс и улучшение запуска Supabase в devUssenko Andrey
- 2026-09-23 14:58Ускорить повторный запуск БД и показать активный backendUssenko Andrey
- 2026-09-23 14:53merge: обновлён интерфейс аналитики в общей ветке devMG-Trener
- 2026-09-23 14:49feat: улучшена читаемость аналитики и добавлена сворачиваемая навигацияMG-Trener
- 2026-09-23 14:32Уточнить подключение Supabase и проверку адреса БДUssenko Andrey
- 2026-09-23 14:29Добавить проверяемый перенос SQLite в SupabaseUssenko Andrey
- 2026-09-23 14:21Добавить повторяемую инициализацию БД и инструкцию локального запускаUssenko Andrey
- 2026-09-23 14:14docs: уточнён статус продуктовой спецификацииUssenko Andrey
- 2026-09-23 14:12merge: добавлена спецификация Михаила и согласован контрактUssenko Andrey
- 2026-09-23 14:11merge: объединён интерфейс Михаила с backend АндреяUssenko Andrey
- 2026-09-23 14:04test: проверены сценарии аналитики и добавлена инструкция запускаMG-Trener
- 2026-09-23 14:04feat: добавлен интерактивный интерфейс аналитики ВЭСMG-Trener
- 2026-09-23 14:03docs: указаны источники и лицензии компонентовUssenko Andrey
- 2026-09-23 14:01ops: подготовлен серверный запуск API и инструкцииUssenko Andrey
- 2026-09-23 13:53fix: исправлен импорт времени и закреплены зависимостиUssenko Andrey
- 2026-09-23 13:50docs: добавлена продуктовая спецификация фронтенда ВЭСMG-Trener
- 2026-09-23 13:49feat: добавлен каркас прогноза ВЭС и план разработкиUssenko Andrey
Структура репозитория (67 файлов)
.env.example .gitignore FRONTEND_PRODUCT_SPEC.md INTEGRATION.md PLAN.md README.md TZ/Dataset HackAlemAI для участников 11.03.2023-28.02.2026 - turbine 1.csv TZ/Dataset HackAlemAI для участников 11.03.2023-28.02.2026 - turbine 2.csv TZ/HackAlem AI_ Agentic AI для прогнозирования выработки ВЭС.docx backend/__init__.py backend/app/__init__.py backend/app/bootstrap.py backend/app/db.py backend/app/main.py backend/app/pipeline.py backend/app/turbines.py backend/app/weather.py backend/cli.py backend/evaluate.py backend/forecast_range.py backend/generate_demo_csv.py backend/migrate_sqlite_to_postgres.py backend/tests/__init__.py backend/tests/test_api.py backend/tests/test_bootstrap.py backend/tests/test_db.py backend/tests/test_migration.py backend/tests/test_pipeline.py backend/tests/test_startup.py deploy/README.md deploy/healthcore-api.service deploy/nginx-healthcore.conf deploy/server.env.example docs/01-participation-eligibility-rules-ru.pdf docs/02-participant-instructions-ru.pdf docs/03-openai-nvidia-activation-ru.pdf docs/04-equipment-preparation-ru.pdf docs/05-cybersecurity-guidelines-ru.pdf docs/06-asu-online-course-ru.pdf docs/google-doc-index.md docs/regulations-summary-2026-09-22.md docs/research-notes-2026-09-22.md docs/screenshots/forecast-dashboard.png docs/screenshots/wind-map.png docs/text/01-participation-eligibility-rules-ru.txt docs/text/02-participant-instructions-ru.txt docs/text/03-openai-nvidia-activation-ru.txt docs/text/04-equipment-preparation-ru.txt docs/text/05-cybersecurity-guidelines-ru.txt docs/text/06-asu-online-course-ru.txt docs/user-brief-2026-09-22.md frontend/.env.example frontend/README.md frontend/index.html frontend/package-lock.json frontend/package.json frontend/public/brand-mark.svg frontend/src/GeographyMap.js frontend/src/main.js frontend/src/model.js frontend/src/style.css frontend/src/windWeather.js frontend/tests/browser.cjs frontend/tests/model.test.js frontend/tests/windWeather.test.js frontend/vite.config.js requirements.txt
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.