Skip to main content

집합 (Set)

학습 목표

  • 집합의 특성(순서 없음, 중복 불가)을 이해한다
  • 합집합, 교집합, 차집합, 대칭차집합 연산을 수행할 수 있다
  • 중복 제거와 멤버십 테스트에 집합을 활용할 수 있다

왜 중요한가

집합은 수학의 집합과 동일한 개념입니다. 중복을 허용하지 않고, 멤버십 테스트(in)가 O(1)로 매우 빠릅니다. 데이터 전처리에서 고유값 추출, 두 데이터셋 간 교차 분석, 중복 제거 등에 핵심적으로 사용됩니다.

집합 생성

# 중괄호로 생성
fruits = {"사과", "바나나", "체리"}
numbers = {1, 2, 3, 4, 5}

# set() 생성자
from_list = set([1, 2, 2, 3, 3, 3])
print(from_list)  # {1, 2, 3} (중복 제거)

from_string = set("hello")
print(from_string)  # {'h', 'e', 'l', 'o'} (순서 보장 안 됨)

# 빈 집합 (주의: {}는 빈 딕셔너리)
empty_set = set()      # 올바른 방법
empty_dict = {}        # 이것은 빈 딕셔너리!

집합 연산

a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# 합집합 (Union)
print(a | b)              # {1, 2, 3, 4, 5, 6, 7, 8}
print(a.union(b))         # 동일

# 교집합 (Intersection)
print(a & b)              # {4, 5}
print(a.intersection(b))  # 동일

# 차집합 (Difference)
print(a - b)              # {1, 2, 3}
print(a.difference(b))    # 동일

# 대칭 차집합 (Symmetric Difference) - 한쪽에만 있는 요소
print(a ^ b)                        # {1, 2, 3, 6, 7, 8}
print(a.symmetric_difference(b))    # 동일

요소 추가와 삭제

colors = {"빨강", "파랑"}

# 추가
colors.add("초록")
print(colors)  # {"빨강", "파랑", "초록"}

# 여러 요소 추가
colors.update(["노랑", "보라"])
print(colors)  # {"빨강", "파랑", "초록", "노랑", "보라"}

# 삭제
colors.remove("빨강")      # 없으면 KeyError
colors.discard("검정")     # 없어도 에러 없음
popped = colors.pop()       # 임의의 요소 제거 및 반환
colors.clear()              # 전체 삭제

부분집합과 상위집합

a = {1, 2, 3}
b = {1, 2, 3, 4, 5}

print(a.issubset(b))      # True  (a ⊆ b)
print(a <= b)             # True

print(b.issuperset(a))    # True  (b ⊇ a)
print(b >= a)             # True

print(a.isdisjoint({6, 7}))  # True (교집합 없음)

중복 제거 활용

# 리스트에서 중복 제거
data = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(data))
print(unique)  # [1, 2, 3, 4] (순서 보장 안 됨)

# 순서를 유지하면서 중복 제거
def deduplicate(items):
    """순서를 유지하면서 중복을 제거합니다."""
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
print(deduplicate(data))  # [3, 1, 4, 5, 9, 2, 6]

# Python 3.7+ dict로도 가능 (삽입 순서 보존)
unique_ordered = list(dict.fromkeys(data))
print(unique_ordered)  # [3, 1, 4, 5, 9, 2, 6]

멤버십 테스트 성능

# 리스트 vs 집합 멤버십 테스트
large_list = list(range(100_000))
large_set = set(range(100_000))

# 리스트: O(n) - 느림
99_999 in large_list   # 최악의 경우 10만 번 비교

# 집합: O(1) - 빠름
99_999 in large_set    # 해시로 즉시 확인
멤버십 테스트(in)를 자주 수행해야 할 때는 리스트 대신 집합을 사용하세요. 특히 데이터가 클수록 성능 차이가 극적으로 벌어집니다.

frozenset

frozenset은 불변(Immutable) 집합입니다. 딕셔너리의 키로 사용하거나, 집합의 집합을 만들 때 필요합니다.
fs = frozenset([1, 2, 3])
# fs.add(4)  # AttributeError - 수정 불가

# 딕셔너리 키로 사용
permissions = {
    frozenset(["read"]): "viewer",
    frozenset(["read", "write"]): "editor",
    frozenset(["read", "write", "admin"]): "admin",
}

AI/ML에서의 활용

# 학습/검증/테스트 데이터 분리 검증
train_ids = {1, 2, 3, 4, 5, 6, 7, 8}
val_ids = {9, 10}
test_ids = {11, 12}

# 데이터 누출(Data Leakage) 검증
assert train_ids.isdisjoint(val_ids), "학습-검증 데이터 겹침!"
assert train_ids.isdisjoint(test_ids), "학습-테스트 데이터 겹침!"
assert val_ids.isdisjoint(test_ids), "검증-테스트 데이터 겹침!"

# 어휘 사전(Vocabulary) 관리
vocab = set()
for sentence in ["I love Python", "Python is great"]:
    words = sentence.lower().split()
    vocab.update(words)
print(vocab)  # {'i', 'love', 'python', 'is', 'great'}

# 고유 카테고리 추출
labels = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique_labels = sorted(set(labels))
print(unique_labels)  # ['bird', 'cat', 'dog']
label_to_id = {label: idx for idx, label in enumerate(unique_labels)}
print(label_to_id)  # {'bird': 0, 'cat': 1, 'dog': 2}
아니요. 집합의 요소는 해시 가능한(Hashable) 불변 타입이어야 합니다. 리스트 대신 튜플을 사용하세요: {(1, 2), (3, 4)}는 가능하지만, {[1, 2], [3, 4]}TypeError가 발생합니다.
집합은 내부적으로 해시 테이블(Hash Table)을 사용합니다. 요소의 해시값에 따라 저장 위치가 결정되므로, 삽입 순서와 저장/출력 순서가 다를 수 있습니다.

체크리스트

  • 집합의 특성(순서 없음, 중복 불가)을 설명할 수 있다
  • 합집합, 교집합, 차집합, 대칭차집합 연산을 수행할 수 있다
  • 중복 제거에 집합을 활용할 수 있다
  • 리스트 대비 집합의 멤버십 테스트 성능 이점을 설명할 수 있다

다음 문서