Metadata-Version: 2.4
Name: trosna-weighted-regression
Version: 0.1.0
Summary: Time-weighted linear regression with protection against model 'falling asleep' on abrupt changes
Keywords: time-series,regression,weighted-least-squares,change-point,scikit-learn,forecasting,virtualization
Author: Andrey Ferubko
Author-email: Andrey Ferubko <ferubko1999@yandex.ru>
License-Expression: MIT
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Dist: numpy>=1.26
Requires-Dist: scikit-learn>=1.6
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/89605502155/trosna-weighted-regression
Project-URL: Repository, https://github.com/89605502155/trosna-weighted-regression
Project-URL: Documentation, https://github.com/89605502155/trosna-weighted-regression#readme
Project-URL: Issues, https://github.com/89605502155/trosna-weighted-regression/issues
Description-Content-Type: text/markdown

# trosna-weighted-regression

[![PyPI](https://img.shields.io/pypi/v/trosna-weighted-regression)](https://pypi.org/project/trosna-weighted-regression/)
[![Python](https://img.shields.io/pypi/pyversions/trosna-weighted-regression)](https://pypi.org/project/trosna-weighted-regression/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/89605502155/trosna-weighted-regression/blob/main/LICENSE)

Исходный код: <https://github.com/89605502155/trosna-weighted-regression>

Взвешенная по времени линейная регрессия с защитой от «засыпания» при резких изменениях.
Чистый Python (numpy + scikit-learn), эстиматор совместим со sklearn.

```bash
pip install trosna-weighted-regression
```

## Проблема

Базовая модель (Ферубко, Казаков, Вестник РГРТУ, 2026, № 95, с. 73–84): прямая строится по 5 последним
точкам, вес точки `w_i ∝ |t − t_i|^(−n)` (метрическое ядро) или `exp(−(t − t_i)² / 2h²)` (экспоненциальное),
`t` — момент следующего мониторинга. После долгой полки или спада старые точки окна тянут наклон назад:

| Сценарий (прогноз на шаг вперёд, n = 1) | Истинные значения        | Без защиты                   | С защитой                  |
|-----------------------------------------|--------------------------|------------------------------|----------------------------|
| Ступенька 1000 → 1500                   | 1500 1500 1500 1500 1500 | 1000 1541 1676 1676 1608     | 1000 1500 1500 1500 1500   |
| Спад, затем рост +100/шаг               | 910 1010 1110 1210 1310  | 800 909 1048 1186 1310       | 800 910 1110 1210 1310     |
| Всплеск на одну точку                   | 1500 1000 1000 1000 1000 | 1000 1541 1135 1000 932      | 1000 1500 1000 1000 1000   |
| Провал на одну точку                    | 500 1000 1000 1000 1000  | 1000 459 865 1000 1068       | 1000 1000 1000 1000 1000   |

Первую точку скачка не предсказывает никто — дело в том, что происходит после неё.

## Защита

1. **Детектор разладки.** Новая точка сравнивается с прогнозом на неё. Ошибка нормируется на своё стандартное
   отклонение: шум точки σ, умноженный на `√(1 + Σc_i²)`, где `ŷ = Σc_i·y_i` — прогноз (по двум свежим точкам
   он втрое шумнее одной точки, по пяти — заметно тише). σ — медиана модулей последних `scale_window`
   нормированных остатков, но не ниже `min_scale` (на ровной полке оценка была бы нулевой). Ошибка больше
   `threshold` — разладка.
2. **Пробуждение.** При разладке точки до неё выбрасываются, окно строится заново с точки разладки. Пока у
   нового режима меньше 5 остатков, шум берётся из прежнего режима — детектор не слепнет и ловит второй
   скачок сразу за первым. Повторные разладки в это время учитываются в оценке шума, поэтому переход в более
   шумный режим не вызывает бесконечного каскада перестроек.
3. **Излом тренда.** Пока в новом окне одна точка, прогноз — её уровень. Следующая точка, легшая на прямую от
   последней точки прежнего режима через точку перестройки, — продолжение разворота (долгий спад → рост), и
   модель сразу идёт по новому наклону. Точка, не легшая ни на уровень, ни на излом, — новая разладка: так
   ложное срабатывание, за которым следует настоящий скачок, не удваивает скачок в прогнозе.
4. **Асимметрия.** Рост принимается сразу (`confirm_up=0`): недодать память опаснее, чем выдать лишнюю. Спад —
   только после `confirm_down=1` подтверждающей точки, до этого модель держит прежний уровень.
5. **Откат всплесков.** Если после немедленной перестройки следующая точка вернулась к старому режиму, старое
   окно и оценка шума восстанавливаются, а всплеск отбрасывается (`spikes_`). Неподтверждённый спад тоже
   отбрасывается.

`guard=False` — базовая модель из статьи без защиты, для сравнения.

## Дополнительные механизмы

По умолчанию выключены или не меняют прогноз: базовые настройки дают модель из статьи плюс защиту выше.

6. **Адаптивное окно (ICI)** — `adaptive_window=True`, `window` — наибольшая длина окна. На каждом прогнозе
   строятся прямые по последним 3, 5, 8, 12, … точкам режима, и берётся самое длинное окно, интервал прогноза
   которого (±`ici_gamma`·σ·√Σc²) ещё пересекается с интервалами всех более коротких. На ровном участке окно
   растёт и прогноз тише, на изломе короткие окна расходятся с длинными, и окно сразу укорачивается. Правило
   пересечения доверительных интервалов — Гольденшлюгер и Немировский, Катковник (LPA-ICI).
7. **Накопительный детектор CUSUM** — `cusum_h` (порог, `None` — выключен) и `cusum_k` (допуск). Те же
   нормированные ошибки прогноза копятся по Пейджу: `S⁺ = max(0, S⁺ + z − k)`, отдельно вниз. Ловит
   устойчивые сдвиги меньше `threshold`, на которых длинное адаптивное окно засыпало бы. При срабатывании окно
   строится заново с точки, с которой начала копиться сумма.
8. **Интервал прогноза** — `predict_interval(X)` возвращает `(lower, upper)` с покрытием `coverage`.
   Полуширина — квантиль нормированных модулей последних `interval_window` ошибок прогноза на шаг вперёд,
   умноженный на шум прогноза в точке. Уровень квантиля подстраивается онлайн (адаптивный конформный вывод,
   Гиббс и Кандес): после промаха интервал расширяется, после попадания понемногу сужается. Шаг подстройки —
   `aci_rate`, текущий уровень промахов — `interval_alpha_`. Верхняя граница — объём ОП, который стоит
   зарезервировать.

Рекомендуемый набор: `WakefulWLS(window=30, adaptive_window=True, cusum_h=5.0)`.

## Проверка на шуме

Монте-Карло: полка, спад или рост длиной 200 точек, затем скачок; шум σ = 5, по 10 запусков на конфигурацию,
ядра метрическое и экспоненциальное, окна 5 и 10. «Шагов» — сколько шагов после скачка прогноз ещё выходит за
4σ от истины.

| Скачок                          | Шагов: без защиты / с защитой | Средняя ошибка, МБ: без / с |
|---------------------------------|-------------------------------|-----------------------------|
| Вверх на 30σ (150 МБ)           | 5–8,4 / **1**                 | 8,2–8,5 / 3,9–5,1           |
| Вверх на 120σ (600 МБ)          | 5–10 / **1**                  | 19,7–25,9 / 3,9–5,1         |
| Вниз на 30σ и 120σ              | 5–10 / 2                      | примерно равны              |
| Пограничный, 6σ (окно 10)       | 1 / 1,7–1,8                   | 3,8–4,4 / 4,0–4,8           |

Спад занимает 2 шага, потому что так задано (`confirm_down=1`, `0` — реагировать сразу). На пограничных скачках
защита чуть уступает: после перестройки прогноз строится по 2–3 свежим точкам и шумнее. Ложные срабатывания на
стационарном гауссовом шуме — около 1 на 1000 точек, на шуме с тяжёлыми хвостами (t₃) — около 11; почти все они
откатываются как всплески на следующей же точке.

### Дополнительные механизмы

Тот же стенд, метрическое ядро. «База» — `window=5`, «защита» — `window=5` с защитой, «ICI» — плюс
`window=30, adaptive_window=True`, «ICI + CUSUM» — плюс `cusum_h=5`. Ячейки — шагов до точного прогноза /
средняя ошибка, МБ.

| Сценарий                                   | База      | Защита    | ICI       | ICI + CUSUM |
|--------------------------------------------|-----------|-----------|-----------|-------------|
| Стационарный шум, ошибка, МБ               | 4,6       | 4,6       | 2,2       | 2,2         |
| Скачок вверх на 30σ                        | 5 / 8,2   | 1 / 5,1   | 1 / 3,4   | 1 / 3,4     |
| Скачок вверх на 6σ                         | 1,2 / 5,2 | 1,5 / 5,3 | 1 / 3,6   | 1 / 3,5     |
| Скачок вниз на 6σ                          | 1,1 / 5,2 | 1,5 / 5,5 | 1,6 / 4,1 | 1,6 / 4,0   |
| Новый наклон 1σ/шаг после полки            | 0 / 4,9   | 0 / 4,9   | 0–1 / 5,2 | 0 / 3,9     |
| Новый наклон 3σ/шаг после полки            | 0,6–0,8 / 5,1–5,2 | 0,6–0,9 / 5,2–5,3 | 2,8–5,2 / 5,5–6,5 | 1,8–2,2 / 4,2 |
| Новый наклон 10σ/шаг после полки           | 3 / 6,6   | 2 / 6,2   | 2 / 4,6   | 2 / 4,6     |

Адаптивное окно вдвое снижает ошибку на спокойных участках и после скачков. Слабое место — излом наклона
порядка 3σ за шаг: без CUSUM длинное окно выходит на новый наклон на 2–4 шага позже короткого. CUSUM сокращает
отставание до 1–1,5 шага, а средняя ошибка с ним ниже, чем у окна из 5 точек. С окном из 5 точек CUSUM почти ничего не
даёт: короткое окно и так догоняет за несколько шагов. Экспоненциальное ядро (h = 3) само почти не смотрит
дальше 6–8 точек, поэтому выигрыш от ICI у него скромнее: около 20 % на шуме.

Интервал на 95 % (6 запусков по 1500 точек):

| Ряд                                         | Покрытие, без подстройки / с ней | Покрытие 10 шагов после скачка |
|---------------------------------------------|----------------------------------|--------------------------------|
| Гауссов шум                                 | 0,951 / 0,949                    | —                              |
| Шум t₃ с тяжёлыми хвостами                  | 0,950 / 0,950                    | —                              |
| Скачки каждые 150 точек, шум вырос втрое    | 0,937 / 0,949                    | 0,87 / 0,91                    |

С адаптивным окном интервал на 25–30 % уже при том же покрытии.

## Использование

```python
import numpy as np
from trosna_weighted_regression import WakefulWLS

t = np.arange(len(mem), dtype=float)[:, None]  # одна колонка времени
model = WakefulWLS(window=5, kernel="metric", power=1, min_scale=10).fit(t, mem)  # mem в МБ
model.predict([[t[-1, 0] + 1]])  # прогноз на следующий мониторинг
model.change_points_, model.spikes_  # где модель «проснулась» и какие всплески отбросила

# Поток: дообучение по мере поступления точек, эквивалентно fit на всём ряде
model.partial_fit([[t_new]], [mem_new])

# Адаптивное окно + CUSUM; верхняя граница 95 % интервала — сколько памяти зарезервировать
model = WakefulWLS(window=30, adaptive_window=True, cusum_h=5.0, min_scale=10).fit(t, mem)
lower, upper = model.predict_interval([[t[-1, 0] + 1]])
```

Работает в `GridSearchCV` (с `TimeSeriesSplit`), `Pipeline`, `clone`. X — ровно одна колонка времени,
поэтому стандартные проверки sklearn, подающие несколько признаков, помечены в тестах как ожидаемые падения;
остальные проходят.

## Разработка

```bash
git clone https://github.com/89605502155/trosna-weighted-regression.git
cd trosna-weighted-regression
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .
```

## Лицензия

MIT, см. [LICENSE](https://github.com/89605502155/trosna-weighted-regression/blob/main/LICENSE).
