До списку уроків
9 клас · Урок 11 з 16

Від сирих даних до KPI: конвеєр ETL

виконано0%
Крок 1

Що робимо сьогодні

Модуль I · IT Skills (Cloud Services & Documents) · тема 6, урок 1 з 2 · 120 хвилин

Сьогодні ти пишеш build_dash.py. Цей скрипт бере сирі відповіді з твоєї форми і перетворює їх на п'ять чисел. Завтра ці числа покаже дашборд.

У темі 2 ти зібрав ~/data/responses.csv. Це сирі дані — такі, як їх записав сервер, без жодної обробки.

Відкрий цей файл, і побачиш безлад. Кілька клітинок порожні. Два рядки однакові. Хтось написав, що робить домашку 999 хвилин. А Grist, grist і GRIST лежать як три різні відповіді, хоча інструмент один і той самий.

Дашборд не вміє з цим працювати. Йому потрібні готові числа. Між сирим CSV і готовим числом стоїть скрипт — і саме він сьогодні головний герой.

Після уроку ти вмієш

Слова ETL, KPI і викид поки нічого не означають. Розберемо їх по черзі, кожне після конкретного прикладу.

Де що виконується

ЩоДеЧому саме там
Пишеш і налагоджуєш build_dash.py у себе на комп'ютері, в редакторі зручно правити, видно помилки
Запускаєш скрипт на сервері, у своєму акаунті там лежить ~/data/responses.csv, який по HTTP не віддається
bin/build_dash.py, dash/data.json, README.md по FTP у кабінет це легкий текст — сервер такі файли приймає
Python уже стоїть на сервері Нічого встановлювати не треба: python3 доступний у твоєму акаунті, скрипт запускається від твого імені й бачить тільки твої теки. Чужі ~/data/ для нього закриті так само, як і для тебе.

Хід уроку

ЕтапХвЩо робиш
Два дашборди10 кажеш, на яке питання відповідає кожен
ETL і чому не «на льоту»20 розкладаєш свої дані на три кроки
Чистка даних і симулятор25 проганяєш конвеєр покроково, крутиш перемикач викиду
KPI і конструктор20 формулюєш 4 питання і отримуєш код під кожне
Практика: скрипт40 пишеш build_dash.py, отримуєш data.json
Перевірка чисел5 звіряєш свій підрахунок із підрахунком сервера
Разом120
Крок 2

Три кроки роботи з даними: E, T, L

Не назва програми, а спосіб розкласти роботу на три кроки, кожен з яких можна перевірити окремо.

Уяви, що готуєш вечерю. Спершу приносиш продукти з магазину. Потім миєш, чистиш і ріжеш. І аж тоді ставиш готове на стіл. Три різні дії й три різні місця: ніхто не чистить картоплю в тарілці гостя.

З даними так само — теж три кроки. Їхні англійські назви скоротили до перших літер, і вийшло слово ETL.

Extract — узяти сирі дані звідти, де вони лежать. Відкрити файл, прочитати рядки, перетворити текст на записи. На цьому кроці нічого не рахують і нічого не виправляють: завдання — просто дістати.

Transform — почистити й порахувати. Викинути сміття, звести різні написання до одного, вирішити, що робити з порожніми клітинками, і вже потім рахувати показники.

Load — покласти результат туди, звідки його читає дашборд. У нашому випадку це один файл ~/www/dash/data.json.

~/data/responses.csv — приватна тека E · EXTRACT відкрити файл прочитати рядки нічого не рахувати T · TRANSFORM почистити звести написання порахувати KPI L · LOAD записати data.json одним файлом у публічну теку dash/index.html читає готове 12 рядків тексту 10 записів → 5 чисел 1 файл, менше 1 КБ як записав сервер у пам'яті скрипта те, що бачить браузер
Числа внизу — з набору даних, на якому працює симулятор нижче в цьому уроці. У твоєму responses.csv вони будуть свої.

Навіщо розділяти, а не робити все в одному місці

