Pandas Drop Duplicates: Python에서 중복 행을 제거하는 방법
업데이트
중복 행은 실제 데이터셋에서 가장 흔한 데이터 품질 문제 중 하나입니다. 반복적인 API 호출, 중복되는 CSV 내보내기, 고장난 ETL 파이프라인 또는 수동 데이터 입력 시 단순한 복사-붙여넣기 오류를 통해 침투합니다. 방치하면 중복은 행 수를 부풀리고, 평균과 합계를 왜곡하며, 머신러닝 모델에 편향을 도입합니다. 10,000개의 고객 레코드가 있는 것처럼 보이는 데이터셋이 실제로는 8,200명의 고유 고객과 그 위에 구축된 모든 계산을 조용히 손상시키는 1,800개의 유령 항목만 포함할 수 있습니다.
pandas drop_duplicates() 메서드는 이러한 중복 행을 감지하고 제거하는 표준 방법입니다. 이 가이드는 모든 매개변수를 살펴보고, 실제 중복 제거의 일반적인 패턴을 보여주며, 대규모 데이터셋에 대한 성능 고려사항을 다룹니다. 모든 코드 예제는 복사 가능하며 예상 출력을 포함합니다.
- Runcell Science: Claude Science를 대체할 오픈소스 AI 연구 워크스페이스
- 맥 잠자기 방지: 맥북 닫아도 Codex와 Claude Code 계속 실행하기
- OpenClaw vs ZeroClaw vs Pi Agent vs Nanobot: 2026년에 어떤 AI 에이전트 스택을 선택해야 할까?
- Claude Code로 Jupyter 노트북을 분석하는 방법 | Data Science 실무 가이드와 한계
- Claude Code 루틴 사용법: AI 에이전트 cron 작업과 자동 트리거
- Claude Code Desktop에서 Bypass permissions 켜는 법
- Google의 A2A 프로토콜을 사용한 두 개의 Python 에이전트 빌드하기 - 단계별 튜토리얼
- 2025년 파이썬에서 가장 성장하는 상위 10개 데이터 시각화 라이브러리
중복이 중요한 이유
코드로 들어가기 전에, 중복 행이 정확히 무엇을 망가뜨리는지 이해할 가치가 있습니다:
| 문제 | 발생하는 현상 | 예시 |
|---|---|---|
| 부풀려진 카운트 | len(df)와 value_counts()가 과다 집계 | 고객이 3번 표시되어 "총 고객 수"가 3배 높음 |
| 잘못된 평균 | mean()이 중복 행에 더 높은 가중치 부여 | 고가 주문이 두 번 계산되어 평균 주문 금액이 상향 왜곡 |
| 깨진 조인 | 중복이 있는 키로 병합하면 행 폭발 발생 | 다대다 병합이 1:1 매핑 대신 카테시안 곱 생성 |
| 나쁜 ML 모델 | 반복 샘플이 포함된 훈련 데이터가 모델에 편향 도입 | 모델이 중복 예제를 암기하고 과적합 |
| 저장 공간 낭비 | 중복 행이 디스크와 메모리를 소비 | 2GB 데이터셋이 중복 제거 후 1.4GB가 될 수 있음 |
해결 방법은 간단합니다: 중복을 찾고, 어떤 사본을 유지할지(있다면) 결정하고, 나머지를 제거합니다.
기본 구문: df.drop_duplicates()
가장 간단한 호출은 모든 열 값이 동일한 행을 제거합니다:
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob'],
'age': [30, 25, 30, 35, 25],
'city': ['NYC', 'LA', 'NYC', 'Chicago', 'LA']
})
print("Before:")
print(df)
df_clean = df.drop_duplicates()
print("\nAfter:")
print(df_clean)출력:
Before:
name age city
0 Alice 30 NYC
1 Bob 25 LA
2 Alice 30 NYC
3 Charlie 35 Chicago
4 Bob 25 LA
After:
name age city
0 Alice 30 NYC
1 Bob 25 LA
3 Charlie 35 Chicago행 2와 행 4는 행 0과 행 1의 정확한 복사본이었으므로 삭제되었습니다. 원래 인덱스 값(0, 1, 3)이 유지되는 것에 주의하세요. 깔끔한 순차 인덱스를 원한다면 .reset_index(drop=True)를 체인하세요.
전체 메서드 시그니처
DataFrame.drop_duplicates(subset=None, keep='first', inplace=False, ignore_index=False)| 매개변수 | 타입 | 기본값 | 설명 |
|---|---|---|---|
subset | 열 레이블 또는 리스트 | None (모든 열) | 중복 식별 시 이 열만 고려 |
keep | 'first', 'last', False | 'first' | 어떤 중복을 유지할지; False는 모든 사본 삭제 |
inplace | bool | False | True이면 DataFrame을 제자리에서 수정하고 None 반환 |
ignore_index | bool | False | True이면 결과의 인덱스를 0부터 n-1로 재설정 |
df.duplicated()로 먼저 중복 찾기
중복을 제거하기 전에 먼저 검사하고 싶은 경우가 많습니다. duplicated() 메서드는 중복 행을 표시하는 불리언 Series를 반환합니다:
df = pd.DataFrame({
'order_id': [101, 102, 103, 101, 104, 102],
'product': ['Widget', 'Gadget', 'Widget', 'Widget', 'Gizmo', 'Gadget'],
'amount': [29.99, 49.99, 29.99, 29.99, 19.99, 49.99]
})
# Show which rows are duplicates
print(df.duplicated())출력:
0 False
1 False
2 False
3 True
4 False
5 True
dtype: bool실제 중복 행을 보려면:
duplicates = df[df.duplicated(keep=False)]
print(duplicates)출력:
order_id product amount
0 101 Widget 29.99
1 102 Gadget 49.99
3 101 Widget 29.99
5 102 Gadget 49.99keep=False를 사용하면 모든 사본을 중복으로 표시합니다(두 번째 발생만이 아님). 이를 통해 중복에 관련된 모든 행을 볼 수 있습니다.
중복 카운트
중복 행이 몇 개 존재하는지 빠르게 세려면:
num_duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {num_duplicates}")
total_rows = len(df)
unique_rows = df.drop_duplicates().shape[0]
print(f"Total: {total_rows}, Unique: {unique_rows}, Duplicates: {total_rows - unique_rows}")출력:
Number of duplicate rows: 2
Total: 6, Unique: 4, Duplicates: 2이것은 정리 전후에 유용한 건전성 검사입니다.
subset 매개변수: 특정 열만 확인
모든 열이 일치해야 하는 것이 아니라 키 열을 기반으로 중복 제거하고 싶은 경우가 많습니다. subset 매개변수를 사용하면 고유성을 결정하는 열을 지정할 수 있습니다:
df = pd.DataFrame({
'email': ['alice@mail.com', 'bob@mail.com', 'alice@mail.com', 'charlie@mail.com'],
'name': ['Alice', 'Bob', 'Alice Smith', 'Charlie'],
'signup_date': ['2025-01-01', '2025-01-02', '2025-01-15', '2025-01-03']
})
print("Original:")
print(df)
# Deduplicate by email only
df_deduped = df.drop_duplicates(subset=['email'])
print("\nDeduplicated by email:")
print(df_deduped)출력:
Original:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
2 alice@mail.com Alice Smith 2025-01-15
3 charlie@mail.com Charlie 2025-01-03
Deduplicated by email:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
3 charlie@mail.com Charlie 2025-01-03name과 signup_date 값이 다르더라도 alice@mail.com이 이미 행 0에 나타났기 때문에 행 2가 제거되었습니다. 여러 열을 전달할 수도 있습니다: subset=['email', 'name']은 두 열이 모두 일치할 때만 행을 중복으로 간주합니다.
keep 매개변수: first, last 또는 False
keep 매개변수는 어떤 발생이 생존하는지를 제어합니다:
df = pd.DataFrame({
'sensor_id': ['S1', 'S2', 'S1', 'S2', 'S1'],
'reading': [22.5, 18.3, 23.1, 18.3, 24.0],
'timestamp': ['08:00', '08:00', '09:00', '09:00', '10:00']
})
print("keep='first' (default):")
print(df.drop_duplicates(subset=['sensor_id'], keep='first'))
print("\nkeep='last':")
print(df.drop_duplicates(subset=['sensor_id'], keep='last'))
print("\nkeep=False (drop all duplicates):")
print(df.drop_duplicates(subset=['sensor_id'], keep=False))출력:
keep='first' (default):
sensor_id reading timestamp
0 S1 22.5 08:00
1 S2 18.3 08:00
keep='last':
sensor_id reading timestamp
3 S2 18.3 09:00
4 S1 24.0 10:00
keep=False (drop all duplicates):
Empty DataFrame
Columns: [sensor_id, reading, timestamp]
Index: []| keep 값 | 동작 | 사용 사례 |
|---|---|---|
'first' | 첫 번째 발생을 유지하고 이후 것을 삭제 | 가장 오래된 레코드 유지 |
'last' | 마지막 발생을 유지하고 이전 것을 삭제 | 가장 최신 레코드 유지 |
False | 중복이 있는 모든 행 삭제 | 진정으로 고유한 행 찾기 (사본 없음) |
위 예제에서 keep=False는 두 센서 ID가 모두 두 번 이상 나타나기 때문에 빈 DataFrame을 반환합니다.
제자리 변경 vs 새 DataFrame 반환
기본적으로 drop_duplicates()는 새 DataFrame을 반환하고 원본은 변경하지 않습니다. inplace=True를 설정하면 원본을 직접 수정합니다:
df = pd.DataFrame({
'id': [1, 2, 2, 3],
'value': ['a', 'b', 'b', 'c']
})
# Returns new DataFrame (original unchanged)
df_new = df.drop_duplicates()
print(f"Original length: {len(df)}, New length: {len(df_new)}")
# Modifies in place (returns None)
df.drop_duplicates(inplace=True)
print(f"After inplace: {len(df)}")출력:
Original length: 4, New length: 3
After inplace: 3현대 pandas 스타일은 inplace=True를 피하고 대신 할당을 사용하는 것을 권장합니다 (df = df.drop_duplicates()). 이렇게 하면 특히 체인 연산에서 코드를 더 읽기 쉽고 디버그하기 쉽게 만듭니다.
대소문자를 구분하지 않는 중복 감지
기본적으로 pandas는 "Alice"와 "alice"를 다른 값으로 취급합니다. 대소문자를 구분하지 않는 중복 제거를 위해 먼저 열을 정규화합니다:
df = pd.DataFrame({
'name': ['Alice', 'alice', 'ALICE', 'Bob', 'bob'],
'score': [90, 85, 92, 78, 80]
})
# Create a normalized column for comparison
df['name_lower'] = df['name'].str.lower()
# Deduplicate on the normalized column, keep the first original-case entry
df_deduped = df.drop_duplicates(subset=['name_lower']).drop(columns=['name_lower'])
print(df_deduped)출력:
name score
0 Alice 90
3 Bob 78이 패턴은 대소문자를 구분하지 않는 중복을 정확히 식별하면서 원래 대소문자를 유지합니다.
실제 예제: 고객 데이터베이스 정리
현실적인 시나리오입니다. CRM에서 고객 내보내기를 받았는데, 같은 고객이 다른 영업 담당자에 의해 여러 번 입력되었습니다:
import pandas as pd
customers = pd.DataFrame({
'customer_id': [1001, 1002, 1001, 1003, 1002, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Acme Corp', 'Gamma LLC', 'Beta Inc.', 'Delta Co'],
'email': ['acme@mail.com', 'beta@mail.com', 'acme@mail.com', 'gamma@mail.com', 'beta@mail.com', 'delta@mail.com'],
'revenue': [50000, 30000, 52000, 45000, 30000, 20000],
'last_contact': ['2025-12-01', '2025-11-15', '2026-01-10', '2025-10-20', '2025-11-15', '2026-01-05']
})
print(f"Rows before cleaning: {len(customers)}")
print(f"Duplicate customer_ids: {customers.duplicated(subset=['customer_id']).sum()}")
# Sort by last_contact descending so the most recent entry is first
customers['last_contact'] = pd.to_datetime(customers['last_contact'])
customers = customers.sort_values('last_contact', ascending=False)
# Keep the most recent record for each customer_id
customers_clean = customers.drop_duplicates(subset=['customer_id'], keep='first')
customers_clean = customers_clean.sort_values('customer_id').reset_index(drop=True)
print(f"\nRows after cleaning: {len(customers_clean)}")
print(customers_clean)출력:
Rows before cleaning: 6
Duplicate customer_ids: 2
Rows after cleaning: 4
customer_id name email revenue last_contact
0 1001 Acme Corp acme@mail.com 52000 2026-01-10
1 1002 Beta Inc beta@mail.com 30000 2025-11-15
2 1003 Gamma LLC gamma@mail.com 45000 2025-10-20
3 1004 Delta Co delta@mail.com 20000 2026-01-05여기서 핵심 패턴은 먼저 정렬하고, keep='first'로 중복 제거하기입니다. 중복 제거 전에 last_contact를 내림차순으로 정렬함으로써 각 고객의 가장 최신 레코드가 생존합니다.
실제 예제: 웹 스크레이핑 결과 중복 제거
웹 스크레이퍼는 페이지가 여러 번 크롤링되거나 페이지네이션이 겹칠 때 흔히 중복을 생성합니다:
import pandas as pd
scraped = pd.DataFrame({
'url': [
'https://shop.com/item/101',
'https://shop.com/item/102',
'https://shop.com/item/101',
'https://shop.com/item/103',
'https://shop.com/item/102',
'https://shop.com/item/104',
],
'title': ['Blue Widget', 'Red Gadget', 'Blue Widget', 'Green Gizmo', 'Red Gadget', 'Yellow Thing'],
'price': [19.99, 29.99, 19.99, 39.99, 31.99, 14.99],
'scraped_at': ['2026-02-01', '2026-02-01', '2026-02-02', '2026-02-02', '2026-02-02', '2026-02-02']
})
print(f"Total scraped rows: {len(scraped)}")
print(f"Unique URLs: {scraped['url'].nunique()}")
# Keep the latest scrape for each URL (prices may have changed)
scraped['scraped_at'] = pd.to_datetime(scraped['scraped_at'])
scraped = scraped.sort_values('scraped_at', ascending=False)
products = scraped.drop_duplicates(subset=['url'], keep='first').reset_index(drop=True)
print(f"\nCleaned rows: {len(products)}")
print(products)출력:
Total scraped rows: 6
Unique URLs: 4
Cleaned rows: 4
url title price scraped_at
0 https://shop.com/item/101 Blue Widget 19.99 2026-02-02
1 https://shop.com/item/102 Red Gadget 31.99 2026-02-02
2 https://shop.com/item/103 Green Gizmo 39.99 2026-02-02
3 https://shop.com/item/104 Yellow Thing 14.99 2026-02-02Red Gadget의 가격이 스크레이핑 사이에 29.99에서 31.99로 업데이트되었음에 주목하세요. 최신 항목을 유지함으로써 가장 현재의 가격을 캡처합니다.
drop_duplicates() vs groupby().first(): 언제 어떤 것을 사용할까
두 접근 방식 모두 데이터를 중복 제거할 수 있지만, 다르게 작동합니다:
import pandas as pd
df = pd.DataFrame({
'user_id': [1, 1, 2, 2, 3],
'action': ['login', 'purchase', 'login', 'login', 'purchase'],
'timestamp': ['2026-01-01', '2026-01-02', '2026-01-01', '2026-01-03', '2026-01-01']
})
# Method 1: drop_duplicates
result1 = df.drop_duplicates(subset=['user_id'], keep='first')
print("drop_duplicates:")
print(result1)
# Method 2: groupby().first()
result2 = df.groupby('user_id').first().reset_index()
print("\ngroupby().first():")
print(result2)출력:
drop_duplicates:
user_id action timestamp
0 1 login 2026-01-01
2 2 login 2026-01-01
4 3 purchase 2026-01-01
groupby().first():
user_id action timestamp
0 1 login 2026-01-01
1 2 login 2026-01-01
2 3 purchase 2026-01-01결과는 비슷해 보이지만 중요한 차이점이 있습니다:
| 특징 | drop_duplicates() | groupby().first() |
|---|---|---|
| 속도 | 단순 중복 제거에서 더 빠름 | 그룹화 오버헤드로 인해 더 느림 |
| NaN 처리 | NaN 값을 그대로 유지 | first()는 기본적으로 NaN을 건너뜀 |
| 인덱스 | 원래 인덱스 유지 | 그룹 키로 재설정 |
| 집계 | 다른 열을 집계할 수 없음 | agg()와 결합하여 다중 열 요약 가능 |
| 메모리 | 메모리 사용량이 적음 | 중간 GroupBy 객체 생성 |
| 사용 사례 | 정확하거나 부분적인 중복 제거 | 집계를 계산하면서 중복 제거 |
경험 법칙: 단순히 중복 행을 제거하고 싶을 때는 drop_duplicates()를 사용하세요. 중복 행의 값도 집계해야 할 때는 groupby().first() (또는 groupby().agg())를 사용하세요 -- 예를 들어, 첫 번째 이름을 유지하면서 중복 고객 레코드의 매출을 합산하는 경우입니다.
대규모 DataFrame을 위한 성능 팁
수백만 행을 다룰 때 중복 제거가 병목이 될 수 있습니다. 속도를 높이는 실용적인 방법은 다음과 같습니다:
1. subset 열 지정
모든 열을 확인하는 것보다 키 열만 확인하는 것이 더 빠릅니다:
# Slower: checks every column
df.drop_duplicates()
# Faster: checks only the key column
df.drop_duplicates(subset=['user_id'])2. 적절한 데이터 타입 사용
카디널리티가 낮은 경우 중복 제거 전에 문자열 열을 category 타입으로 변환합니다:
df['status'] = df['status'].astype('category')
df['country'] = df['country'].astype('category')
df_clean = df.drop_duplicates(subset=['status', 'country'])3. 필요할 때만 중복 제거 전에 정렬
keep='first'가 어떤 행을 선택할지 제어하기 위해서만 큰 DataFrame을 정렬하면 상당한 시간이 추가됩니다. 어떤 중복이 남는지 상관없다면 정렬을 건너뛰세요.
4. 매우 큰 파일에 대해 청크 처리 고려
메모리에 맞지 않는 파일의 경우 청크로 처리합니다:
chunks = pd.read_csv('large_file.csv', chunksize=100_000)
seen = set()
clean_chunks = []
for chunk in chunks:
chunk = chunk.drop_duplicates(subset=['id'])
new_rows = chunk[~chunk['id'].isin(seen)]
seen.update(new_rows['id'].tolist())
clean_chunks.append(new_rows)
df_clean = pd.concat(clean_chunks, ignore_index=True)5. 벤치마크: 일반적인 성능
| 행 수 | 확인 열 | 시간 (약) |
|---|---|---|
| 100K | 전체 (10개 열) | ~15 ms |
| 1M | 전체 (10개 열) | ~150 ms |
| 1M | 1개 열 | ~50 ms |
| 10M | 1개 열 | ~500 ms |
이 수치는 하드웨어와 데이터 타입에 따라 다르지만, 핵심 요점은 drop_duplicates()가 선형적으로 확장되며 대부분의 데이터셋을 1초 미만으로 처리한다는 것입니다.
PyGWalker로 정리된 데이터 탐색하기
중복을 제거한 후 다음 단계는 보통 정리된 데이터셋을 탐색하는 것입니다 -- 분포를 확인하고, 이상치를 찾고, 중복 제거가 예상대로 작동했는지 검증합니다. 여러 matplotlib이나 seaborn 호출을 작성하는 대신 PyGWalker (opens in a new tab)를 사용할 수 있습니다. 이것은 모든 pandas DataFrame을 Jupyter Notebook에서 직접 인터랙티브한 Tableau 스타일의 시각화 인터페이스로 변환하는 오픈소스 Python 라이브러리입니다.
import pandas as pd
import pygwalker as pyg
# Your cleaned customer data
customers_clean = pd.DataFrame({
'customer_id': [1001, 1002, 1003, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co'],
'revenue': [52000, 30000, 45000, 20000],
'region': ['East', 'West', 'East', 'South']
})
# Launch interactive visualization
walker = pyg.walk(customers_clean)PyGWalker를 사용하면 region을 x축에, revenue를 y축에 드래그하여 지역별 매출 분포를 즉시 확인할 수 있습니다. 필드를 드래그 앤 드롭하는 것만으로 막대 차트, 산점도, 히스토그램, 히트맵을 만들 수 있습니다 -- 차트 코드가 필요 없습니다. 정리 로직이 합리적인 결과를 만들어냈는지 확인하고 싶을 때 중복 제거 후에 특히 유용합니다.
pip install pygwalker로 PyGWalker를 설치하거나 Google Colab (opens in a new tab)에서 사용해 보세요.
FAQ
drop_duplicates()는 원본 DataFrame을 수정하나요?
아니요, 기본적으로 drop_duplicates()는 새 DataFrame을 반환하고 원본은 변경하지 않습니다. 제자리에서 수정하려면 inplace=True를 전달하세요. 하지만 권장 방법은 할당을 사용하는 것입니다: df = df.drop_duplicates().
하나의 열만 기준으로 중복을 삭제하려면 어떻게 하나요?
열 이름을 subset 매개변수에 전달합니다: df.drop_duplicates(subset=['email']). 이렇게 하면 각 고유 이메일의 첫 번째 행을 유지하고, 다른 열의 차이와 관계없이 같은 이메일을 가진 후속 행을 삭제합니다.
duplicated()와 drop_duplicates()의 차이점은 무엇인가요?
duplicated()는 어떤 행이 중복인지 표시하는 불리언 Series를 반환합니다(검사 및 카운트에 유용). drop_duplicates()는 중복 행이 제거된 DataFrame을 반환합니다. 데이터를 이해하기 위해 먼저 duplicated()를 사용하고, 정리하기 위해 drop_duplicates()를 사용합니다.
조건에 따라 중복을 삭제할 수 있나요?
drop_duplicates()로 직접은 할 수 없습니다. 대신 조건 열로 DataFrame을 먼저 정렬한 다음 keep='first'로 drop_duplicates()를 호출합니다. 예를 들어, 각 고객의 가장 높은 매출 행을 유지하려면: df.sort_values('revenue', ascending=False).drop_duplicates(subset=['customer_id'], keep='first').
대소문자를 구분하지 않는 중복을 어떻게 처리하나요?
임시 소문자 열을 만들고, 그것으로 중복 제거를 수행한 다음, 도우미 열을 삭제합니다: df['key'] = df['name'].str.lower() 다음에 df.drop_duplicates(subset=['key']).drop(columns=['key']). 이렇게 하면 원래 대소문자를 유지하면서 중복을 정확히 식별합니다.
결론
중복 행을 제거하는 것은 모든 데이터 정리 워크플로우에서 기본적인 단계입니다. pandas의 drop_duplicates() 메서드는 몇 가지 매개변수만으로 대부분의 중복 제거 작업을 처리합니다:
- **
subset**을 사용하여 모든 열이 아닌 특정 열에서 중복을 제거합니다. keep='first'또는 **keep='last'**를 사용하여 어떤 발생이 남을지 제어합니다; **keep=False**로 모든 사본을 제거합니다.- **
duplicated()**를 사용하여 제거하기 전에 중복을 검사하고 카운트합니다. - 특정 행을 유지해야 할 때(예: 가장 최신 레코드)는 중복 제거 전에 정렬합니다.
- 대소문자를 구분하지 않는 중복 제거를 위해 먼저 임시 열에서 소문자로 정규화합니다.
- 대규모 데이터셋의 경우
subset열을 제한하고 적절한 데이터 타입을 사용하여 성능을 빠르게 유지합니다.
데이터가 정리되면 PyGWalker (opens in a new tab)와 같은 도구를 사용하여 차트 코드를 작성하지 않고도 결과를 시각적으로 탐색할 수 있으며, 중복 제거가 올바르게 작동했는지 확인하고 곧바로 분석으로 이동할 수 있습니다.