идентичность эпизода — интервал: блокер снят измерением

- находка 47: ключ (date,start,end) даёт 174 координаты сна против 170 по
  метке и ноль столкновений внутри доставки против 33; value в ключе лишний
- UUID в выгрузку Apple не попадает, другой идентичности у эпизода нет
- поэпизодный sleep_analysis вернулся в scope razbor-metrik-v-obekty
This commit is contained in:
av
2026-08-01 17:09:45 +03:00
parent 809153e03f
commit 3c9d226a34
11 changed files with 159 additions and 122 deletions
+53
View File
@@ -1459,6 +1459,59 @@ type="HKWorkoutEventTypeMotionPaused|Resumed" ← совпадение по
следующего проверенного экспорта, иначе между концом ретеншена и датой
снапшота в журнале образуется дыра.
## 47. Идентичность эпизода сна — `start` и `end`, и другой у Apple нет
Координата `метрика + слой + метка` для поэпизодного `sleep_analysis` неверна:
под одной меткой лежит до трёх разных эпизодов. Замер по всем 94 доставкам
(1880 эпизодных точек, суточные сводки исключены):
| Ключ | Координат | Схлопнуто точек | Дублей **внутри одной** доставки |
|---|---|---|---|
| `date` | 170 | 1710 | 33 |
| `date + start + end` | 174 | 1706 | **0** |
| `date + start + end + value` | 174 | 1706 | 0 |
`start`/`end` закрывают всё; `value` в ключе не добавляет ни одной координаты.
Сильнее: из 174 координат **ни одна не несёт двух разных содержимых** — ни с
`source`, ни без него. Правило разрешения столкновений на эпизодном сне за весь
корпус не срабатывает ни разу.
Внутридоставочные дубли важнее междоставочных: там `received_at` общий, и любой
тай-брейк по времени приёма неприменим в принципе — исход решал бы порядок
элементов в JSON-массиве, а он нестабилен (находка 2).
**Это не дефект данных, а задуманное представление.** Записи с одним `startDate`
и разными `endDate` — вложенность «в кровати» и фазы сна внутри неё; HealthKit
намеренно допускает перекрывающиеся сэмплы, чтобы выразить «в кровати» и
«спит» одновременно.
**Другой идентичности Apple не даёт.** Атрибуты записи сна в экспорте:
```
type sourceName sourceVersion creationDate startDate endDate value
```
`HKObject.uuid` существует в API, но **в выгрузку не попадает**. Значит модель
идентичности обязана выражаться через `start`/`end`: иначе `import(экспорт)` не
сойдётся с `replay(HAE)` и журнал перестанет быть журналом.
### Как это решают другие
- **Health CSV Importer** дедуплицирует по `Start Date + End Date + Data Type +
Type Identifier`. Совпадает с нашим замером, включая отсутствие источника в
ключе.
- **Ингесторы поверх InfluxDB** (`irvinlim/apple-health-ingester`,
`joeecarter/health-import-server`) ключуют по `measurement + tags + timestamp`
— то есть имеют ровно эту коллизию и разрешают её молчаливым last-write-wins
движка. Один обходит её тем, что кладёт сон плоской точкой
(`inBed`/`inBedStart`/`inBedEnd`), то есть поддерживает только суточную схему.
- **Пайплайн HAE → FastAPI → Postgres** (ladvien) даёт каждой записи
суррогатный UUID-PK без уникального ограничения: обещанная идемпотентность не
реализована, повторная доставка задваивает строки.
То есть обе распространённые схемы хранения теряют данные ровно там, где мы это
измерили, а единственная принятая схема дедупликации — это `start`+`end`.
## Инструмент
Разбор ведётся скриптом `tmp/research/hl.py` (Python 3, только стандартная