Спокуса зробити все однією функцією велика: відкрив файл, тут-таки порахував, тут-таки намалював. Так теж працює — рівно до першої зміни.

СитуаціяУсе в одному місціРозділені E · T · L
Той самий графік потрібен на другій сторінці копіюєш увесь код рахунку друга сторінка читає той самий data.json
Джерело переїхало з CSV у базу переписуєш усе переписуєш тільки Extract
Порахували неправильно шукаєш помилку серед верстки дивишся data.json: число видно очима
Дані ще не зібрані сторінка не малюється взагалі сторінка малює вчорашній data.json

Головне: між кроками з'являється видимий проміжний результат. Помилку видно на тому кроці, де вона сталася, а не в кінці, коли графік «якийсь дивний».

Питання

Ти написав одну функцію: вона читає CSV, тут-таки рахує середнє і одразу малює графік у браузері. Через тиждень той самий графік треба поставити ще на сторінку класу. Що піде не так?

Крок 3

Чому дашборд не рахує сам

Найчастіше питання на цьому уроці: «а навіщо взагалі проміжний файл, хай браузер прочитає CSV і порахує». Є три відповіді, і кожна сама по собі достатня.

Перша: дашборд має відкриватися миттєво

Дашборд дивляться між справами: зайшов, побачив, пішов. Якщо сторінка спершу тягне весь CSV, а потім рахує медіану, людина бачить білий екран. Скрипт рахує один раз на сервері — сторінка тільки читає результат.

Браузер рахує сам качає CSV парсить, чистить, рахує медіану — і так щоразу малює довго Браузер читає готове data.json малює одразу довжина смуг ілюстративна: показує співвідношення роботи, а не секунди
Уся важка робота переїжджає з браузера кожного глядача на сервер, де вона робиться один раз для всіх.

Друга: знімок показників не має мінятися сам собою

Сирі дані живі: кожні кілька хвилин приходить нова відповідь. Уяви, що дашборд рахує на льоту. Ти дивишся на нього з телефона, сусід — з ноутбука. У вас на екранах різні числа, і поговорити про них ви вже не можете.

Тому скрипт робить знімок: рахує один раз і кладе результат у data.json. Усередині є мітка часу generated_at — видно, о котрій знімок зроблено. Сперечаються саме про нього.

Третя, найважливіша: приватність

responses.csv лежить у теці ~/data/. По HTTP ця тека не віддається: з інтернету її не відкрити ніяк.

Тримає цю межу число 750 — код прав на теку. Читають його по одній цифрі, зліва направо. Перша цифра — про тебе, власника теки. Друга — про твою групу. Третя — про всіх решту.

7 означає «власник може все»: заходити, читати, створювати файли. 5 — «група може зайти й прочитати, але не міняти». 0 — «решта не може нічого». Вебсервер працює саме від «решти». Тому він і не має шансів віддати цей файл комусь у браузер.

Якщо змусити браузер читати CSV, файл доведеться покласти в ~/www/. А це означає віддати сирі відповіді однокласників усьому інтернету. Агрегат у data.json віддавати можна: там числа, а не люди.

Правило, яке легко порушити випадково У data.json не має бути жодного поля, за яким можна впізнати конкретну людину: ні імені, ні session_id, ні тексту вільної відповіді. Тільки згорнуті числа й категорії.
Питання

Однокласник зробив дашборд, який читає responses.csv прямо в браузері й рахує медіану при кожному відкритті. Файл виріс до 40 тисяч рядків. Що тут найгірше?

Крок 4

Брудні дані: пропуски, дублікати, викиди

Дані, зібрані від людей, ніколи не бувають чистими. Чистка — не косметика, а місце, де ти приймаєш рішення, і кожне з них видно в підсумковому числі.

Пропущені значення

Хтось не відповів на питання про час. У CSV це порожня клітинка. Що з нею робити — не технічне питання, а змістовне: різні рішення дають різні числа і різні висловлювання про клас.

