collections в Python — Counter, defaultdict и другие
Коротко
Модуль collections расширяет стандартные контейнеры Python специализированными структурами данных. Для аналитика самые полезные — Counter (подсчёт частот), defaultdict (группировка без проверки ключей) и namedtuple (именованные кортежи). На собеседованиях Counter встречается в задачах на частотный анализ, defaultdict — на группировку.
Counter — подсчёт частот
Counter считает, сколько раз каждый элемент встречается в коллекции:
from collections import Counter
# Из списка
events = ['login', 'purchase', 'login', 'logout', 'login', 'purchase']
counts = Counter(events)
print(counts)
# Counter({'login': 3, 'purchase': 2, 'logout': 1})
# Из строки
Counter('analytics')
# Counter({'a': 2, 'n': 1, 'l': 1, 'y': 1, 't': 1, 'i': 1, 'c': 1, 's': 1})
# Из словаря
Counter({'sql': 5, 'python': 3, 'excel': 1})Основные методы
c = Counter(['a', 'b', 'a', 'c', 'a', 'b'])
# Самые частые элементы
c.most_common(2) # [('a', 3), ('b', 2)]
c.most_common() # все, по убыванию частоты
# Общее количество
c.total() # 6 (Python 3.10+)
sum(c.values()) # 6 (любая версия)
# Доступ по ключу
c['a'] # 3
c['z'] # 0 (не KeyError!)
# Обновление
c.update(['a', 'd', 'd'])
print(c) # Counter({'a': 4, 'd': 2, 'b': 2, 'c': 1})Ключевое отличие от dict: доступ к несуществующему ключу возвращает 0, а не KeyError.
Арифметика Counter
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
c1 + c2 # Counter({'a': 4, 'b': 3})
c1 - c2 # Counter({'a': 2}) — только положительные
c1 & c2 # Counter({'a': 1, 'b': 1}) — минимум
c1 | c2 # Counter({'a': 3, 'b': 2}) — максимумАналитические примеры
from collections import Counter
# Топ-5 городов пользователей
cities = ['Москва', 'Питер', 'Москва', 'Казань', 'Москва', 'Питер', 'Новосибирск']
top_cities = Counter(cities).most_common(5)
# [('Москва', 3), ('Питер', 2), ('Казань', 1), ('Новосибирск', 1)]
# Частота слов в отзывах
reviews = ['отличный сервис', 'плохой сервис', 'отличный продукт']
words = Counter(word for review in reviews for word in review.split())
# Counter({'отличный': 2, 'сервис': 2, 'плохой': 1, 'продукт': 1})
# Распределение оценок
ratings = [5, 4, 5, 3, 5, 4, 2, 5, 4, 3]
dist = Counter(ratings)
for rating, count in sorted(dist.items()):
print(f'{rating}★: {"█" * count} ({count})')
# 2★: █ (1)
# 3★: ██ (2)
# 4★: ███ (3)
# 5★: ████ (4)defaultdict — словарь с default
defaultdict автоматически создаёт значение для несуществующего ключа:
from collections import defaultdict
# Без defaultdict — нужна проверка
groups = {}
for item in data:
key = item['category']
if key not in groups:
groups[key] = []
groups[key].append(item)
# С defaultdict — чище
groups = defaultdict(list)
for item in data:
groups[item['category']].append(item)Типы default
from collections import defaultdict
# list — группировка
d = defaultdict(list)
d['fruits'].append('apple')
d['fruits'].append('banana')
# {'fruits': ['apple', 'banana']}
# int — подсчёт (как Counter, но ручной)
d = defaultdict(int)
for word in words:
d[word] += 1
# set — уникальные значения
d = defaultdict(set)
d['user_1'].add('login')
d['user_1'].add('purchase')
d['user_1'].add('login') # дубликат игнорируется
# {'user_1': {'login', 'purchase'}}
# Вложенный defaultdict
d = defaultdict(lambda: defaultdict(int))
d['2025-03']['revenue'] += 5000
d['2025-03']['orders'] += 1Аналитический пример: группировка событий
from collections import defaultdict
events = [
{'user_id': 1, 'event': 'login', 'ts': '2025-03-15 09:00'},
{'user_id': 1, 'event': 'purchase', 'ts': '2025-03-15 09:30'},
{'user_id': 2, 'event': 'login', 'ts': '2025-03-15 10:00'},
{'user_id': 1, 'event': 'logout', 'ts': '2025-03-15 11:00'},
]
# Действия каждого пользователя
user_events = defaultdict(list)
for e in events:
user_events[e['user_id']].append(e['event'])
# {1: ['login', 'purchase', 'logout'], 2: ['login']}deque — двусторонняя очередь
from collections import deque
# Эффективное добавление/удаление с обоих концов
d = deque([1, 2, 3])
d.appendleft(0) # [0, 1, 2, 3]
d.append(4) # [0, 1, 2, 3, 4]
d.popleft() # 0, deque = [1, 2, 3, 4]
d.pop() # 4, deque = [1, 2, 3]
# Ограниченная длина — скользящее окно
last_5 = deque(maxlen=5)
for i in range(10):
last_5.append(i)
print(last_5) # deque([5, 6, 7, 8, 9])Полезно для скользящего окна (last N events) и очередей задач. appendleft и popleft — O(1), в списке insert(0, x) — O(n).
namedtuple — именованные кортежи
from collections import namedtuple
User = namedtuple('User', ['id', 'name', 'city'])
u = User(id=1, name='Иван', city='Москва')
print(u.name) # Иван
print(u[1]) # Иван (как обычный tuple)
print(u.city) # Москва
# Неизменяемый — нельзя u.name = 'Анна'
# Используется как лёгкая замена классаnamedtuple удобен для представления строк данных. Легче dict, но с именованным доступом. Для новых проектов рекомендуется dataclass (Python 3.7+).
OrderedDict
from collections import OrderedDict
# В Python 3.7+ обычный dict сохраняет порядок
# OrderedDict нужен для:
# - Сравнение с учётом порядка
# - move_to_end()
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a') # {'b': 2, 'c': 3, 'a': 1}
od.move_to_end('c', last=False) # {'c': 3, 'b': 2, 'a': 1}На практике в Python 3.7+ обычный dict почти заменяет OrderedDict.
Counter vs pandas value_counts
| Задача | Counter | pandas |
|---|---|---|
| Частоты | Counter(data) |
series.value_counts() |
| Топ-N | .most_common(n) |
.head(n) |
| Проценты | Ручной расчёт | normalize=True |
| Фильтрация | Ручная | .isin(), индексация |
| Скорость (чистый Python) | Быстрее | — |
| Удобство (DataFrame) | — | Удобнее |
Counter — для чистого Python и небольших данных. value_counts — для pandas DataFrame.
Вопросы с собеседований
-- Как подсчитать частоту элементов в списке?
-- Counter(list) — самый быстрый способ. most_common(n) — топ-N по частоте. Доступ к несуществующему ключу — 0 (не KeyError).
-- Чем defaultdict отличается от dict? -- defaultdict автоматически создаёт значение для несуществующего ключа (list, int, set). dict выбросит KeyError. defaultdict удобнее для группировки и подсчёта.
-- Когда deque лучше list? -- Когда нужны частые операции в начале: appendleft() и popleft() — O(1). В list insert(0, x) — O(n). Также для скользящих окон фиксированной длины (maxlen).
-- Как найти N самых частых элементов?
-- Counter(data).most_common(N). Возвращает список кортежей [(element, count), ...], отсортированный по убыванию частоты.
Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.
FAQ
Counter vs dict с ручным подсчётом?
Counter быстрее и чище: Counter(data) вместо цикла с d[key] = d.get(key, 0) + 1. Плюс арифметика, most_common и доступ с дефолтным 0.
Когда использовать namedtuple vs dataclass?
namedtuple — неизменяемый, легковесный, совместим с tuple. dataclass (Python 3.7+) — изменяемый по умолчанию, поддерживает методы, удобнее для сложных объектов. Для простых записей — namedtuple. Для всего остального — dataclass.
Есть ли аналог Counter в SQL?
SELECT col, COUNT(*) FROM table GROUP BY col ORDER BY COUNT(*) DESC — точный аналог Counter(data).most_common().
Как тренироваться
collections — must-know модуль для Python-собеседований. Задачи на структуры данных Python — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.