Глава 13. Итоговый проект: отчёт по успеваемости
Задача главы: собрать сквозной скрипт «выгрузка → расчёты → готовый отчёт», в котором работают инструменты всех предыдущих глав вместе, а не по одному в изолированном примере — и получить свой первый завершённый инструмент аналитика.
13.1 Постановка задачи и формат входной выгрузки
Деканат выгружает из системы один файл journal_dirty.csv со всеми предметами и всеми потоками сразу — одна строка выгрузки описывает одну сдачу одного предмета одним учеником:
| student_name | subject | score | submitted_at |
|---|---|---|---|
Иванов Иван | Python | 78 | 16.05.2024 |
| Ким Юрий | Python | н/д | 20.05.2024 |
Петров Пётр | Python | 150 | 19.05.2024 |
| Сидорова Анна | Аналитика | 90 | 15.05.2024 |
Выгрузка почти никогда не приходит идеальной: в именах бывают случайные пробелы, в баллах — текстовые заглушки вместо чисел и опечатки, из-за которых балл выходит за пределы 0–100. Задача — не «написать код для чистых данных», а собрать отчёт, который выдержит именно такую выгрузку целиком, ни разу не упав и не потеряв ни одной строки без следа.
На выходе нужны три вещи:
- Файл
report.csv— по одной строке на ученика: средний балл, статус, просрочка. - Консольная сводка — рейтинг, группа риска, разбивка по предметам, медиана, разброс, распределение статусов.
- Журнал отбракованных строк — что не попало в отчёт и почему.
Это ровно то же самое разделение, что и в главе Файлы и CSV («чтение → расчёты → запись»), только теперь на входе — реалистично грязная выгрузка, а расчётов — на порядок больше.
13.2 Слой чтения и нормализации: главы 3, 10, 11
Зачем это нужно. Прежде чем что-либо считать, выгрузку нужно превратить из «текста в файле» в чистые, однотипные значения — и сделать это так, чтобы одна кривая строка не обрушила чтение всех остальных. Это уже знакомая связка трёх глав: csv.DictReader открывает файл (Файлы и CSV), .strip() убирает случайные пробелы по краям ФИО и предмета (Строки и f-строки), а try / except перехватывает как нечисловой балл, так и балл вне диапазона, откладывая проблемную строку в отдельный список вместо падения (Ошибки и исключения).
Функция отдаёт два списка сразу: valid_rows — то, с чем можно дальше считать, и rejected_rows — то, что попало под нож, с понятной причиной для каждой строки. Ни одна строка выгрузки не пропадает без объяснения — тот же принцип, что закрывал мини-кейс главы 11.
13.3 Слой расчётов: функции из глав 4, 5, 9, 12
Зачем это нужно. У valid_rows — плоская структура: одна строка на пару «ученик–предмет». Чтобы получить средний балл и статус по ученику, а не по отдельной сдаче, данные сначала группируют (Словари), затем считают через собственные функции (Функции), уточняют медианой и разбросом (Стандартная библиотека) и, наконец, выстраивают в рейтинг (Списки).
get_average_score, переданная в sorted(..., key=get_average_score) без круглых скобок, — не новый трюк, а прямое применение ловушки из 9.1: имя функции без () — это ссылка на сам объект-функцию, а не её результат. Обычно эта ссылка мешает (переменной по ошибке достаётся функция вместо числа); здесь она работает как задумано — sorted() сам вызовет эту функцию на каждом элементе, чтобы понять, по чему сравнивать. Это тот самый приём с собственной функцией в key=, который был обещан ещё в главе Списки.
13.4 Слой вывода: f-строки, файл отчёта, сводка по группам
Зачем это нужно. Расчёты бесполезны, пока они не превратились в две конкретные вещи: файл, который можно переслать коллеге, и текст на экране, который читается за десять секунд. Запись — тот же csv.writer в режиме 'w' с newline='' из главы Файлы и CSV; вывод на экран — f-строки из главы Переменные и ввод данных и спецификаторы :.1f из главы Строки и f-строки.
Сводка по группам (предметам) и рейтинг по ученикам — это один и тот же приём «сгруппировать → посчитать → напечатать», примененный на двух разных уровнях одной и той же выгрузки: по строкам предмета и по строкам ученика.
13.5 Разбор итогов и куда двигаться дальше
Всё, что написано в этой главе, делает вручную то, что уже умеет специализированная библиотека pandas — но написанное вручную не потрачено впустую: без него pandas выглядел бы чёрным ящиком, а его ошибки — необъяснимой магией.
Две строки pandas заменяют десятки строк ручной группировки — но groupby() — это тот же setdefault() из главы 5 под капотом, а .mean() — тот же statistics.mean() из главы 12, просто с другим синтаксисом сверху. Ошибка KeyError в pandas — тот же KeyError, что уже разобран в главе Словари. Библиотеки для визуализации (matplotlib, seaborn) превращают subject_averages и ranking из этой главы в столбчатые диаграммы и графики — следующий логичный шаг после того, как текстовый отчёт уже научился строиться надёжно.
Финальный кейс: полный скрипт «выгрузка → расчёты → отчёт»
Задача: собрать все слои этой главы в один запускаемый скрипт — от намеренно грязной выгрузки до сохранённого report.csv и полной консольной сводки по потоку.
--- ИТОГОВЫЙ ОТЧЁТ ПО ПОТОКУ ---Отчёт сохранён в report.csv
Рейтинг (топ-3):1. Смирнова Ольга — 90.02. Сидорова Анна — 89.03. Иванов Иван — 85.0
Группа риска (статус «незачёт»):Ким Юрий — 55.0, просрочка 5 дн.Петров Пётр — 15.0, просрочка 4 дн.
Средний балл по предметам:Python: 86.0Аналитика: 68.0
Медиана среднего балла по потоку: 85.0Разброс (стандартное отклонение): 32.3Распределение статусов: {'зачёт': 3, 'незачёт': 2}Просрочили дедлайн: 3 из 5
Отбраковано строк при чтении: 2 Ким Юрий / Python: invalid literal for int() with base 10: 'н/д' Петров Пётр / Python: балл вне диапазона 0-100: 150Что получилось: из десяти строк грязной выгрузки в отчёт попали данные пяти учеников, обе испорченные строки отброшены не молча, а с точной причиной каждая. Рейтинг, группа риска, разбивка по предметам, медиана и распределение статусов — семь разных срезов одной и той же выгрузки, и ни один из них не потребовал нового синтаксиса: только csv, try/except, словари, свои функции и модули statistics/collections/datetime, изученные по одной главе за раз. report.csv на диске содержит только пять чистых строк ранжированного отчёта — то, за что можно ручаться перед деканатом.
Итоговый чек-лист аналитика (вместо чек-листа главы)
Эта глава последняя, поэтому чек-лист — не по одной теме, а по всем двенадцати:
- Различаю
+(склейка) и f-строку (f'{value}') при сборке текста — и не смешиваюstrс числом напрямую (гл. 1) - Помню, что
input()всегда возвращаетstr, и привожу кint()/float()до арифметики (гл. 1) - Использую
//и%осознанно, различаю «сколько аудиторий» и «сколько сядет в последнюю» (гл. 2) - Расставляю скобки в
and/orявно, не полагаясь на приоритет операторов по умолчанию (гл. 2) - Присваиваю результат строковых методов обратно — строки неизменяемы (гл. 3)
- Проверяю правую границу срезов
[start:stop]— она не включается ни у строк, ни у списков (гл. 3, 4) - Не присваиваю результат
.append(),.sort(),.insert()— они меняют объект на месте и возвращаютNone(гл. 4) - Копирую списки срезом
[:]илиlist(), когда нужна независимая копия, а не второе имя (гл. 4) - Читаю поля словаря через
.get(key, default), если ключ может отсутствовать (гл. 5) - Группирую данные через
setdefault(key, []).append(value)вместо ручной проверки «есть ли уже ключ» (гл. 5) - Нормализую регистр и пробелы перед сравнением множеств или строк из разных выгрузок (гл. 3, 6)
- Ставлю узкие условия (
elif) и узкиеexceptвыше широких — иначе широкая ветка перекрывает частные (гл. 7, 11) - Проверяю данные на пустоту (
if not scores:) до расчётов, а не после падения (гл. 7, 12) - Инициализирую счётчики и сумматоры один раз, до цикла, а не внутри тела (гл. 8)
- Отделяю логику расчёта в свою функцию (
def), а не копирую формулу по разным местам скрипта (гл. 9) - Возвращаю результат через
return, а не только печатаю его внутри функции, если результат нужен дальше (гл. 9) - Не использую изменяемый объект (
[],{}) как значение по умолчанию в аргументах функции (гл. 9) - Открываю файлы через
with open(..., encoding='utf-8')и указываюnewline=''при записи CSV (гл. 10) - Выбираю
'w'для нового отчёта и'a'для журнала, который копит историю запусков (гл. 10) - Оборачиваю в
try/exceptконкретный код, который может упасть на кривых данных, а не весь скрипт (гл. 11) - Считаю и логирую отбракованные строки вместо
except ... pass— итоговые цифры должны совпадать с числом строк на входе (гл. 11) - Беру медиану вместо среднего, когда в данных возможны выбросы (гл. 12)
- Фиксирую формат даты в одной константе и сверяю его с документацией источника, а не угадываю (гл. 12)
- Собираю выгрузку через слои — чтение и нормализация → расчёты → вывод — а не в одном нерасчленимом блоке кода (гл. 13)
Учебник на этом закончен — но собранные в нём функции, привычки читать traceback и разложенные по слоям скрипты теперь ваш личный набор инструментов, который переносится в любую новую выгрузку. Следующий шаг за пределами этой книги — pandas и визуализация, обозначенные в 13.5.
Возврат к началу: Оглавление