РішенняЩо ти цим стверджуєшКоли доречно
Викинути запис цілком «цієї людини для нас не існує» майже ніколи: ти губиш і всі інші її відповіді
Не рахувати в цій метриці, але зберегти запис «ця людина відповіла, але не на це питання» звичайний вибір, якщо пропусків небагато
Замінити нулем «вона витрачає 0 хвилин» тільки якщо порожньо справді означає нуль
Замінити середнім «вона така сама, як усі» у школі — не варто: середнє стає ще більш «середнім»

Правильний за замовчуванням варіант — другий. І обов'язково винести в data.json число rows_used: скільки записів реально пішло в розрахунок.

Питання

У 6 із 40 відповідей поле «хвилини на домашку» порожнє. Ти вирішив підставити туди нулі, щоб скрипт не падав. Що станеться із середнім?

Дублікати

Дублікат — це не «два однакові числа». Дублікат — це один і той самий факт, записаний двічі: людина двічі натиснула «Надіслати», або форму перезавантажили, або сервер повторив запит.

Як його виявити: шукаєш записи, у яких збігається ключ, що мав би бути унікальним. У твоїй формі це session_id.

А от якщо збігається лише набір відповідей, а session_id різні — це двоє різних людей, які просто відповіли однаково. Викидати їх не можна.

як побачити дублікати одним рядком
$ cut -d, -f1 ~/data/responses.csv | sort | uniq -d s08

Перша колонка — session_id. uniq -d показує тільки ті значення, що трапилися більше одного разу. Порожній вивід — дублікатів немає.

Питання

У CSV два однакові рядки поспіль: та сама секунда, той самий session_id, ті самі відповіді. Далі в файлі ще два однакові рядки, але з різними session_id. Що з цим робити?

Викиди

Викид — значення, яке різко випадає з решти. Найчастіше це не злодійство. Це жарт («999 годин»). Або помилка одиниць: «2» замість «120», бо людина писала години. Або просто промах по клавіші.

Одного викиду досить, щоб зламати середнє. Не «трохи змістити» — саме зламати: середнє почне показувати число, якого немає ні в кого.

З викидом 999 · дев'ять відповідей 0 500 1000 хв 999 середнє 144,9 медіана 40 Без викиду · вісім відповідей, та сама шкала середнє 38,1 медіана 37,5 — обидві тут, у купі Той самий шматок, збільшено: 0—70 хвилин медіана 37,5 середнє без викиду 38,1 20 60
Один жарт зсунув середнє з 38,1 до 144,9 — у 3,8 раза. Медіана за той самий час зсунулася з 37,5 до 40. Числа справжні: це набір даних симулятора нижче, можеш перерахувати вручну.

Чому медіана стійкіша

Вишикуй клас за зростом. Той, хто стоїть точно посередині шеренги, — це медіана. Прийде в клас баскетболіст на два метри — він просто стане скраю, а посередині лишиться той самий учень.

Із середнім інакше. Його рахують так: складають усі зрости й ділять на кількість людей. Ті два метри піднімуть число одразу для всіх.

З хвилинами на домашку те саме. Кожне значення тягне середнє на себе, а величезне тягне сильно. Медіані байдуже, наскільки велике крайнє число, — важливо лише, що воно крайнє.

Тому правило: показуй обидва. Якщо середнє й медіана близькі — розподіл рівний. Якщо різко різні — щось перекошує картину, і це саме по собі новина.

Що робити з викидом технічно Не видаляй запис — він містить й інші коректні відповіді. Виключи саме значення з розрахунку часу за розумною межею (minutes <= 180). Напиши в README.md, яку межу ти взяв і чому. Межа, взята зі стелі й нікому не сказана, — це вже підгонка.
Питання

Середній час на домашку у класі — 47 хвилин, а медіана 25. Що це означає і хто зіпсував середнє?

Різні написання одного й того самого

Цю пастку ти вже бачив в уроці 3. Grist, grist, GRIST і  Grist з пробілом — це чотири різні значення для комп'ютера. Для людини — одне.

Лікується двома викликами. .strip() прибирає пробіли з країв, .lower() зводить регістр.

