[파이썬 100강] 67강. collections.Counter로 빈도 분석 자동화하기
로그, 검색어, 에러 코드, 사용자 행동 이벤트를 다루다 보면 결국 자주 받는 질문은 하나입니다. “그래서 뭐가 제일 많이 나왔는데?” 이번 강의에서는 collections.Counter로 빈도 집계 코드를 짧고 정확하게 만드는 방법을 바로 예제로 익힙니다.
핵심 개념
Counter는 해시 가능한 값을 키로 삼아 등장 횟수(빈도) 를 저장하는 딕셔너리 확장 타입입니다.most_common()으로 상위 항목을 즉시 뽑을 수 있어 Top-N 분석에 매우 유리합니다.update,subtract, 카운터 간 덧셈/뺄셈/교집합/합집합 연산으로 집계 파이프라인을 구성할 수 있습니다.
초보 관점에서 Counter를 이해할 때 핵심은 “딕셔너리를 직접 if key in ...로 누적하는 반복 코드를 표준화한다”는 점입니다. 직접 누적하는 방식도 동작은 하지만, 코드가 길어지고 누락 케이스(초기값 처리, 음수 카운트, 상위 N 추출)가 자주 생깁니다. Counter는 이런 반복 패턴을 언어 차원에서 제공하기 때문에 팀 협업 시 가독성과 안정성이 좋아집니다. 특히 운영 로그처럼 데이터가 계속 들어오는 상황에서는 update()를 통해 배치 단위로 집계를 추가하고, 필요하면 subtract()로 특정 기간 데이터를 제거해 순집계를 만들 수 있습니다. 또한 Counter는 딕셔너리처럼 보이지만 빈도 분석 전용 편의 기능이 많아, 코드 의도가 “빈도 계산”임을 명확하게 드러냅니다. 즉, 단순히 코드 줄 수를 줄이는 도구가 아니라, 분석 로직을 실수 없이 유지하게 도와주는 구조적 도구입니다.
기본 사용
예제 1) 리스트/문자열 빈도 집계의 기본
>>> from collections import Counter
>>> fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
>>> c = Counter(fruits)
>>> c
Counter({'apple': 3, 'banana': 2, 'orange': 1})
>>> c['apple']
3
>>> c['grape']
0
해설:
- 등장하지 않은 키를 조회했을 때
KeyError가 아니라0을 반환합니다. 집계 코드에서 매우 편합니다. - 문자열도 iterable이므로 문자 빈도를 바로 계산할 수 있습니다.
- “초기값 세팅”을 매번 직접 할 필요가 없습니다.
예제 2) Top-N 뽑기와 동률 처리 감각 익히기
>>> from collections import Counter
>>> logs = ['E01', 'E02', 'E01', 'E03', 'E02', 'E01', 'E02']
>>> counter = Counter(logs)
>>> counter.most_common(2)
[('E01', 3), ('E02', 3)]
>>> counter.most_common()
[('E01', 3), ('E02', 3), ('E03', 1)]
해설:
most_common(n)은 상위 n개를(키, 빈도)튜플 리스트로 반환합니다.- 동률이 있을 때 순서는 내부 정렬 기준에 영향받을 수 있으므로, 보고서에서 안정 정렬이 필요하면 추가 정렬 규칙을 명시하세요.
- 대시보드/알림 시스템에서 Top 에러 코드 추출에 바로 사용할 수 있습니다.
예제 3) update/subtract로 기간별 순집계 만들기
>>> from collections import Counter
>>> day1 = Counter(['login', 'login', 'purchase', 'logout'])
>>> day2 = Counter(['login', 'purchase', 'purchase', 'refund'])
>>> total = Counter()
>>> total.update(day1)
>>> total.update(day2)
>>> total
Counter({'login': 3, 'purchase': 3, 'logout': 1, 'refund': 1})
>>> total.subtract(Counter(['refund']))
>>> total
Counter({'login': 3, 'purchase': 3, 'logout': 1, 'refund': 0})
해설:
- 여러 배치 데이터를 순차 누적할 때
update()가 직관적입니다. - 정정 이벤트(취소/환불 등)를 반영할 때
subtract()를 쓰면 로직이 분리되어 읽기 쉽습니다. - 다만 0 또는 음수 카운트가 남을 수 있어 후처리 정책이 필요합니다.
예제 4) 카운터 연산으로 데이터 비교하기
>>> from collections import Counter
>>> a = Counter({'python': 5, 'sql': 2, 'docker': 1})
>>> b = Counter({'python': 2, 'sql': 3, 'k8s': 4})
>>> a + b
Counter({'python': 7, 'k8s': 4, 'sql': 5, 'docker': 1})
>>> a - b
Counter({'python': 3, 'docker': 1})
>>> a & b
Counter({'python': 2, 'sql': 2})
>>> a | b
Counter({'python': 5, 'k8s': 4, 'sql': 3, 'docker': 1})
해설:
+: 단순 합산,-: 음수/0은 기본적으로 결과에서 제거되는 점을 기억하세요.&: 각 키의 최소값(교집합),|: 최대값(합집합)입니다.- “두 기간의 공통 상위 키워드” 같은 비교 분석에 강력합니다.
자주 하는 실수
실수 1) most_common 결과를 딕셔너리처럼 다루기
>>> from collections import Counter
>>> c = Counter(['a', 'b', 'a'])
>>> top = c.most_common(1)
>>> top['a']
Traceback (most recent call last):
...
TypeError: list indices must be integers or slices, not str
원인:
most_common()은list[tuple]을 반환하는데, 이를 Counter/딕셔너리로 착각합니다.
해결:
>>> top = c.most_common(1)
>>> top
[('a', 2)]
>>> key, cnt = top[0]
>>> key, cnt
('a', 2)
- 반환 타입을 먼저 확인하고 언패킹해서 사용하면 실수를 줄일 수 있습니다.
실수 2) 0/음수 카운트를 그대로 보고서에 내보내기
>>> from collections import Counter
>>> c = Counter({'ok': 10, 'fail': 2})
>>> c.subtract(Counter({'fail': 3}))
>>> c
Counter({'ok': 10, 'fail': -1})
원인:
subtract()는 음수를 허용합니다. 집계 의도는 맞지만, 보고서 출력에는 부적합할 수 있습니다.
해결:
>>> cleaned = Counter({k: v for k, v in c.items() if v > 0})
>>> cleaned
Counter({'ok': 10})
- 최종 출력 단계에서
v > 0필터링 규칙을 고정하세요.
실수 3) 비정규화 입력을 그대로 세기
- 증상:
"Error","error"," error "가 서로 다른 키로 집계됨 - 원인: 전처리(공백 제거, 대소문자 정규화) 없이 바로 Counter에 넣음
- 해결:
strip().lower()같은 정규화 파이프라인을 먼저 적용한 후 집계합니다.
실수 4) Counter를 정렬된 구조로 오해하기
- 증상: 항상 같은 순서로 나올 거라 믿고 인덱스 기반 로직 작성
- 원인: dict의 순서 보장 특성과 빈도 정렬 개념을 혼동
- 해결: 순서가 중요하면
most_common()또는sorted(counter.items(), key=...)로 명시적으로 정렬합니다.
실무 패턴
-
입력 검증 규칙
- 집계 전에 값 타입을 문자열/정수 등으로 통일하고
None, 빈 문자열을 제거합니다. - 로그 레벨, 이벤트명, 에러코드 같은 도메인 키는 정규화 함수(
normalize_event)를 통해 단일 규칙으로 맞춥니다.
- 집계 전에 값 타입을 문자열/정수 등으로 통일하고
-
로그/예외 처리 규칙
- 원시 데이터 개수와 정규화 후 유효 데이터 개수를 함께 로깅해 집계 누락 여부를 추적합니다.
- 카운트가 비정상 급증(예: 평소 대비 3배)하면 알림을 발생시키는 임계치를 둡니다.
-
재사용 함수/구조화 팁
build_counter(records, key_fn)형태의 함수로 집계 대상을 일반화합니다.- Top-N 보고서는
to_rank_rows(counter, n)같은 포맷 함수로 분리해 화면/문서 출력 계층과 분리합니다.
-
성능/메모리 체크 포인트
- 키 종류가 매우 많을 때 Counter는 메모리를 많이 쓸 수 있으므로 기간 단위 샤딩(시간별/일별) 집계를 고려합니다.
- 스트리밍 환경에서는 전체 데이터 보관 대신
update()를 배치 처리하고, 원본은 별도 저장소에 두는 구조가 안전합니다.
>>> from collections import Counter
>>> def normalize_event(raw: str) -> str:
... return raw.strip().lower()
...
>>> def count_events(events):
... normalized = [normalize_event(e) for e in events if e and e.strip()]
... return Counter(normalized)
...
>>> count_events([' Error ', 'error', 'LOGIN', 'login', ''])
Counter({'error': 2, 'login': 2})
위 패턴의 포인트는 “집계 이전 정규화”입니다. 실제 장애 분석에서 가장 흔한 함정은 로직이 틀린 게 아니라 데이터가 제각각인 경우입니다. 정규화 → 집계 → 상위 항목 추출을 파이프라인으로 고정하면, 담당자가 바뀌어도 분석 결과 일관성이 유지됩니다. 그리고 Counter는 단순한 자료구조처럼 보이지만, 운영 자동화에서는 작은 규칙 차이가 지표 왜곡으로 직결되므로 출력 정책(0/음수 제거, 동률 정렬 기준)을 코드로 문서화하는 것이 중요합니다.
오늘의 결론
한 줄 요약: 빈도 분석은 Counter로 표준화하면 코드가 짧아지고, 실수는 줄고, 운영 지표의 신뢰도는 올라갑니다.
기억할 것:
Counter는 “딕셔너리 + 빈도 분석 도구”라고 이해하면 쉽습니다.most_common,update,subtract, 카운터 연산을 조합하면 대부분의 집계 요구를 커버할 수 있습니다.- 실무에서는 전처리(정규화)와 후처리(0/음수 필터링) 규칙을 반드시 함께 설계해야 합니다.
연습문제
- 문자열 리스트
['A', 'b', 'a', 'B', 'a', ' c ']를 대소문자/공백 정규화한 뒤 빈도를 집계하고 상위 2개를 구하세요. - 주간 집계
week1 = Counter({'error': 12, 'warn': 30}),week2 = Counter({'error': 8, 'info': 50})가 있을 때 합산 결과와 교집합(&) 결과를 구하세요. - 이벤트 로그에서
cancel이벤트를purchase에서 차감해 순구매 횟수를 구하는 함수를 작성하세요. 음수는 0으로 보정하도록 구현해 보세요.
이전 강의 정답
bisect_left/right로 값 10의 구간 찾기
>>> import bisect
>>> arr = [5, 10, 10, 20, 30]
>>> start = bisect.bisect_left(arr, 10)
>>> end = bisect.bisect_right(arr, 10)
>>> start, end
(1, 3)
- 컷오프 기반 등급 계산
>>> import bisect
>>> cutoffs = [50, 70, 90]
>>> labels = ['D', 'C', 'B', 'A']
>>> def grade(score):
... return labels[bisect.bisect_right(cutoffs, score)]
...
>>> [grade(s) for s in [49, 50, 69, 70, 90]]
['D', 'C', 'C', 'B', 'A']
insort로 스트림 정렬 유지
>>> import bisect
>>> stream = [7, 1, 5, 3, 9]
>>> sorted_values = []
>>> states = []
>>> for x in stream:
... bisect.insort(sorted_values, x)
... states.append(sorted_values.copy())
...
>>> states
[[7], [1, 7], [1, 5, 7], [1, 3, 5, 7], [1, 3, 5, 7, 9]]
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
collections.Counter(표준 라이브러리) - 실행 방법: 터미널
pythonREPL 또는ipython - 재현 체크:
most_common(2)결과에서 상위 빈도 추출 확인subtract()이후 음수 카운트 발생 여부 확인- 정규화 후 집계 결과가 기대와 일치하는지 확인