Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
20.12.2023
Дев-адвент 20: інкрементальне оновлення — це складно
З вчора, як писав, залишилась задача зробити інкрементальне оновлення. Тобто нові виміри я вже зрозумів, як отримувати, але ж треба їх застосувати до вже обчислених з мінімальними витратами.
Оновлення приходять з HKAnchoredObjectQuery у вигляді двох списків: доданих записів та видалених записів. Ну, хоч редагування в Apple Health немає. Зате користувач може додати або видалити записи з будь-якого місця історії — наприклад, імпортувати старі дані.
В загальному вигляді інкрементальне оновлення зробити складно. Тому беру урок у Redshift та використовую інкрементальне оновлення тільки в прямолінійному випадку. Для інших випадків роблю повне обчисленння, яке вже готово і працює.
Для мого алгоритму “прямолінійний” випадок — це поява даних на наступні дати. В тому сенсі, що така зміна і є інкрементальною. Але також і в тому, що саме таким буде типове щоденне оновлення.
Мій внутрішній перфекціоніст каже, що належно обробити кожну можливу ситуацію та зробити алгоритм інкрементальним для всіх випадків. Втім мій внутрішній менеджер заперечує: на це немає ані часу, ані потреби.
До того ж оновлення нелегко зробити й в простому випадку, бо треба боротися з датами, а типу календарної дати у Swift немає, і може варто було б вже зробити його власноруч.
19.12.2023
Дев-адвент 19: збереження стану
Як вже казав, завантаження даних з Apple Health та їх обробка займають невеличкий, але помітний проміжок часу. Це значить, що старт програми гальмує. Розвʼязок простий — зберігати вже оброблені дані, щоб не обчислювати все наново.
Саме збереження-відновлення зробити нескладно. Обрав як метод серіалізацію в JSON. У Swift прості структури можна серіалізувати, якщо навісити на них тип Codable - решта робиться автоматично. Я вже писав, що запис у файл — це метод на рядку, що дуже дивно. Насправді ще є клас FileHandle з більш звичним API читання-запису.
Нехитре збереження стану в файл JSON роблять завантаження під час запуску непомітним. Ніби справа зроблена — але не до кінця. Потрібно ще доповнити дані тими, що могли зʼявитися після останнього запуску програми.
На цей час знайшов брутальне рішення — роблю обчислення в повному обсязі після того, як завантажу початковий стан. Такий підхід вигідний своєю простотою, але даремно витрачає батарею, та й займає час — тобто оновлення даних все ж доведеться чекати, хіба що не дивлячись в порожній екран.
Проте хочеться натомість отримувати тільки нові дані; при чому алгоритм готовий їх прийняти без повного переобчислення. Але як взяти нові дані? Виявилося, що в Apple Health є спеціальний тип запиту саме для отримання нових (для нас) даних - HKAnchoredObjectQuery. Цей запит повертає значення-“якір”, яке можна навіть зберігати між запусками програми. Також його можна виконувати в режимі спостереження, тобто замінити HKObserverQuery, про який я писав.
18.12.2023
Дев-адвент 18: заповнення порожнеч на графіку
Мій графік ваги (та весь підхід) розрахований на щоденні зважування. Але, звісно, трапляється таке, що зважуватися не виходить — наприклад, в подорожах. Виникає питання, що робити з графіком, якщо на цей день немає реальних даних.
Хоч “реальне” значення ваги обчислюється за методом рухомого середнього, але складники цього середнього завжди беруться з попередніх дат — тож якщо даних на дату немає, отримаємо не інтерполяцію, а просто майже сталу лінію — обчислене значення наближатиметься до останнього відомого, от і все.
Коли йдеться про найсвіжіші дні, то можна взагалі нічого не малювати, та чекати, поки ми повернемось з відпустки та внесемо наступну вагу. А от з порожнечею між відомими значеннями вже можна щось зробити.
Є два випадки. Якщо порожнеча довша за вікно рухомого середнього, то наступне після неї значення буде, фактично, початком нового графіку. Таку порожнечу краще нічим не заповнювати, а навпаки, показати як розрив. Це трохи технічно складно, тому поки я цим не займався.
Інший випадок — коли значення після порожнечі все ще коректується минулими значеннями (як на ілюстрації.) Як можна побачити, наївний алгоритм просто робить стрибки в графіку там, де зʼявляється нова інформація. Більш природно інтерполювати обчислене, “реальне” значення, що я й зробив.
Все це суттєво складніше, ніж просто зʼєднувати точки лінією, як це робить Apple Health. Зате отримуємо: 1) наочну динаміку та 2) відсутність штучних стрибків, які цю наочність підривають та примушують нас вмикати мозок.
17.12.2023
Дев-адвент 17: фокусування
Зрозумів, що якщо я хочу зробити привабливий MVP та ще й в найближчі 8 днів, доведеться обмежити функціонал до дійсно мінімального. Це значить, залишити тільки графік та аналітику по ньому.
*(Треба памʼятати, що продукт з однієї чудової функції більш привабливий ніж з однієї чудової та пʼяти недороблених. Дивись ефект менше-краще.)_
Тож зараз працюю над генерацією текстів пояснення за результатами аналізу (такими, як “скидаєш вагу з 10 грудня, такими темпами досягнеш мети через 2 місяці.”.)
До речі, все ж відмовився від регресії як оцінки проміжків. Окрім того, що це повільно, як писав вчора, то ще й результати не краще ніж мій попередній метод. В деяких випадках - навіть гірше, бо найкраща регресія в мене не обовʼязково закінчувалася на піках графіку.
А регресія знадобиться для визначення поточного тренду. Тобто для того, для чого її придумали.
16.12.2023
Дев-адвент 16: інтеграція регресії
Отже, вчора обрав влучні параметри функції ціни, щоб покривати графік відрізками регресії. Залишилось повернутися назад в головну програму та зробити робочу версію.
В чому проблема: я писав, що придумав, як уникнути кубічної складності при обчисленні розвʼязка. Поясню. Сам алгоритм динамічного програмування має квадратичну складність: один цикл для збільшення діапазону розвʼязку, а всередині — ще один, для перебору з розвʼязків для менших діапазонів.
Регресію я знайшов, як робити паралельно з внутрішнім циклом. Але оцінка якості регресії — теж цикл; необхідно порівняти значення в кожній точці. Виходить, що щоб оцінити, який розвʼязок найкращий, доведеться зробити ще один цикл — оцінки регресії.
Квадратична складність на декількох тисячах точок — не проблема для сучасного процесору, а кубічна — вже займає помітний час, декілька секунд.
Все це значить банально те, що розвʼязок потрібно зберігати, а не обчислювати наново щоразу. Бо минула вага є константою, тож і розбиття на відрізки теж. Хіба що у випадку редагування або імпорту старих даних доведеться обчислити повторно.
А решта мені подобається — модель нарешті математично акуратна.
15.12.2023
Дев-адвент 15: цінність прототипування
📈 Нарешті, взяв експортовані дані та зробив з ними тестовий каркас. Щоправда, обрав не Excel, а JavaScript та D3.js, бо тут мені звичніше.Отримав за годину більше прогресу ніж за дні абстрактних міркувань та розглядання чисел.
Щоб зробити дещо просте в D3, достатньо одного файлу HTML. Скрипт — там же ж. Дані — там же ж. Ніякої збірки. Prettier легко впорається з автоформатуванням. (До речі, щоб не чіпати CSV, перетворив їх на JSON через один рядок Ruby.)
Відтворив алгоритм на JavaScript, та зробив так, що кожний крок робиться або за таймером, або натисканням кнопки. Мені було потрібно зрозуміти поведінку регресії та підібрати функцію ціни. Побудова регресії на графіку робить поведінку буквально очевидною.
З функцією ціни трохи складніше, бо треба уявляти поведінку математичних функцій. Наприклад, мені вже відомо, що щоб алгоритм віддавав перевагу довшим відрізкам (замість декількох коротких), можна взяти ступінь від довжини. Але яку ступінь? Це легко зрозуміти, якщо результати відтворювати на графіку.
На мою думку, досвідчений інженер повинен вміти робити не тільки великі, красиві та акуратні рішення, а й працездатні рішення в найкоротший час. Це не тільки експериментів стосується, а й скриптів для разових дій, прототипів, досліджень.
14.12.2023
Дев-адвент 14: Експорт в CSV
Отже, експорт даних в CSV. Зробив. Фіча ця збирається з послідовності компонентів, як з кубиків.
-
Дані в мене вже є. Окрім зважувань з Apple Health, я експортую обчислене “правильне” значення, а також нотатки.
-
Для генерації CSV пішов найпростішим шляхом, тобто набираю дані в рядок з комами посередині та й усе. Ще за досвідом Сінтри дійшов того, що до CSV можна ставитись просто. Добре, коли в мові є зручна вбудована бібліотека для генерації CSV (Ruby, Go навіть), але у Swift немає. Точніше, є така можливість у класу DataFrame зі стандартної бібліотеки для машинного навчання. Спочатку навіть спробував залучити його. Але швидко відмовився через безглуздість цього підходу.
-
У Swift найбільш дивний підхід до запису в файл з тих, що я бачив, виглядає це так:
csvString.write(
to: URL.temporaryDirectory.appending(path: "diet.csv"),
atomically: true,
encoding: .utf8
)
Так, запис у файл це метод на рядку. А ще у Swift шляхи до файлів є URL.
- Такий файл буде доступний тільки моїй програмі — доступ до диска в iOS чітко розділений. Є відомий спосіб віддати файл користувачу — діалог активностей — той самий, який зʼявляється по кнопці “Поділитися”. Знайшов десять рядків коду, які покажуть цей діалог. Віддаєш діалогу файл, та користувач вже сам вільний надіслати його туди, куди потрібно — зберегти в “Файли”, відкрити прямо в Numbers, або навіть — скопіювати та вставити на ноутбук за допомогою універсального буфера обміну.
Імпорт буде складніше, бо CSV доведеться інтерпретувати.
13.12.2023
Дев-адвент 13: лінійна регресія
Згадав ще один спосіб розбивати графік на відрізки. А саме, розглядати кожний відрізок як окрему лінійну регресію.
Регресія — то, попросту кажучи, пряма, що наближує ламану, описану точками. Зрозуміло, що весь графік одною регресією не наблизити, але, оскільки алгоритм розбиття на відрізки в мене вже є, можна приєднати до нього регресію як функцію ціни та подивитися, що вийде.
Що тут таке ціна? На перший погляд, все знову очевидно — ціна — це сума розбіжностей між наближеннями з регресії та реальними значеннями. Але цього недостатньо, бо, наприклад, для двох точок регресія дає ідеальне наближення, та й з більшою кількістю буде віддавати перевагу відрізкам, де виміри однакові. (Про збіги ваг взагалі можна окремий пост.) Тобто окрім розбіжностей, ціна має враховувати довжину відрізка; чим довше, тим більше ми можемо пробачити.
(Не обовʼязково зі збільшенням числа точок розбіжності збільшуються; навпаки, якщо процес є лінійним, то більше точок будуть давати краще наближення через позбавлення від шуму. Наша задача як раз виявити в графіку більш-менш лінійні процеси. Якщо я не помиляюсь, то вага зазвичай змінюється лінійно, бо вона є функцією образа життя, який все ж має сталість.)
Поки знайшов гарну інкрементальну формулу для обчислення регресії. Інкрементальна формула для мене дуже важлива, щоб з квадратичної складності не отримати кубічну.
А ще зрозумів, що експорт даних в CSV суттєво розблокує мені дослідження, бо тоді можна буде робити експерименти не на Swift, та результати бачити не в консолі, а в Екселі — який для таких експериментів встократ краще пристосований.
12.12.2023
Дев-адвент 12: фокусування
Все ж моєю метою є випустити продукт до різдвяних канікул в App Store. Тому роблю паузу, щоб зібрати з фіч цілісну картинку.
Пропозиція програми — допомогти з контролем ваги. Для цього вона витягує з вимірів ваги якомога більше розуміння. Це: побудова графіка за методом рухомого середнього (це не є придумав, книжка є);автоматичне визначення періодів схуднення, стабільної ваги та набору ваги; та побудова тренду.
Мета всього цього: завжди мати наочне відображення ваги як процесу. На рівні тваринних рефлексів, а не аналітичного мислення. На моєму досвіді, це дійсно покращує контроль, бо відбирає можливість втекти у “все складно, я краще зараз не буду думати, а завтра все зʼясую.”
Щодо власного досвіду: звісно, цільова аудиторія це такі люди, як я. А саме, інженери, які вже стежать за вагою, але не довіряють типовим програмам через їхню ускладненість та перенасиченість функціями, першою з яких, на мою думку, є щоденник харчування. Нам би бачити простий показник та тягнути його вниз.
Доводиться балансувати між MVP, який реально зробити, та функціями, які хочеться мати самому, бодай в примітивному стані.
Одним з найскладніших аспектів програми є пристосування до користувача без історії, бо тоді вона фактично не вміє нічого, але все ж потрібно привертати увагу принаймні тиждень. Так що нагадування — в першу чергу хотілка — все ж стануть до нагоди.
11.12.2023
Дев-адвент 11: сповіщення
Раз мій метод ведення графіку потребує щоденних зважень, про них потрібно нагадувати. Для того є в iOS проста система локальних сповіщень.
З боку користувача цього не видно, але сповіщення відбуваються не тільки через виклик з інтернету (що називається push-сповіщення). Будь-яка програма здатна запланувати локальні сповіщення, які відбудуться за інтервалом, календарем, або навіть геолокацією.
Локальні сповіщення не запускають додаток, поки на них не натиснеш, тобто як і з віджетами, ми мусимо заздалегідь задати всі параметри. Поки я зробив найпростіше зі сповіщень — щоденне.
Навіть навколо найпростішого сповіщення є список задач. Спочатку потрібно запросити дозвіл — причому нам дано це зробити лише раз; якщо користувач відмовить, в наступні рази можемо тільки попросити піти до додатка “Налаштування” та увімкнути вручну.
Далі, маємо зберігати власні налаштування того, чи включені сповіщення. Для того ідеально підходить база даних UserDefaults. У Swift робота з нею виглядає приблизно так:
.onAppear {
isEnabled = UserDefaults.standard.bool(forKey: "enabled")
}.onChange(of: isEnabled) {
UserDefaults.standard.setValue(isEnabled, forKey: "enabled")
}
Та, нарешті, залишається утворити графік сповіщень — поки є дозвіл, ми можемо змінювати графік коли завгодно, тобто підтримувати його актуальність.