До: 12 рядків з файлу session · хв · інструмент s01 · 45 · Grist s02 · 30 · grist s03 ·    · Google Таблиці  ← порожньо s04 · 60 · Grist s05 · 999 · NocoDB  ← викид s06 · 25 · Excel s07 · 40 · GRIST s08 · 35 · Google Таблиці s08 · 35 · Google Таблиці  ← дубль s09 · 50 · Excel s10 · 20 · Grist s11 · 55 · NocoDB, дуже зручний ← 6 колонок і окремо, у всіх рядках одразу: сім написань чотирьох інструментів Після: 10 записів session · хв · інструмент s01 · 45 · grist s02 · 30 · grist s03 · null · google таблиці s04 · 60 · grist s05 · 999* · nocodb s06 · 25 · excel s07 · 40 · grist s08 · 35 · google таблиці s09 · 50 · excel s10 · 20 · grist дубль s08 і рядок s11 відкинуті null не йде в середнє * 999 не йде в розрахунок часу, але відповідь зберігається інструментів було 7 написань → стало 4
Той самий набір до і після кроку Transform. Праворуч уже можна рахувати: усі значення одного виду, дублікатів немає, а рішення щодо порожнього поля й викиду прийняте явно.
Крок 5

Симулятор конвеєра

Ось 12 рядків із сирими проблемами. Тисни кнопки по порядку і дивись, що саме змінюється на кожному кроці. Підсвічені рядки — ті, до яких скрипт щось зробив.

