Pandas Drop Duplicates: Python에서 중복 행을 제거하는 방법
Updated on
중복 행은 실제 데이터셋에서 가장 흔한 데이터 품질 문제 중 하나입니다. 반복적인 API 호출, 중복되는 CSV 내보내기, 고장난 ETL 파이프라인 또는 수동 데이터 입력 시 단순한 복사-붙여넣기 오류를 통해 침투합니다. 방치하면 중복은 행 수를 부풀리고, 평균과 합계를 왜곡하며, 머신러닝 모델에 편향을 도입합니다. 10,000개의 고객 레코드가 있는 것처럼 보이는 데이터셋이 실제로는 8,200명의 고유 고객과 그 위에 구축된 모든 계산을 조용히 손상시키는 1,800개의 유령 항목만 포함할 수 있습니다.
pandas drop_duplicates() 메서드는 이러한 중복 행을 감지하고 제거하는 표준 방법입니다. 이 가이드는 모든 매개변수를 살펴보고, 실제 중복 제거의 일반적인 패턴을 보여주며, 대규모 데이터셋에 대한 성능 고려사항을 다룹니다. 모든 코드 예제는 복사 가능하며 예상 출력을 포함합니다.
중복이 중요한 이유
코드로 들어가기 전에, 중복 행이 정확히 무엇을 망가뜨리는지 이해할 가치가 있습니다:
| 문제 | 발생하는 현상 | 예시 |
|---|---|---|
| 부풀려진 카운트 | 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)와 같은 도구를 사용하여 차트 코드를 작성하지 않고도 결과를 시각적으로 탐색할 수 있으며, 중복 제거가 올바르게 작동했는지 확인하고 곧바로 분석으로 이동할 수 있습니다.