Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

파이썬 Pandas에서 결측값(NaN)이 있는 두 DataFrame을 비교하는 방법


들어가며

Pandas는 데이터 속 결측값(missing value)을 표현하기 위해 NumPy의 NaN 객체(np.nan)를 사용합니다. 이 NaN 값은 흥미로운 수학적 특성을 지니고 있습니다. 예를 들어 NaN은 자기 자신과도 같지 않습니다. 반면 파이썬의 None 객체는 자기 자신과 비교하면 True로 평가됩니다.

np.nan의 특수한 동작 방식

간단한 예제를 통해 np.nan이 어떻게 동작하는지 살펴보겠습니다.

import pandas as pd
import numpy as np

# 파이썬 None 객체를 자기 자신과 비교
print(f"Output \n *** {None == None} ")

실행 결과:

*** True
# NumPy의 nan을 자기 자신과 비교
print(f"Output \n *** {np.nan == np.nan} ")

실행 결과:

*** False
# nan > 10 또는 1000 인가?
print(f"Output \n *** {np.nan > 10} ")

실행 결과:

*** False

위 결과에서 알 수 있듯이 NaN은 어떤 값과 비교해도 False를 반환하는 특징이 있습니다. 이러한 특성 때문에 결측값이 포함된 DataFrame을 비교할 때는 일반적인 방법과 다른 접근이 필요합니다.

등호 연산자(==)를 이용한 DataFrame 비교

전통적으로 Series와 DataFrame은 등호 연산자(==)를 사용해 요소별(element-wise)로 비교하며, 그 결과는 원본과 같은 크기의 불리언 객체입니다. 테니스 선수와 그랜드슬램 우승 횟수 데이터로 실습해 보겠습니다.

# 테니스 선수와 그랜드슬램 우승 횟수로 DataFrame 생성
df = pd.DataFrame(data={"players": ['Federer', 'Nadal', 'Djokovic', 'Murray', 'Medvedev', 'Zverev'],
"titles": [20, 19, 17, 3, np.nan, np.nan]})
# players 열을 인덱스로 설정
df.index = df['players']

# 인덱스를 오름차순으로 정렬
df.sort_index(inplace=True, ascending=True)

# 인덱스가 오름차순으로 정렬되었는지 확인
df.index.is_monotonic_increasing

# 데이터 출력
print(f"Output \n{df}")

실행 결과:

          players  titles
players                  
Djokovic Djokovic    17.0
Federer   Federer    20.0
Medvedev Medvedev     NaN
Murray     Murray     3.0
Nadal       Nadal    19.0
Zverev     Zverev     NaN

1. 스칼라 값과의 비교

이해를 돕기 위해 모든 선수를 스칼라 값 "Federer"와 비교해 결과를 확인해 보겠습니다.

print(f'Output \n {df == "Federer"}')

실행 결과:

          players  titles
players                  
Djokovic    False   False
Federer      True   False
Medvedev    False   False
Murray      False   False
Nadal       False   False
Zverev      False   False

예상대로 동작하지만, 문자열과 숫자형 열을 함께 비교할 경우 FutureWarning 경고가 발생할 수 있습니다. 이는 향후 버전에서 요소별 비교 방식이 변경될 수 있음을 알려주는 경고입니다.

2. DataFrame을 자기 자신과 비교할 때 발생하는 문제

그렇다면 DataFrame을 자기 자신과 비교하면 어떻게 될까요? 당연히 모든 값이 True여야 합니다. 하지만 결측값이 포함되어 있으면 이야기가 달라집니다.

df_compare = df == df
print(f'Output \n {df_compare}')

실행 결과:

          players  titles
players                  
Djokovic    True    True
Federer     True    True
Medvedev    True   False
Murray      True    True
Nadal       True    True
Zverev      True   False

3. .all() 메서드로 검증하기

겉보기에는 대부분의 값이 올바른 것처럼 보입니다. 하지만 각 열이 True만 포함하는지 확인하기 위해 .all() 메서드를 사용해 보면 예상치 못한 결과가 나옵니다.

print(f'Output \n {df_compare.all()}')

실행 결과:

players     True
titles     False
dtype: bool

같은 객체를 비교했음에도 titles 열이 False로 평가된 것입니다.

4. 원인 분석: NaN은 서로 같지 않다

앞서 언급했듯이, 결측값(NaN)은 서로 같다고 비교되지 않기 때문입니다. Medvedev와 Zverev는 우승 기록이 없으므로(즉, NaN) 각 열의 결측값 개수를 세면 titles 열은 2, players 열은 0이 나와야 합니다. 직접 확인해 보겠습니다.

print(f'Output \n {(df_compare == np.nan).sum()}')

실행 결과:

players    0
titles     0
dtype: int64

예상과 달리 두 열 모두 0이 반환되었습니다. NaN은 어떤 값과도 같지 않다는 특성 때문에 == 연산자로는 NaN의 위치를 찾을 수 없습니다. 참고로 결측값을 확인할 때는 == 연산자 대신 .isna() 메서드를 사용해야 올바르게 탐지할 수 있습니다.

올바른 비교 방법: .equals 메서드

두 DataFrame 전체를 비교하는 올바른 방법은 등호 연산자(==)가 아니라 .equals() 메서드를 사용하는 것입니다. 이 메서드는 동일한 위치에 있는 NaN끼리는 같은 값으로 취급하여 비교합니다.

주의할 점은 .eq() 메서드는 == 연산자와 동일한 동작을 하며, .equals()와는 다르다는 것입니다.

print(f'Output \n {df_compare.equals(df_compare)}')

실행 결과:

True

단위 테스트에서의 비교: assert_frame_equal

단위 테스트(unit testing) 과정에서 두 DataFrame을 비교해야 한다면 또 다른 방법도 있습니다. pandas.testing 모듈의 assert_frame_equal 함수는 두 DataFrame이 같지 않으면 AssertionError를 발생시키고, 같으면 None을 반환합니다.

from pandas.testing import assert_frame_equal
print(f'Output \n {assert_frame_equal(df_compare, df_compare) is None}')

실행 결과:

True

정리

  • NaN은 자기 자신과도 같지 않아 == 연산자 비교 시 항상 False를 반환합니다.
  • 등호 연산자(==)는 결측값이 포함된 DataFrame 비교에서 신뢰할 수 없는 결과를 냅니다.
  • 두 DataFrame 전체를 비교할 때는 .equals() 메서드를 사용하는 것이 올바른 방법입니다.
  • 단위 테스트에서는 assert_frame_equal 함수를 활용하면 편리하게 검증할 수 있습니다.