Проект: анализ уникальных посетителей на Python
Итоговый проект: журнал из 12 визитов вида «имя:день» превращается в отчёт — уникальные посетители, гости по дням, постоянные через пересечение, топ повторов. Три куска, каждый самодостаточен.
Редакция Питоники
Финальный проект собирает раздел в одну программу. Данные — журнал посещений: двенадцать записей вида «имя:день», как их выдал бы сервер или счётчик посещений. Заказчик просит четыре ответа: сколько всего визитов и разных людей, кто пришёл в каждый день, кто постоянный гость двух дней и кто ходит чаще всех. Всё это уже было по кусочкам: уникальность множества, пересечение и разность, дедупликация — из них собирается отчёт.
| Кусок проекта | Что отвечает | Какой приём |
|---|---|---|
| Кусок 1 | Уникальные посетители и гости каждого дня | split, словарь множеств, len |
| Кусок 2 | Постоянные и одиночки между днями | & , -, ^ на множествах |
| Кусок 3 | Частота визитов и топ повторов | Счётчик get, sorted с key |
Данные: журнал как список строк
В песочнице файлов нет, поэтому журнал — список строк, тот же объект, что вернул бы readlines. Первое правило любого разбора — показать данные как есть, с номерами записей:
visits = [
'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
'Анна:вт', 'Глеб:вт', 'Дана:вт',
'Борис:ср', 'Анна:ср',
'Вера:пт', 'Анна:пт', 'Глеб:пт',
]
for n, record in enumerate(visits, 1):
print(n, '-', record)
1 - Анна:пн 2 - Борис:пн 3 - Вера:пн 4 - Егор:пн 5 - Анна:вт 6 - Глеб:вт 7 - Дана:вт 8 - Борис:ср 9 - Анна:ср 10 - Вера:пт 11 - Анна:пт 12 - Глеб:пт
Двенадцать записей, четыре дня, шесть разных имён — все данные проекта видны с первого взгляда. Формат каждой записи один: имя, двоеточие, день. Значит, split(':') из курса про строки разберёт любую запись на пару:
name, day = 'Анна:пн'.split(':')
print('имя:', name, '- день:', day)
print('частей:', len('Анна:пн'.split(':')))
имя: Анна - день: пн частей: 2
Кусок 1: уникальные посетители и гости по дням
Разбор идёт одним циклом: каждая запись делится на имя и день, имя добавляется в множество своего дня. Словарь дней хранит set — повторные визиты одного человека за день поглощаются молча. Параллельно копится объединение всех гостей:
visits = [
'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
'Анна:вт', 'Глеб:вт', 'Дана:вт',
'Борис:ср', 'Анна:ср',
'Вера:пт', 'Анна:пт', 'Глеб:пт',
]
days = {}
all_guests = set()
for record in visits:
name, day = record.split(':')
if day not in days:
days[day] = set()
days[day].add(name)
all_guests.add(name)
print('записей в журнале:', len(visits))
print('уникальных посетителей:', len(all_guests))
for day in ['пн', 'вт', 'ср', 'пт']:
print(day, '- гостей:', len(days[day]), '-', sorted(days[day]))
записей в журнале: 12 уникальных посетителей: 6 пн - гостей: 4 - ['Анна', 'Борис', 'Вера', 'Егор'] вт - гостей: 3 - ['Анна', 'Глеб', 'Дана'] ср - гостей: 2 - ['Анна', 'Борис'] пт - гостей: 3 - ['Анна', 'Вера', 'Глеб']
Первая половина заказа выполнена: двенадцать визитов, шесть разных людей, и по каждому дню виден состав гостей. Проверьте сумму дней: четыре плюс три плюс два плюс три — тоже двенадцать, ни одна запись не потерялась. Печать гостей идёт через sorted — правило проекта: множества печатаются только отсортированными, иначе вывод меняется от запуска к запуску.
visits = ['Анна:пн', 'Борис:пн', 'Анна:вт', 'Вера:пт']
days = {}
for record in visits:
name, day = record.split(':')
if day not in days:
days[day] = set()
days[day].add(name)
total = 0
for day in days:
total += len(days[day])
print('по дням:', total)
print('записей:', len(visits))
print('сходится:', total == len(visits))
по дням: 4 записей: 4 сходится: True
Сверка полноты стоит после разбора и работает на любых данных: каждая запись попала ровно в одно множество дня, поэтому сумма размеров множеств обязана равняться числу записей. Если когда-нибудь строка окажется в двух днях или потеряется — проверка скажет сходится: False, и это правильное место для assert из курса про исключения.
Кусок 2: постоянные гости и одиночки
Второй вопрос заказчика: кто приходил в оба дня из пары, а кто только в один. Дни уже лежат множествами — остаются операции из уроков 7 и 8. Пересечение находит постоянных, симметричная разность — тех, кто был ровно в одном из двух дней:
visits = [
'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
'Анна:вт', 'Глеб:вт', 'Дана:вт',
'Борис:ср', 'Анна:ср',
'Вера:пт', 'Анна:пт', 'Глеб:пт',
]
days = {}
for record in visits:
name, day = record.split(':')
if day not in days:
days[day] = set()
days[day].add(name)
mon, tue = days['пн'], days['вт']
print('пришли в пн и вт:', sorted(mon & tue))
print('были в пн или вт, но не в оба:', sorted(mon ^ tue))
print('постоянные пн и ср:', sorted(days['пн'] & days['ср']))
пришли в пн и вт: ['Анна'] были в пн или вт, но не в оба: ['Борис', 'Вера', 'Глеб', 'Дана', 'Егор'] постоянные пн и ср: ['Анна', 'Борис']
Три вопроса — три операции. Вера и Егор пришли в понедельник, но не во вторник, поэтому попали в симметричную разность; Анна была в оба дня и выпала из неё. Никаких циклов по парам людей: множества сравниваются целиком, и код читается как формулировка вопроса. Если заказчик завтра спросит про вт и ср — поменяются две буквы.
Кусок 3: частота визитов и итоговый отчёт
Третий вопрос — кто ходит чаще. Множества здесь бессильны: они знают факт «был», но не число раз. Частоту считает словарь-счётчик — тот же приём, что в задачах на множества:
visits = [
'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
'Анна:вт', 'Глеб:вт', 'Дана:вт',
'Борис:ср', 'Анна:ср',
'Вера:пт', 'Анна:пт', 'Глеб:пт',
]
counts = {}
for record in visits:
name, day = record.split(':')
counts[name] = counts.get(name, 0) + 1
for name in sorted(counts):
print(name, '-', counts[name])
Анна - 4 Борис - 2 Вера - 2 Глеб - 2 Дана - 1 Егор - 1
Шесть гостей и их счётчики: Анна ходит чаще всех, трое имеют по два визита, Дана и Егор пришли по разу. Топ собирает sorted с составным ключом — сначала по убыванию числа визитов, при равенстве — по алфавиту имени — и печатает первые три строки. Собираем всё в одну функцию analyze и печатаем итоговый отчёт:
visits = [
'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
'Анна:вт', 'Глеб:вт', 'Дана:вт',
'Борис:ср', 'Анна:ср',
'Вера:пт', 'Анна:пт', 'Глеб:пт',
]
def analyze(visits):
days = {}
counts = {}
for record in visits:
name, day = record.split(':')
if day not in days:
days[day] = set()
days[day].add(name)
counts[name] = counts.get(name, 0) + 1
return days, counts
days, counts = analyze(visits)
print('=== отчёт по журналу ===')
print('записей:', len(visits))
print('уникальных посетителей:', len(counts))
print()
for day in ['пн', 'вт', 'ср', 'пт']:
print(day, '- гостей:', len(days[day]), '-', sorted(days[day]))
print()
print('постоянные пн и вт:', sorted(days['пн'] & days['вт']))
print()
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
print('топ повторов (визитов):')
for i, (name, n) in enumerate(top[:3], 1):
print(' ', i, '.', name, '-', n)
=== отчёт по журналу === записей: 12 уникальных посетителей: 6 пн - гостей: 4 - ['Анна', 'Борис', 'Вера', 'Егор'] вт - гостей: 3 - ['Анна', 'Глеб', 'Дана'] ср - гостей: 2 - ['Анна', 'Борис'] пт - гостей: 3 - ['Анна', 'Вера', 'Глеб'] постоянные пн и вт: ['Анна'] топ повторов (визитов): 1 . Анна - 4 2 . Борис - 2 3 . Вера - 2
Отчёт отвечает на все четыре вопроса заказа разом: записи и люди, состав дней, постоянные гости, топ ходоков. Ключ сортировки (-kv[1], kv[0]) стоит разобрать: минус переворачивает число визитов — самые частые первыми, а имя вторым элементом решает ничьи в алфавитном порядке: у Бориса, Веры и Глеба поровну, и они встали по букве. Множество отвечает, кто приходил вообще; счётчик — кто приходил чаще: вместе они покрывают весь отчёт по журналу.
От списка в коде к настоящему журналу
Единственное отличие проекта от настоящей программы — две строки, где журнал превращается в список строк:
visits = []
with open('visits.txt', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
visits.append(line)
strip и проверка на пустоту — тот же шаблон, что в проекте про исключения: файл даёт мусор по краям, разбор его отрезает до split. Дальше всё без изменений: analyze получает список строк и не знает, откуда они приехали.
Что проект закрепил
- запись имя:день разбирается split-ом в пару, и распаковка даёт две переменные без индексов;
- словарь дней хранит set посетителей: повторные визиты за день поглощаются автоматически;
- уникальные посетители — объединение всех множеств или отдельный set в цикле разбора;
- постоянные гости — пересечение & дней, одиночки — симметричная разность ^, без единого цикла по парам;
- частоту хранит словарь-счётчик get(name, 0) + 1: множество знает факт, но не число раз;
- топ собирает sorted с ключом (-число, имя): убывание частоты и алфавит при ничьих;
- множества печатаются только через sorted и len — вывод не зависит от запуска.
Раздел «Множества и кортежи» окончен: от первого set до отчёта, который из двенадцати строк журнала выжимает состав дней, постоянных гостей и топ повторов. Дальше эти структуры встретятся сразу в нескольких курсах: исключения страхуют разбор грязных журналов try-ом, а словари — родной дом для счётчиков и пар ключ-значение.
Сначала предскажи ответ в голове — это главный навык программиста.
visits = ['Анна:пн', 'Анна:пн', 'Борис:вт']
names = set()
for r in visits:
names.add(r.split(':')[0])
print(len(visits), len(names))
days = {}
for r in ['Анна:пн', 'Борис:пн', 'Анна:пн']:
name, day = r.split(':')
if day not in days:
days[day] = set()
days[day].add(name)
print(len(days['пн']))
print(sorted(days['пн']))
counts = {}
for name in ['А', 'Б', 'А', 'А', 'Б']:
counts[name] = counts.get(name, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
print(top[0])
1. Как в проекте узнать число уникальных посетителей журнала?
2. Зачем в разборе проверка if day not in days?
3. Как найти посетителей, пришедших и в пн, и в вт?
4. Почему частоту визитов считает словарь-счётчик, а не множество дней?
5. Что делает ключ (-kv[1], kv[0]) в sorted(counts.items(), key=...)?
6. Строка журнала пришла без двоеточия. Что случится в разборе?
7. Почему analyze возвращает данные, а не печатает отчёт сам?
Дан журнал visits из проекта. Посчитайте, сколько раз приходил Борис, и найдите людей, которые были и в вт, и в ср. Напечатайте строку «Борис: N» и отсортированный список пересечения.
Как посчитать уникальных посетителей в журнале логов?
Разобрать каждую запись split-ом на имя и день и складывать имена в множество: len(множества) даст число разных посетителей, len(журнала) — число визитов. Повторы множество поглощает автоматически — циклы со сравнением имён не нужны.
Как найти людей, которые приходили в оба дня?
Собрать посетителей каждого дня в отдельные множества и взять пересечение: days['пн'] & days['вт']. Симметричная разность ^ наоборот найдёт тех, кто был ровно в один из двух дней, а объединение | — всех гостей пары дней.
Урок 7: пересечение множествКак построить топ самых частых посетителей?
Считать визиты словарём: counts[name] = counts.get(name, 0) + 1, затем отсортировать пары по составному ключу: sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])). Минус ставит частые записи первыми, имя при равенстве решает ничью по алфавиту; срез [:3] берёт топ-3.
Почему повторный визит одного человека за день не увеличивает число гостей?
Потому что день хранится множеством: второй add того же имени ничего не меняет. Это осознанная модель: множество отвечает на вопрос «кто был», а частоту визитов хранит отдельный словарь-счётчик. Гостей за день считает len множества, визитов — сумма счётчиков.
Что делать с битыми записями журнала без двоеточия?
Страховать разбор try-ом: name, day = record.split(':') внутри блока try, except ValueError отправляет битую строку в журнал ошибок с номером записи, а отчёт продолжает разбирать остальные. Каркас тот же, что в проекте курса про исключения.
Проект: разбор грязного файла с tryПонравился урок? Сошлитесь на него
«Множество отвечает, кто приходил вообще; счётчик — кто приходил чаще: вместе они покрывают весь отчёт по журналу.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Множества · Урок 18
Задачи на множества Python: практика с решениями
Четыре задачи на множества из жизни данных: уникальные посетители журнала, общие друзья, первый неповторяющийся элемент и гости, пришедшие оба дня. Код, вывод и разбор каждого шага.
Множества · Урок 19
Задачи на кортежи Python: практика с решениями
Пять задач на кортежи из практики: точки и функции, максимум с минимумом одним вызовом, защита записей от правок, ходы как ключи словаря и сортировка по полю. Код, вывод, разбор.
Множества · Урок 16
Как удалить дубликаты из списка в Python одной строкой
Убрать повторы из списка — две строки: set(x) выкидывает дубликаты, dict.fromkeys(x) сохраняет порядок первого вхождения. Разбираем оба приёма и случай, когда множеству нужен ваш ключ.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Строки · Урок 20
Проект: анализатор текстового отчёта
Финальный проект курса: настоящий текстовый отчёт магазина, полный ловушек из уроков 19 — пробелы, регистр, пустые строки. Собираем анализатор, который чистит, разбирает и выводит итоги.
проект строки pythonpython разбор текста
Словари · Урок 20
Проект: частотный словарь текста
Финальный проект: частотный словарь текста — режем текст на слова, чистим, отбрасываем стоп-слова, считаем через Counter и выводим топ-5 ровными колонками.
частотный словарь pythonанализ текста python для начинающих
Множества · Урок 7
Пересечение множеств Python: оператор & и intersection
Общие элементы двух множеств одним знаком: & находит тех, кто есть в обоих. Пустое пересечение, isdisjoint и пересечение трёх наборов.
python пересечение множествмножества python операции