문제 상황
시퀀스(sequence)에서 가장 자주 등장하는 항목을 식별해야 하는 경우가 자주 있습니다. 예를 들어 문서에서 가장 많이 쓰인 단어를 찾거나, 로그 데이터에서 빈도가 높은 이벤트를 분석할 때 이런 작업이 필요합니다.
해결 방법
collections 모듈의 Counter를 사용하면 시퀀스 내 항목의 개수를 아주 손쉽게 추적할 수 있습니다.
Counter란 무엇인가?
Counter는 각 키(key)에 대해 정수형 개수(count)를 저장하는 매핑(mapping) 객체입니다. 이미 존재하는 키를 업데이트하면 해당 키의 개수가 증가합니다. 이 객체는 해시 가능한(hashable) 객체의 등장 횟수를 세거나, 멀티셋(multiset)처럼 활용할 때 유용합니다.
데이터 분석 작업에서 Counter는 없어서는 안 될 강력한 도구 중 하나입니다. Counter는 Python에 오랫동안 포함되어 있었기 때문에 이미 익숙한 분들에게는 좋은 복습이 될 것입니다.
먼저 collections에서 Counter를 임포트합니다.
from collections import Counter
일반 딕셔너리의 한계: KeyError
전통적인 딕셔너리(dictionary)는 존재하지 않는 키에 접근하면 KeyError 예외를 발생시킵니다.
# 빈 딕셔너리 생성
dict = {}
# 빈 딕셔너리에서 키 조회
dict['mystring']
# 에러 메시지
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-12-1e03564507c6> in <module>
...
KeyError: 'mystring'이런 상황에서 KeyError를 어떻게 피할 수 있을까?
Counter는 딕셔너리의 하위 클래스(sub-class)로 딕셔너리와 거의 동일하게 동작하지만, 존재하지 않는 키를 조회했을 때 KeyError를 발생시키는 대신 0을 반환한다는 점이 다릅니다. 덕분에 개수를 누적하는 코드를 훨씬 안전하고 간결하게 작성할 수 있습니다.
# 카운터 정의 c = Counter()
# 존재하지 않는 키 조회
print(f"Output\n{c['mystring']}")출력 결과
0
c['mystring'] += 1
print(f"Output\n{c}")출력 결과
Counter({'mystring': 1})예제
print(f"Output\n{type(c)}")출력 결과
<class 'collections.Counter'>
시퀀스에서 가장 자주 등장하는 항목 찾기
Counter의 또 다른 장점은 객체 리스트를 전달하기만 하면 자동으로 개수를 세어준다는 점입니다. 반복문을 직접 작성해 카운터를 구성할 필요가 없습니다.
Counter('Peas porridge hot peas porridge cold peas porridge in the pot nine days old'.split())출력 결과
Counter({'Peas': 1,
'porridge': 3,
'hot': 1,
'peas': 2,
'cold': 1,
'in': 1,
'the': 1,
'pot': 1,
'nine': 1,
'days': 1,
'old': 1})split() 메서드는 문자열을 공백(white space)을 기준으로 잘라 단어 리스트로 만들어 줍니다. 그러면 Counter가 이 리스트를 순회하며 모든 단어의 개수를 계산하고, 위와 같은 결과를 반환합니다.
most_common()으로 최다 빈도 항목 확인하기
여기서 더 나아가, 문장에서 가장 많이 등장한 단어들만 골라낼 수도 있습니다. most_common() 메서드는 빈도가 높은 항목들을 순서대로 반환합니다.
count = Counter('Peas porridge hot peas porridge cold peas porridge in the pot nine days old'.split())
print(f"Output\n{count.most_common(1)}")출력 결과
[('porridge', 3)]예제
print(f"Output\n{count.most_common(2)}")출력 결과
[('porridge', 3), ('peas', 2)]예제
print(f"Output\n{count.most_common(3)}")출력 결과
[('porridge', 3), ('peas', 2), ('Peas', 1)]반환값이 튜플(tuple)의 리스트라는 점에 주목하세요. 튜플의 첫 번째 요소는 단어이고, 두 번째 요소는 그 개수입니다.
수학 연산으로 Counter 결합하기
Counter 인스턴스의 잘 알려지지 않은 기능 중 하나는 다양한 수학적 연산을 통해 손쉽게 결합할 수 있다는 점입니다.
string = 'Peas porridge hot peas porridge cold peas porridge in the pot nine days old' another_string = 'Peas peas hot peas peas peas cold peas' a = Counter(string.split()) b = Counter(another_string.split())
개수 더하기 (+)
# 개수 더하기
add = a + b
print(f"Output\n{add}")출력 결과
Counter({'peas': 7, 'porridge': 3, 'Peas': 2, 'hot': 2, 'cold': 2, 'in': 1, 'the': 1, 'pot': 1, 'nine': 1, 'days': 1, 'old': 1})개수 빼기 (-)
# 개수 빼기
sub = a - b
print(f"Output\n{sub}")출력 결과
Counter({'porridge': 3, 'in': 1, 'the': 1, 'pot': 1, 'nine': 1, 'days': 1, 'old': 1})멀티셋(Multiset)으로서의 Counter
마지막으로, Counter는 데이터를 컨테이너에 저장하는 방식이 매우 똑똑합니다. 위 출력에서 볼 수 있듯이 같은 단어들을 하나로 묶어 저장하며, 이렇게 묶인 형태를 흔히 멀티셋(multiset)이라고 부릅니다.
elements() 메서드를 사용하면 단어를 하나씩 꺼낼 수 있습니다. 원래의 순서는 기억하지 않지만, 같은 문장에 속한 단어들이 함께 모여 있습니다.
예제
print(f"Output\n{list(a.elements())}")출력 결과
['Peas', 'porridge', 'porridge', 'porridge', 'hot', 'peas', 'peas', 'cold', 'in', 'the', 'pot', 'nine', 'days', 'old']
예제
print(f"Output\n{list(a.values())}")출력 결과
[1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1]
예제
print(f"Output\n{list(a.items())}")출력 결과
[('Peas', 1), ('porridge', 3), ('hot', 1), ('peas', 2), ('cold', 1), ('in', 1), ('the', 1), ('pot', 1), ('nine', 1), ('days', 1), ('old', 1)]마무리
정리하면, Python의 collections.Counter는 다음과 같은 상황에서 특히 유용합니다.
- 존재하지 않는 키 조회 시 KeyError 대신 0을 반환하므로 안전하게 개수를 누적할 수 있습니다.
- 리스트나 문자열을 바로 전달해 반복문 없이 빈도를 계산할 수 있습니다.
most_common(n)으로 최다 빈도 항목을 간단히 추출할 수 있습니다.- +/- 연산자로 여러 Counter를 결합하거나 차감할 수 있습니다.
elements(),values(),items()등 딕셔너리 API를 그대로 활용할 수 있습니다.