Skip to content
주제
Pandas
Pandas Drop Duplicates: Python에서 중복 행을 제거하는 방법

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는 모든 사본 삭제
inplaceboolFalseTrue이면 DataFrame을 제자리에서 수정하고 None 반환
ignore_indexboolFalseTrue이면 결과의 인덱스를 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.99

keep=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-03

namesignup_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-02

Red 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
1M1개 열~50 ms
10M1개 열~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)와 같은 도구를 사용하여 차트 코드를 작성하지 않고도 결과를 시각적으로 탐색할 수 있으며, 중복 제거가 올바르게 작동했는지 확인하고 곧바로 분석으로 이동할 수 있습니다.

📚