P3. Воспроизводимый конвейер «данные → результат → сайт»¶
Задача¶
Подготовить скрипт, который читает исходные данные эксперимента, выполняет расчёт и формирует графики и таблицы, и встроить его вызов в сборку сайта. Показать три вещи: обновление сайта при изменении данных, кэширование без лишнего пересчёта и метку версии на странице.
Данные¶
Входные данные — 8 CSV-логов взгляда из VR-музея, записанных 16 сентября 2026 года моей пилотной системой одной кнопкой (gaze_session_20260916_*.csv). Формат: timestamp, gaze_x, gaze_y, gaze_z (вектор направления взгляда), pupil (диаметр зрачка), openness (раскрытие глаза). Всего 17 132 строки, 206 секунд записи. Файлы лежат в data/raw/.
Что показал первый просмотр данных:
- Частота записи в семи сессиях около 72 Гц, в самой первой — около 370 Гц.
- Первая сессия (8 с) полностью статична: вектор взгляда не меняется ни в одной строке. Скрипт помечает такие сессии как «статичные» и не считает для них эффект стабилизации.
- Столбцы
pupilиopennessво всех сессиях равны 1,0 и информации не несут, поэтому в расчёте не используются. - В пяти сессиях из восьми есть мгновенные разрывы направления взгляда, всего 32: между соседними кадрами вектор поворачивается на 42–174°, большинство — на 125–175°. Для глаз и головы это физически невозможно (при 72 Гц это тысячи градусов в секунду), поэтому я считаю такие шаги разрывом сигнала.
Скрипт терпим к особенностям выгрузки: сам определяет разделитель, регистр названий колонок, BOM в начале файла и единицы времени (секунды, миллисекунды, микросекунды, наносекунды).
Схема конвейера¶
В Makefile расчёт — зависимость сборки: make strict сначала вызывает scripts/analyze.py, затем mkdocs build --strict. В CI те же две команды выполняются на чистом раннере.
Что считает скрипт¶
Направление взгляда переводится в два угла (горизонтальный и вертикальный, градусы). Шаги больше 30° между соседними кадрами вырезаются, а оставшиеся сегменты склеиваются: иначе ступенька попадает в спектр и ложно увеличивает «тремор». Затем сигнал приводится к равномерной сетке по медианному шагу времени и считаются:
- тремор — RMS сигнала в полосе 4–12 Гц (полосовой фильтр Баттерворта 2-го порядка) по обоим углам, градусы;
- джиттер — RMS всего, что быстрее 2 Гц; сюда входят и быстрые произвольные движения головы, поэтому как оценку тремора я использую его осторожно;
- частота пика спектра в полосе тремора (метод Уэлча), только если в полосе есть настоящий локальный максимум;
- медианная угловая скорость взгляда;
- те же метрики после стабилизатора — фильтра One Euro (
mincutoff=1.0,beta=0.001,dcutoff=1.0). Это простой эталонный фильтр для сравнения, а не моя обученная модель-стабилизатор.
Результаты¶
| Параметр | Значение |
|---|---|
| Коммит | 825dc76 |
| Дата сборки (UTC) | 2026-10-05 12:13:46 |
| Версия набора данных | pilot-2026-09-16 |
| SHA-256 набора данных | 4a161632da0e2ed9… |
| Файлов / строк | 8 / 17132 |
| Версия скрипта | 1.0 |
Сводная таблица по сессиям¶
| Сессия | Строк | Длит., с | Частота, Гц | Разрывов | Скорость, °/с | Тремор до, ° | Тремор после, ° | Снижение, % | Джиттер >2 Гц до, ° | Джиттер после, ° | Пик, Гц |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 20260916_205941 (статичная) | 2938 | 8,0 | 370,4 | 0 | 0,0 | 0,000 | 0,000 | — | 0,000 | 0,000 | — |
| 20260916_210658 | 2866 | 39,9 | 71,9 | 11 | 19,3 | 0,369 | 0,242 | 34,4 | 2,576 | 1,712 | 5,3 |
| 20260916_210915 | 471 | 6,5 | 71,9 | 0 | 4,3 | 0,157 | 0,044 | 72,2 | 0,408 | 0,241 | 8,3 |
| 20260916_210929 | 2575 | 35,9 | 71,9 | 11 | 23,3 | 0,375 | 0,159 | 57,5 | 2,209 | 1,317 | 6,5 |
| 20260916_211104 | 326 | 4,8 | 71,9 | 0 | 8,5 | 0,156 | 0,038 | 75,8 | 0,576 | 0,372 | 4,8 |
| 20260916_211118 | 2898 | 40,4 | 71,9 | 6 | 20,2 | 0,370 | 0,133 | 64,0 | 1,946 | 1,041 | 4,8 |
| 20260916_212632 | 2678 | 37,4 | 71,9 | 3 | 28,7 | 0,232 | 0,087 | 62,6 | 1,137 | 0,877 | 4,8 |
| 20260916_212720 | 2380 | 33,1 | 71,9 | 1 | 36,5 | 0,202 | 0,085 | 57,8 | 1,113 | 0,841 | 5,3 |
| Итого / среднее по 7 сессиям с движением | 17132 | 206,0 | 71,9 | 32 | 20,1 | 0,266 | 0,113 | 60,6 | 1,423 | 0,914 | — |
«Тремор» — RMS в полосе 4–12 Гц, «Снижение» показывает, насколько фильтр уменьшил этот показатель. Средние значения посчитаны только по сессиям, где взгляд двигался.
Графики¶