Конвеєр наживо

    Мітка generated_at у прикладі фіксована, щоб файл не мінявся від перезавантаження сторінки. Твій скрипт має ставити реальний час запуску.

    Питання

    Після чистки лишилося 10 записів із 12 прочитаних. Що обов'язково має потрапити в data.json разом із метриками?

    Крок 6

    Що таке KPI і чим він відрізняється від просто числа

    KPI — key performance indicator, ключовий показник. Ключове слово тут не «показник», а «ключовий»: він має відповідати на питання, яке хтось справді ставить.

    Уяви стрілку бензину в машині. На неї дивляться не заради цифри, а щоб вирішити: заїжджати на заправку чи їхати далі. Це й є ключовий показник — число, від якого залежить чиясь дія.

    Перевірка проста. Назви показник уголос і спитай себе: яке рішення зміниться, якщо це число буде іншим? Якщо жодне — це не KPI, це просто цифра на екрані.

    Хто дивитьсяЙого питанняKPI, що відповідає
    УчительЧи встигає клас із домашкою? медіана хвилин на домашку
    СтаростаЧи всі здали відповіді? частка тих, хто відповів, від списку класу
    ТиЧи росте кількість відповідей день у день? відповіді за днями (часовий ряд)
    УченьЧи я один такий повільний? моє значення поруч із медіаною

    «Кількість» проти «частки»

    Це найчастіша помилка шкільних дашбордів. Кількість — скільки штук. Частка — скільки штук зі скількох можливих. Кількість без знаменника нічого не означає.

    «Відповіли 18 людей» — це багато чи мало? Якщо в класі 28 — це 64%, нормально. Якщо опитування було на паралель зі 120 учнів — це 15%, і за такими даними висновків про паралель робити не можна.

    Показник без бази 18 відповідей «Це багато?» — відповіді немає Показник із базою 64% 18 з 28 учнів класу «Ще десятьох треба дособрати»
    Праворуч видно і скільки зібрано, і скільки лишилося. Ліворуч — тільки число. Числа на схемі ілюстративні.
    Питання

    На дашборді написано великими цифрами: «Відповідей: 34». Учителька питає: «це багато?». Чому показник поганий?

    Метрика і вимір

    Метрика — те, що ми міряємо числом: кількість, середнє, медіана, частка. Вимір — розріз, у якому дивимось на метрику: за днями, за класом, за інструментом.

    Один KPI = одна метрика + нуль або один вимір. «Медіана хвилин» — метрика без виміру, одне число. «Медіана хвилин за днями» — та сама метрика в розрізі днів, уже часовий ряд. Плутанина тут — головна причина, чому дашборд виходить нечитабельним: люди беруть три виміри одразу.

    Показник без порівняння марний У KPI має бути з чим порівняти: із базою (частка), з минулим тижнем (динаміка) або з іншою групою (9A проти 9B). Число саме по собі не каже, добре це чи погано.
    Питання

    Ти хочеш показати, «скільки часу дев'ятикласники витрачають на домашку по днях тижня». Що тут метрика, а що вимір?

    Крок 7

    Конструктор KPI: від питання до коду

    Обери метрику, вимір і умову — і подивись, який фрагмент build_dash.py це дає і яке число виходить на даних симулятора.

    Конструктор
    Метрика — що рахуємо
    Вимір — у якому розрізі
    Умова — кого беремо

    
        

    Фрагмент розрахований на змінну clean — список уже почищених записів. Викид (понад 180 хв) із розрахунку часу виключено.

    Не бери три виміри одразу «Медіана хвилин за днями, за класами й за інструментами» — це не один KPI, а куб, у якому нічого не видно. Розбий на три показники або обери один розріз.
    Крок 8

    Python по-шкільному: п'ять інструментів

    Усе, що треба сьогодні, є в стандартній поставці Python 3, яка вже стоїть на сервері.

    Для роботи з таблицями дорослі аналітики зазвичай докачують окрему бібліотеку — вона зветься pandas і вміє багато чого одним рядком. Сьогодні вона нам не потрібна: п'яти вбудованих інструментів вистачить на весь конвеєр. Заразом ти побачиш, що всередині таких бібліотек немає жодної магії.

    1. Прочитати CSV: csv.DictReader

    Він читає перший рядок як заголовок і повертає кожен наступний рядок як словник: {'ts': '...', 'minutes': '45', ...}. Звертатися можна за назвою колонки, а не за номером — код лишається читабельним.

    import csv
    
    with open(SRC, newline='', encoding='utf-8-sig') as f:
        rows = list(csv.DictReader(f))
    
    print(rows[0]['minutes'])   # '45' — рядок, не число!
    Три аргументи, кожен рятує від конкретної біди newline='' — щоб модуль csv сам розбирався з перенесеннями рядків і не ламався на файлі, збереженому у Windows. encoding='utf-8-sig' — щоб з'їсти BOM, який Excel любить дописувати на початок файлу. Без цього перша колонка називатиметься session_id і row['session_id'] впаде з KeyError. DictReader не падає на рядку з зайвою комою — він тихо складає зайве під ключ None. Тож такий рядок треба ловити самому.

    2. Словники як записи

    Кожен рядок — словник. Додавати в нього свої поля можна прямо на місці: r['date'] = r['ts'][:10]. Перші 10 символів ISO-дати — це рівно 2026-02-02, тобто день без часу.

    clean, seen = [], set()
    for r in rows:
        if None in r or None in r.values():
            continue                       # поламана кількість колонок
        if r['session_id'] in seen:
            continue                       # дублікат
        seen.add(r['session_id'])
        r['tool'] = r['tool'].strip().lower()
        r['date'] = r['ts'][:10]
        m = r['minutes'].strip()
        r['minutes'] = int(m) if m.isdigit() else None
        clean.append(r)

    m.isdigit() тут не прикраса: саме він рятує від ValueError на порожній клітинці. int('') завжди падає, а перевірити перед перетворенням — один рядок.

    3. Порахувати категорії: collections.Counter

    Counter — це словник, який сам рахує, скільки разів трапилося кожне значення. Метод .most_common() віддає пари «значення — скільки», відсортовані за спаданням.

    from collections import Counter
    
    by_tool = Counter(r['tool'] for r in clean)
    print(by_tool)                     # Counter({'grist': 5, 'google таблиці': 2, ...})
    print(by_tool.most_common(1))      # [('grist', 5)]

    4. Середнє й медіана: statistics

    Обидві функції беруть список чисел. Порожній список їм не можна — буде помилка, тому перед викликом завжди перевіряй, що список не порожній.

    from statistics import mean, median
    
    vals = [r['minutes'] for r in clean
            if r['minutes'] is not None and r['minutes'] <= 180]
    
    avg = round(mean(vals), 1) if vals else None
    med = median(vals) if vals else None

    median для парної кількості значень повертає півсуму двох середніх: для восьми відповідей це може бути 37.5, і це нормальне число, а не помилка.

    5. Записати JSON: json.dump

    import json
    
    with open(DST, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    ensure_ascii=False лишає українські слова літерами. Без нього "grist" не зміниться, а "таблиці" перетвориться на "\u0442\u0430\u0431\u043b\u0438\u0446\u0456". Такий запис браузер прочитає, а ти — ні.

    indent=2 розставляє відступи. Файл стає придатним для читання очима — а він саме для очей і потрібен: це ж контракт.

    Питання

    Скрипт падає з повідомленням ValueError: invalid literal for int() with base 10: ''. Що сталося і як лікувати?

    Повний скелет build_dash.py

    #!/usr/bin/env python3
    # ~/bin/build_dash.py — конвеєр ETL для дашборда
    import csv, json, os
    from collections import Counter
    from datetime import datetime, timezone
    from statistics import mean, median
    
    SRC = os.path.expanduser('~/data/responses.csv')
    DST = os.path.expanduser('~/www/dash/data.json')
    
    # ── EXTRACT ──────────────────────────────────────
    with open(SRC, newline='', encoding='utf-8-sig') as f:
        rows = list(csv.DictReader(f))
    
    # ── TRANSFORM ────────────────────────────────────
    clean, seen = [], set()
    for r in rows:
        if None in r or None in r.values():
            continue
        if r['session_id'] in seen:
            continue
        seen.add(r['session_id'])
        r['tool'] = r['tool'].strip().lower()
        r['date'] = r['ts'][:10]
        m = r['minutes'].strip()
        r['minutes'] = int(m) if m.isdigit() else None
        clean.append(r)
    
    vals = [r['minutes'] for r in clean
            if r['minutes'] is not None and r['minutes'] <= 180]
    by_day  = Counter(r['date'] for r in clean)
    by_tool = Counter(r['tool'] for r in clean)
    
    # ── LOAD ─────────────────────────────────────────
    data = {
        'generated_at': datetime.now(timezone.utc).astimezone().isoformat(timespec='seconds'),
        'source': {'file': os.path.basename(SRC),
                   'rows_read': len(rows), 'rows_used': len(clean)},
        'metrics': {
            'responses_total': len(clean),
            'avg_minutes':     round(mean(vals), 1) if vals else None,
            'median_minutes':  median(vals) if vals else None,
            'share_over_45':   round(sum(1 for v in vals if v > 45) / len(vals), 3) if vals else None,
            'top_tool':        by_tool.most_common(1)[0][0] if by_tool else None,
        },
        'series':  [{'date': d, 'responses': by_day[d]} for d in sorted(by_day)],
        'by_tool': dict(by_tool.most_common()),
    }
    
    os.makedirs(os.path.dirname(DST), exist_ok=True)
    with open(DST, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print('ok:', data['metrics']['responses_total'], 'записів →', DST)
    Це скелет, а не готова здача Тут чотири метрики й один вимір. Твоє завдання — поставити свої питання й свої KPI, а series довести до 7+ точок. Копія цього коду без жодної власної думки видно одразу: у README.md не буде чого написати.
    запуск на сервері
    $ python3 ~/bin/build_dash.py ok: 10 записів → /home/ivan/www/dash/data.json
    Крок 9

    data.json як контракт

    Контракт — це домовленість між скриптом і сторінкою: скрипт обіцяє класти дані саме в такі поля, сторінка обіцяє читати саме звідти. Хто перший порушить — той і зламав дашборд.

    data.json generated_at рядок ISO 8601 — коли зроблено знімок source об'єкт — звідки і скільки взято rows_read — прочитано рядків rows_used — пішло в розрахунок metrics об'єкт — чотири й більше KPI responses_total — ціле число avg_minutes · median_minutes share_over_45 — частка від 0 до 1 top_tool — рядок series масив — 7+ точок {date, responses} by_tool об'єкт — категорія: скільки
    Метрики — окремо, часовий ряд — окремо, розподіл за категоріями — окремо. Сторінка бере кожен блок під свій тип віджета: картку, лінію, стовпчики.

    Три правила контракту

    1. Назви полів не міняються без попередження. Перейменував avg_minutes на average — і картка на сторінці показує undefined.
    2. Тип поля сталий. Якщо median_minutes одного разу число, а іншого разу рядок "37,5" з комою — графік упаде. У JSON десятковий роздільник — крапка, завжди.
    3. Порожньо — це null, а не «0» і не «—». Нуль означає «порахували, вийшло нуль». null означає «порахувати не було з чого». Це різні висловлювання.
    Питання

    Ти відкрив data.json і бачиш "top_tool": "\u0433\u0440\u0456\u0441\u0442". Сторінка показує слово правильно, але читати файл очима неможливо. Що ти забув?

    Дати в series

    У часовому ряді дати мають бути в ISO 8601: 2026-02-02. Причина не в красі: такий запис сортується як текст точно так само, як хронологічно. Рік, місяць, день — від більшого до меншого, кожне поле фіксованої ширини.

    ЗаписСортування текстомЩо вийде
    2026-01-12, 2026-02-02 2026-01-122026-02-02 правильно
    12.01, 02.02 02.0212.01 лютий опинився перед січнем
    1/12/2026, 2/2/2026 як пощастить ще й незрозуміло, де день, а де місяць
    Питання

    У твоєму series дати записані як 02.02 і 04.02. Сортування дає правильний порядок, усе працює. Коли це зламається?

    Дірки в часовому ряді Якщо у вівторок ніхто не відповів, у Counter цього дня просто не буде. Графік з'єднає понеділок із середою прямою лінією, наче нічого не сталося. Правильно: пройти всі дати від першої до останньої й підставити 0 там, де записів немає. Це ж і найпростіший спосіб довести series до потрібних 7+ точок.
    Крок 10

    Практика: пишемо build_dash.py

    Кроки 1—3 і 15 — на твоєму комп'ютері, кроки 4—14 — по SSH на сервері, крок 16 — по FTP. Галочки зберігаються, сторінку можна закрити.

    Що піде по FTP build_dash.py і README.md — у ~/upload/, звідти скрипт кладеш у ~/bin/, а README.md в ~/www/dash/. data.json нормально робить сам скрипт на сервері; якщо ти налагоджував локально, залити його теж можна — це легкий текст. А ось responses.csv у ~/www/ не потрапляє ніколи.
    Крок 11

    Автоперевірка check 11

    Чекер запускає твій скрипт у пісочниці, підсовує йому зіпсовані дані й перераховує контрольну метрику сам. Вигадані числа не сходяться. Спроби не обмежені.

    Демонстраційний режим: результат згенеровано для показу. На сервері ця кнопка викликає POST /api/check, який запускає check 11 від імені учня.

    Що вчитель дивиться на твоєму екрані

    Крок 12

    Шкала виконання

    Це те, що бачить учитель, коли виставляє оцінку.

    СкладникВагаРезультат
    Виконано
    0%
    Рекомендований бал за 12-бальною

    Чотири речення, які варто запам'ятати

    Де найчастіше помиляються

    Домашнє завдання

    Додай п'ятий KPI, який не рахується «в лоб». Наприклад, частку відповідей за останні три дні від усіх. Або різницю медіан 9A і 9B.

    Опиши його в README.md парою «питання → метрика». І поясни одним реченням, яке рішення міняється від цього числа.

    Джерела

    Звідки взяті означення й приклади

    Усе — з документації Python, стандартів і відкритих довідників. Перевіряти дозволено й корисно.

    Повний список із поясненнями, що звідки взято, — у файлі urok-11-джерела.md поруч із цією сторінкою.