Проект: анализ оценок класса на циклах Python
Итоговый проект раздела: из журнала оценок — средний балл, лучший ученик, отстающие и гистограмма. Три куска отчёта, каждый запускается отдельно.
Редакция Питоники
Финальный проект раздела собирает в одной программе всё, чему он научил: перебор, аккумуляторы, условия, any и all, умножение строки на число. Задача — отчёт об оценках класса: из журнала получить средний балл, лучшего ученика, список отстающих и гистограмму. Отчёт печатается тремя кусками, и каждый кусок — самостоятельная программа со своими данными: её можно запустить отдельно и проверить свой кусок отчёта. Маленькие проверяемые шаги вместо одного огромного скрипта — так собирается любой отчёт по данным.
| Кусок отчёта | Что считает | Какой приём |
|---|---|---|
| Кусок 1 | Средний балл, лучший результат, отличники | Аккумулятор, максимум, фильтр-список |
| Кусок 2 | Кто ниже порога, есть ли проблемные | Фильтр с печатью, any и all |
| Кусок 3 | Распределение оценок и гистограмма | Счётчик-словарь, умножение строки |
Данные: журнал класса
Данные проекта — словарь: имя ученика — его оценка. Восемь учеников, оценки от двойки до пятёрки. Словарь выбран не случайно: обход по items() отдаёт пары «имя — оценка» в порядке вставки, а по values() — одни оценки, когда имена не нужны. Прежде чем считать, посмотрим на данные глазами:
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
for name, mark in grades.items():
print(f'{name:<7}{mark}')
Анна 5 Борис 4 Вера 3 Глеб 5 Даша 4 Егор 2 Женя 4 Зина 3
Первый шаг любого анализа — показать данные как есть: пары столбиком, ровно, через f-строку с шириной. Так проект начинается всегда: сначала глаза на данные, потом вычисления. Теперь три куска отчёта.
Кусок 1: состав, средний балл, лучший
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
total = 0
for mark in grades.values():
total += mark
print('Учеников:', len(grades))
print('Средний балл:', total / len(grades))
best_name = ''
best_mark = 0
for name, mark in grades.items():
if mark > best_mark:
best_mark = mark
best_name = name
print('Лучший результат:', best_name, '-', best_mark)
excellent = []
for name, mark in grades.items():
if mark == 5:
excellent.append(name)
print('Отличники:', excellent)
Учеников: 8 Средний балл: 3.75 Лучший результат: Анна - 5 Отличники: ['Анна', 'Глеб']
В одном куске живут три знакомых приёма. Аккумулятор total собирает сумму по values(), деление на len даёт среднее: тридцать на восемь — ровно три и три четверти, Python печатает 3.75 без хвостов. Каркас максимума — пара best_name, best_mark со строгим сравнением: Глеб тоже отличник, но Анна стоит в журнале раньше, и побеждает первый встреченный. Фильтр-список собирает всех пятёрочников сразу: максимум отвечает «кто лучший», фильтр — «кто соответствует условию».
average = sum(grades.values()) / len(grades)
Кусок 2: кто ниже порога
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
print('Ниже порога:')
for name, mark in grades.items():
if mark < 4:
print(' ', name, '-', mark)
risky = any(m < 4 for m in grades.values())
print('Есть проблемные:', risky)
print('Все выше двойки:', all(m >= 3 for m in grades.values()))
Ниже порога: Вера - 3 Егор - 2 Зина - 3 Есть проблемные: True Все выше двойки: False
Проверка mark < 4 отбирает тройки и двойки вместе с именами — учительский список «кого тянуть». После него — строка-вердикт через any из урока про any и all: правда ли, что проблемные есть. Здесь any кажется избыточным — список уже напечатан, но приём масштабируется: на журнале из тысячи записей any ответит, не пробегая его целиком, короткое замыкание остановит проверку на первом же нарушителе. А all даёт обратный взгляд: все ли выше двойки — нет, Егор сломал всеобщность.
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
worst = 5
for mark in grades.values():
if mark < worst:
worst = mark
print('Худшая оценка:', worst)
Худшая оценка: 2
Каркас максимума работает и в обратную сторону: старт с верхней границы и знак меньше — и отчёт знает самую слабую оценку класса. Тот же перебор, зеркальное сравнение, никаких новых приёмов.
Кусок 3: распределение и гистограмма
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
counts = {}
for mark in grades.values():
counts[mark] = counts.get(mark, 0) + 1
print(counts)
best_mark = 0
best_count = 0
for mark, count in counts.items():
if count > best_count:
best_count = count
best_mark = mark
print('Мода журнала: оценка', best_mark)
print()
for mark in [5, 4, 3, 2]:
print(mark, ':', '#' * counts.get(mark, 0))
{5: 2, 4: 3, 3: 2, 2: 1}
Мода журнала: оценка 4
5 : ##
4 : ###
3 : ##
2 : #Кусок в три хода. Первый — счётчик-словарь из курса словарей: get с умолчанием ноль достаёт текущий счёт, к нему прибавляется единица; ключи в печатном словаре стоят в порядке первого появления. Второй — знакомый поиск максимума, но уже по счётам: мода журнала — четвёрка, она встретилась трижды. Третий — гистограмма: умножение строки на число превращает счёт в столько решёток, сколько учеников получили оценку. Обход по готовому списку [5, 4, 3, 2] фиксирует порядок строк от пятёрки к двойке, как в школьном журнале, а get с нулём страхует оценку, которой в классе нет: столбик будет пустым, но строка останется и отчёт не упадёт.
print(f'{mark}: {counts.get(mark, 0):>2} ' + '#' * counts.get(mark, 0))
Бонус: вердикт каждому
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
for name, mark in grades.items():
if mark == 5:
verdict = 'отлично'
elif mark == 4:
verdict = 'хорошо'
elif mark == 3:
verdict = 'удовлетворительно'
else:
verdict = 'надо подтянуть'
print(name, '-', verdict)
Анна - отлично Борис - хорошо Вера - удовлетворительно Глеб - отлично Даша - хорошо Егор - надо подтянуть Женя - хорошо Зина - удовлетворительно
Цепочка if/elif переводит число в слово для каждого ученика — тот же перебор по items(), только вместо подсчёта внутри цикла живёт выбор ветки. Обратите внимание на отступы: print стоит на уровне тела внешнего цикла, поэтому выполняется для каждой пары, а не один раз после цикла.
Проценты: доля от общего
Отчёт о классе почти всегда смотрят в процентах: «25 процентов получили пятёрку» читается быстрее, чем «двое из восьми». Проценты считаются тем же аккумулятором, только с делением.
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
marks = list(grades.values())
for mark in [5, 4, 3, 2]:
count = sum(1 for m in marks if m == mark)
share = round(count * 100 / len(marks))
print(mark, '-', count, 'учеников,', share, '%')
5 - 2 учеников, 25 % 4 - 3 учеников, 38 % 3 - 2 учеников, 25 % 2 - 1 учеников, 12 %
38 процентов — это 3 из 8: 37,5 округлилось до 38. Функция round возвращает целое число, и в отчёте дробная часть процента обычно не нужна. Но если проценты складывают в сумму, округление даёт расхождение: 25 + 38 + 25 + 12 даёт ровно 100, а вот на других данных может выйти 101. Тогда проценты считают от общей суммы и проверяют сумму.
Вертикальная гистограмма
Горизонтальная гистограмма из третьего куска показывает столбики в строку. Вертикальная — привычнее: оценки идут сверху вниз, а ученики стоят в колонках. Делается она двумя вложенными циклами.
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
marks = list(grades.values())
for level in range(5, 1, -1):
row = ''
for m in marks:
row += '#' if m == level else ' '
print(f'{level} |{row.rstrip()}')
5 |# # 4 | # # # 3 | # # 2 | #
Внешний цикл идёт по оценкам сверху вниз, внутренний — по ученикам слева направо. Каждая пара «оценка, ученик» решается: если у ученика такая оценка — рисуем решётку, иначе пробел. Метод rstrip убирает хвостовые пробелы, иначе строки выравнивались бы по разной длине. Это ровно тот случай, где вложенные циклы из восьмого урока работают как надо.
Группа риска: тройки и двойки вместе
В школьной практике отстающих удобно собирать одним списком, а не двумя. Фильтр с условием «меньше или равно» делает это одной строкой.
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
at_risk = []
for name, mark in grades.items():
if mark <= 3:
at_risk.append(name)
print('группа риска:', at_risk)
print('всего:', len(at_risk), 'из', len(grades))
группа риска: ['Вера', 'Егор', 'Зина'] всего: 3 из 8
Трое из восьми — 37,5 процента класса. Список собирается в порядке обхода словаря, то есть в порядке вставки: Вера, Егор, Зина. Условие <= вместо < принципиально: при < тройки в список не попали бы и группа риска вышла бы из одного человека.
at_risk = [name for name, mark in grades.items() if mark <= 3]
Что стоит проверить перед сдачей отчёта
Отчёт, который никто не проверил, — это набор чисел без уверенности. Пять проверок закрывают почти все сюрпризы, и все они стоят одну строку.
- сложились ли части отчёта: средний балл, количество учеников и сумма оценок должны сходиться;
- не пуст ли журнал — на пустом словаре деление на len даст ZeroDivisionError;
- все ли оценки попали в счётчик: сумма счётчиков по всем оценкам равна числу учеников;
- одинаков ли порядок в горизонтальной и вертикальной гистограммах;
- не потеряны ли ученики с оценкой вне ожидаемого диапазона — например, с шестёркой или единицей.
Последний пункт стоит проверить первым. Если в словаре появится шестёрка, счётчик её учтёт, а гистограмма по списку [5, 4, 3, 2] — нет: столбика для шестёрки не будет, и ученик исчезнет из картинки. Цикл по ключам счётчика напечатал бы всё, но потерял бы порядок от пятёрки к двойке. Тот случай, когда приходится выбирать: полнота или порядок.
grades = {
'Анна': 5, 'Борис': 4, 'Вера': 3,
'Глеб': 5, 'Даша': 4, 'Егор': 2,
'Женя': 4, 'Зина': 3,
}
counts = {}
for mark in grades.values():
counts[mark] = counts.get(mark, 0) + 1
print('всего оценок:', sum(counts.values()))
print('всего учеников:', len(grades))
for mark in sorted(counts, reverse=True):
print(mark, '->', counts[mark])
всего оценок: 8 всего учеников: 8 5 -> 2 4 -> 3 3 -> 2 2 -> 1
Сумма счётчиков равна числу учеников — значит, ни одна оценка не потерялась. А цикл по отсортированным ключам печатает оценки от пяти к двум без жёсткого списка в коде: шестёрка, появись она в данных, напечаталась бы первой. Противоречие с таблицей выше разрешается просто: для красоты нужен фиксированный порядок, для полноты — сортировка ключей.
Раздел «Циклы» окончен: от первого for по списку до программы, которая читает журнал и отвечает на четыре вопроса о данных. Цикл — самый рабочий инструмент программиста: списки обрабатываются перебором, строки разбираются посимвольно, словари обходит items(). Логичный следующий шаг — курс «Словари»: журналы, счётчики и частотные таблицы станут основной работой, а не бонусом к циклам.
Сначала предскажи ответ в голове — это главный навык программиста.
marks = [4, 2, 5]
total = 0
for m in marks:
total += m
print(total // len(marks))
d = {}
for m in [3, 4, 3]:
d[m] = d.get(m, 0) + 1
print(d)
marks = [4, 4, 5]
best = 0
for m in marks:
if m > best:
best = m
print(best)
1. Каким каркасом в проекте считается средний балл?
2. Почему в поиске лучшего ученика сравнение строгое — mark > best_mark?
3. Что вернёт any(m < 4 for m in grades.values()), если в классе есть двойка?
4. Как в проекте печатается гистограмма?
5. Что вернёт counts.get(1, 0), если единиц в журнале не было?
6. Как в проекте собрать список учеников с оценкой не выше тройки?
7. Зачем в вертикальной гистограмме используют rstrip()?
8. Что проверит сумма счётчиков по всем оценкам?
Дан журнал grades = {'Аня': 5, 'Борис': 3, 'Вера': 4, 'Глеб': 2}. Выведи количество учеников, средний балл и список имён тех, у кого оценка выше тройки.
Как посчитать средний балл списка оценок на Python?
Аккумулятором: total = 0, в цикле total += mark, после цикла total / len(marks). Для встроенного решения — sum(marks) / len(marks). На пустом списке будет ZeroDivisionError, поэтому данные проверяют перед делением.
Урок 11: счётчики и аккумуляторыКак найти ученика с максимальной оценкой?
Каркасом максимума: пара переменных имя-оценка и строгое сравнение в цикле по items(). При равных оценках первым останется тот, кто встретился раньше — порядок словаря стабилен, результат воспроизводим.
Как построить гистограмму символами в консоли?
Умножением строки на число: '#' * count печатает столько решёток, сколько насчитал счётчик. Счёт собирается словарём counts.get(mark, 0) + 1 — или Counter из курса словарей, если ручной счёт не нужен.
Урок 11 курса словарей: CounterЧто учить после курса по циклам?
Курс «Словари»: журналы, счётчики и частотные таблицы — следующая ступень работы с данными, и весь этот курс опирается на циклы. Как альтернатива — функции: куски отчёта проекта можно свернуть в переиспользуемые блоки.
Курс Словари: урок 1Как посчитать процент учеников с какой-либо оценкой?
Разделить количество на общее число учеников: count * 100 / len(grades). Важно, чтобы делитель был один на весь отчёт: процент от числа учеников и процент от суммы баллов — разные базы, и смешивать их нельзя.
Как нарисовать вертикальную гистограмму оценок?
Два вложенных цикла: внешний идёт по оценкам от пяти к двойке, внутренний — по ученикам. Внутри сравниваем оценку ученика с текущей и ставим решётку или пробел. rstrip убирает хвостовые пробелы, чтобы строки были одинаковой длины.
Урок 8: вложенные циклыКак понять, что в отчёте никто не потерялся?
Сложить счётчики по всем оценкам: сумма должна равняться числу учеников. Если меньше — какая-то запись не попала в счётчик или попала дважды. Простая сверка, которая ловит ошибку до того, как её увидит учитель.
Понравился урок? Сошлитесь на него
«Маленькие проверяемые шаги вместо одного огромного скрипта — так собирается любой отчёт по данным.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Циклы · Урок 11
Счётчики и аккумуляторы в циклах Python
Сумма чека, число совпадений, лучший результат: переменная, объявленная до цикла и копящая значения внутри, — приём старше языков. Разбираем счётчики, суммы, максимумы и их ловушки.
Циклы · Урок 15
any и all в Python: проверка без ручного цикла
any и all отвечают на вопросы «есть ли хоть один» и «все ли до единого» одной строкой — вместо цикла с булевым флагом.
Словари · Урок 11
Counter: словарь, который считает сам
Counter из collections — словарь, который считает сам: слова строки, буквы слова, повторения в списке. most_common отдаёт самых частых, арифметика счётчиков сравнивает две выборки.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
NumPy · Урок 10
Проект на NumPy: анализируем температуру за год и строим отчёт
Финальный проект раздела: генерируем год температуры с фиксированным seed, считаем статистику по месяцам через reshape(12, 30), ловим аномалии масками, сглаживаем ряд и печатаем отчёт.
проект numpyанализ данных numpy
Pandas · Урок 10
Проект на Pandas: полный анализ продаж интернет-магазина
Финальный проект раздела: сырая выгрузка заказов, чистка по чек-листу, выручка и средний чек, топ-товары, сводные по городам, тренд по месяцам и выводы для руководителя.
проект pandasанализ данных python pandas
Циклы · Урок 18
Числа в цикле Python: суммы, факториалы, Фибоначчи
Сумма, факториал и Фибоначчи — три классические задачи на один каркас: аккумулятор со стартовым значением и цикл, который шаг за шагом вносит вклад.
задачи на циклы pythonpython сумма чисел циклом