Чувствительность к параметру фильтра¶
Параметр beta определяет, как быстро фильтр ослабляет сглаживание при быстром движении. Я проверила три значения на всех сессиях с движением:
| beta | Среднее снижение тремора, % | Сессий со снижением | Средняя задержка, мс |
|---|---|---|---|
| 0 | 78,7 | 7 из 7 | 123 |
| 0,001 | 60,6 | 7 из 7 | 91 |
| 0,007 | 13,4 | 4 из 7 | 52 |
При beta=0 фильтр — обычный низкочастотный с частотой среза 1 Гц: тремор подавляется сильнее всего, но взгляд отстаёт примерно на 120 мс, и для навигации в VR это много. При beta=0,007 задержка вдвое меньше, но при быстрых поворотах головы фильтр почти перестаёт сглаживать: в трёх сессиях из семи тремор в полосе 4–12 Гц даже вырос. Значение 0,001 я оставила как компромисс между подавлением тремора и задержкой. Подбор параметров сделан на тех же данных, на которых измерен результат, поэтому цифры — оценка возможностей простого фильтра, а не проверка на новых данных.
Как воспроизвести¶
Демонстрация требований P3¶
1. Изменение данных → push → обновление сайта¶
В CI на каждый push в main запускается scripts/analyze.py, поэтому новая версия CSV в data/raw/ меняет таблицу, графики и метку версии без ручных действий.
Проверила на реальном запуске. Перед изменением сайт был собран из коммита 83e27e3: 8 файлов, 17 132 строки, SHA-256 набора данных 4a161632da0e2ed9….

Затем я удалила из data/raw/ одну сессию (gaze_session_20260916_210658.csv, 2 866 строк, 11 разрывов) и запушила коммит ade4237 в main. Запуск №7 прошёл за 1 мин 40 с, сайт обновился без ручных действий: в метке версии стали коммит ade4237, 7 файлов, 14 266 строк (17 132 − 2 866) и другой SHA-256 набора данных c7d0a833f36cbf8e…. Из сводной таблицы пропала строка этой сессии.

После проверки я вернула файл коммитом-откатом (git revert ade4237), поэтому в текущей версии сайта снова 8 сессий и 17 132 строки.
2. Кэширование и замер выигрыша¶
Скрипт считает ключ кэша из хеша SHA-256 входных CSV, хеша самого скрипта и параметров обработки. Если ключ совпал с сохранённым в results/.cache.json, тяжёлый расчёт пропускается, обновляются только коммит и дата в метке версии.
Замер локальный (make bench): три запуска с --force и три повторных без изменений на моих 8 сессиях:
| Режим | Время расчёта, с | Что происходит |
|---|---|---|
Холодный (--force) |
0,72 / 0,75 / 0,91 | чтение CSV, расчёт метрик, три прогона фильтра, графики |
| Тёплый (кэш) | 0,01 / 0,01 / 0,01 | совпал ключ — пересчёт пропущен |
Замер сделан в облачной среде подготовки, а не на моём компьютере и не на раннере CI. На 17 тысячах строк выигрыш небольшой в абсолютных секундах; кэш станет заметным на датасете из 50 участников, где расчёт займёт минуты.
Замер в CI на GitHub Actions (job lint, раннер ubuntu-latest, Python 3.11) для двух первых запусков:
| Запуск | Кэш pip | Install dependencies | Расчёт | Весь job lint |
|---|---|---|---|---|
№1, коммит 46be2a4 |
нет (первый запуск, кэш сохраняется) | 27 с | 4 с | 41 с |
№2, коммит 7977203 |
попадание (109 МБ восстановлено) | 19 с | 2 с | 28 с |
Кэш зависимостей (cache: pip в actions/setup-python) сократил установку на 8 секунд, а весь job на 13 секунд (около 32%). Расчёт в CI занимает 2–4 секунды при обоих запусках: кэш результатов лежит в results/.cache.json, этот файл в репозиторий не коммитится, поэтому на чистом раннере расчёт каждый раз выполняется заново. Разница между 4 и 2 секундами — разброс раннера, а не эффект кэша. Для двух запусков это одиночные замеры, статистики из них не получится.
3. Метка версии на странице¶
Блок «Метка версии» выше формируется скриптом и содержит хеш коммита, дату сборки в UTC, версию набора данных (из файла data/DATASET_VERSION) и SHA-256 всех входных CSV. По нему опубликованный результат однозначно связывается с версией кода и данных.
Ограничения¶
- Таблица и графики описывают одну пилотную запись одного автора (8 сессий, 206 секунд). Сравнение групп с ОВЗ и без ОВЗ — задача будущего сбора данных, пилотных данных для него недостаточно.
- Метки «тремор» и «джиттер» — оценки по сигналу взгляда в градусах и по спектру; в данных остаются быстрые произвольные движения головы, поэтому для клинических выводов эти значения не предназначены.
- Порог разрыва 30° выбран по виду данных: обычные шаги между кадрами не превышают 7°, а следующие по величине — 42° и больше, так что значения между 7° и 42° в записи не встречаются. Причину разрывов я не устанавливала.
- Стабилизатор в расчёте — эталонный фильтр One Euro, а не обученная модель; сравнение с моделью — отдельная работа.