В учебной последовательности шесть строк: A, A, B, A, A, C. Если превратить размеры соседних серий в словарь с ключом-кодом, сумма его значений окажется равна четырём: ранняя серия A будет заменена поздней. Строки не исчезли из исходного файла: ошибочным оказался переход от соседних серий к общим частотам. Разберём, что возвращает itertools.groupby и как проверить, на какой вопрос отвечает результат.
Буквы ниже обозначают вымышленные коды категорий, а не идентификаторы реальных участников. Код выполнен в Python 3.12.14 без внешних пакетов; использована официальная документация ветви 3.12. Считаем строки по категориям, не число уникальных людей и не число независимых наблюдений.
Соседняя серия и общая частота — разные задачи
Согласно документации itertools.groupby, новая группа начинается при изменении ключа. Функция не собирает автоматически все одинаковые коды из разных мест последовательности. В нашем порядке две пары A разделены строкой B, поэтому получатся две отдельные серии A.
| Строка исходного файла | Код категории |
|---|---|
| r1 | A |
| r2 | A |
| r3 | B |
| r4 | A |
| r5 | A |
| r6 | C |

Для исследования последовательности такой результат полезен: видны длины непрерывных серий. Если требуется общее количество строк каждой категории, нужно другое представление. Нельзя называть две серии A двумя разными категориями только потому, что они расположены отдельно.
Полный пример: четыре серии, но три категории
runs хранит списки строк каждой серии, run_sizes хранит их длины. last_run_only намеренно показывает ошибочное сворачивание серий в словарь, а totals даёт общий подсчёт после сортировки копии. Исходный список codes сохраняет прежний порядок.
from itertools import groupby
codes = ["A", "A", "B", "A", "A", "C"]
runs = [(key, list(group)) for key, group in groupby(codes)]
run_sizes = [(key, len(group)) for key, group in runs]
last_run_only = {key: len(group) for key, group in runs}
totals = {key: len(list(group)) for key, group in groupby(sorted(codes))}
print("runs =", run_sizes)
print("last_run_only =", last_run_only)
print("sum_last_run_only =", sum(last_run_only.values()))
print("totals =", totals)
print("sum_totals =", sum(totals.values()))
print("original =", codes)
assert run_sizes == [("A", 2), ("B", 1), ("A", 2), ("C", 1)]
assert last_run_only == {"A": 2, "B": 1, "C": 1}
assert sum(last_run_only.values()) == 4
assert totals == {"A": 4, "B": 1, "C": 1}
assert sum(totals.values()) == len(codes) == 6
assert codes == ["A", "A", "B", "A", "A", "C"]
runs = [('A', 2), ('B', 1), ('A', 2), ('C', 1)]
last_run_only = {'A': 2, 'B': 1, 'C': 1}
sum_last_run_only = 4
totals = {'A': 4, 'B': 1, 'C': 1}
sum_totals = 6
original = ['A', 'A', 'B', 'A', 'A', 'C']
Получены серии A/2, B/1, A/2, C/1. В словаре last_run_only повторный ключ A получает значение последней серии: 2, а не сумму 4. Это свойство словаря Python, а не автоматическое суммирование groupby. Поэтому сумма значений ошибочного словаря равна 2 + 1 + 1 = 4 при шести исходных строках.
После sorted(codes) одинаковые коды стоят рядом. В totals получаем A: 4, B: 1, C: 1; сумма равна 6. Здесь сортируется новый список, а не сам codes. Проверки assert сверяют обе промежуточные структуры и итог, а не только число в последней строке.
Когда сортировка поможет, а когда изменит смысл
| Вопрос читателя | Подход к этому примеру |
|---|---|
| Каковы длины серий в порядке файла? | Сохранить исходный порядок и немедленно материализовать каждую группу. |
| Сколько всего строк каждой категории? | Для показанных строковых кодов сгруппировать отсортированную копию по тому же ключу. |
| Нужно ли удалить повторные строки? | Эта программа не отвечает на вопрос о дубликатах и ничего не удаляет. |
Если порядок отражает время или последовательность событий, сортировка по категории уничтожает информацию о расположении серий в этой рабочей копии. Не подменяйте один вопрос другим. Для сложных записей явно задайте одну и ту же функцию ключа при сортировке и группировке; возможность сортировки и смысл ключа нужно проверить для своих типов данных.
Группа, которую возвращает groupby, сама является итератором и разделяет источник с внешним итератором. Если значения нужны позже, сохраните list(group) во время обхода, как в примере. Простое сохранение списка пар из ключа и итератора без чтения групп не равнозначно сохранению всех элементов: продвижение внешнего итератора влияет на доступность предыдущей группы.
Проверка перед переносом в свою работу
- Сформулируйте задачу: соседние серии или общие частоты; укажите единицу подсчёта.
- Сохраните исходный порядок отдельно и зафиксируйте, какую копию сортировали.
- Сверьте сумму частот с числом включённых строк и проверьте повторные ключи при создании словаря.
- При подсчёте по записям определите ключ до запуска. Пропуски, неодинаковые типы и конфликтные значения требуют отдельной проверки.
Шесть кодов достаточны для демонстрации поведения программы, но не для вывода о реальных группах. Этот фрагмент не читает CSV, не проверяет схему файла и не различает копию записи и повторный визит. Отдельно разобрано, как отличить дубликат от повторного наблюдения: там нужен смысл ключей, а здесь проверяется операция группировки последовательности.
Если после проверки структуры нужна консультация по обработке своего набора, можно обсудить помощь со статистическим анализом. Подготовьте исходный файл, описание категорий и точный вопрос: одинаковые счетчики сами по себе не подтверждают корректность исследовательского вывода.