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 — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.