import pandas as pd
import numpy as npPandas로 데이터 연산하기
NumPy의 큰 장점 중 하나는 기본 산술 연산(덧셈, 뺄셈, 곱셈 등)은 물론, 복잡한 연산(삼각 함수, 지수 및 로그 함수 등)까지 요소별로 빠르게 수행할 수 있다는 점입니다. Pandas는 NumPy의 이러한 기능을 대부분 상속받았으며, NumPy 배열 연산: 범용 함수에서 소개한 ufunc가 그 핵심 역할을 합니다.
하지만 Pandas에는 몇 가지 유용한 차별점이 있습니다. 단항 연산(부호 변경, 삼각 함수 등)의 경우 ufunc 결과에서도 인덱스와 열 레이블이 그대로 유지됩니다. 이항 연산(덧셈, 곱셈 등)의 경우에는 Pandas가 객체를 ufunc에 전달할 때 자동으로 인덱스를 정렬합니다. 덕분에 데이터의 맥락을 유지하면서 서로 다른 소스의 데이터를 결합하는 작업이 Pandas에서는 매우 매끄럽게 이루어집니다. (NumPy 배열만 사용했다면 오류가 발생하기 쉬웠을 작업들입니다.) 추가적으로 1차원 Series와 2차원 DataFrame 사이의 연산 규칙도 함께 살펴보겠습니다.
Ufuncs: 인덱스 보존
Pandas는 NumPy와 함께 사용하도록 설계되었으므로, 모든 NumPy ufunc는 Pandas ‘Series’와 ’DataFrame’ 객체에서 잘 작동합니다. 이를 확인하기 위해 먼저 간단한 Series와 DataFrame을 정의해 보겠습니다.
rng = np.random.default_rng(42)
ser = pd.Series(rng.integers(0, 10, 4))
ser0 0
1 7
2 6
3 4
dtype: int64
df = pd.DataFrame(rng.integers(0, 10, (3, 4)), columns=["A", "B", "C", "D"])
df| A | B | C | D | |
|---|---|---|---|---|
| 0 | 4 | 8 | 0 | 6 |
| 1 | 2 | 0 | 5 | 9 |
| 2 | 7 | 7 | 7 | 7 |
이러한 객체에 NumPy ufunc를 적용하면, 결과물은 인덱스가 보존된 또 다른 Pandas 객체가 됩니다.
np.exp(ser)0 1.000000
1 1096.633158
2 403.428793
3 54.598150
dtype: float64
이는 더 복잡한 연산 과정에서도 동일하게 적용됩니다.
np.sin(df * np.pi / 4)| A | B | C | D | |
|---|---|---|---|---|
| 0 | 1.224647e-16 | -2.449294e-16 | 0.000000 | -1.000000 |
| 1 | 1.000000e+00 | 0.000000e+00 | -0.707107 | 0.707107 |
| 2 | -7.071068e-01 | -7.071068e-01 | -0.707107 | -0.707107 |
NumPy 배열 연산: 범용 함수에서 다룬 모든 ufunc를 같은 방식으로 활용합니다.
Ufuncs: 인덱스 정렬
두 개의 ‘Series’ 또는 ‘DataFrame’ 객체에 이항 연산을 수행할 때, Pandas는 연산 과정에서 자동으로 인덱스를 정렬합니다. 이는 다음 예제들처럼 일부 데이터가 누락된 경우를 다룰 때 매우 편리합니다.
Series 인덱스 정렬
예를 들어 서로 다른 소스에서 데이터를 가져와서, 면적 기준 미국 상위 3개 주와 인구 기준 미국 상위 3개 주를 비교한다고 가정해 보겠습니다.
area = pd.Series(
{"Alaska": 1723337, "Texas": 695662, "California": 423967}, name="area"
)
population = pd.Series(
{"California": 39538223, "Texas": 29145505, "Florida": 21538187}, name="population"
)인구 밀도를 계산하기 위해 이들을 나누면 어떤 결과가 나오는지 살펴보겠습니다.
population / areaAlaska NaN
California 93.257784
Florida NaN
Texas 41.896072
dtype: float64
결과 배열에는 두 입력 배열 인덱스의 합집합이 포함됩니다. 이는 다음과 같이 인덱스를 확인하여 알 수 있습니다.
area.index.union(population.index)Index(['Alaska', 'California', 'Florida', 'Texas'], dtype='object')
어느 한쪽에만 데이터가 있는 항목은 NaN(“Not a Number”)으로 표시됩니다. 이는 Pandas가 누락된 데이터를 처리하는 표준 방식입니다.(누락된 데이터 처리에서 더 자세히 다룹니다.) 이러한 인덱스 매칭 방식은 Python의 모든 내장 산술 연산 식에 적용되며, 누락된 값은 모두 NaN이 됩니다.
A = pd.Series([2, 4, 6], index=[0, 1, 2])
B = pd.Series([1, 3, 5], index=[1, 2, 3])
A + B0 NaN
1 5.0
2 9.0
3 NaN
dtype: float64
만약 NaN 값을 원하지 않는다면, 연산자 대신 객체 메서드를 사용하여 채우기(Fill) 값을 직접 지정합니다. 예를 들어 A.add(B)는 A + B와 같지만, A나 B에 없는 요소를 어떤 값으로 채울지 명시할 수 있는 옵션을 제공합니다.
A.add(B, fill_value=0)0 2.0
1 5.0
2 9.0
3 5.0
dtype: float64
DataFrame 인덱스 정렬
DataFrame 객체 연산에서도 행(Index)과 열(Column) 모두에 대해 동일한 방식의 정렬이 이루어집니다.
A = pd.DataFrame(rng.integers(0, 20, (2, 2)), columns=["a", "b"])
A| a | b | |
|---|---|---|
| 0 | 10 | 2 |
| 1 | 16 | 9 |
B = pd.DataFrame(rng.integers(0, 10, (3, 3)), columns=["b", "a", "c"])
B| b | a | c | |
|---|---|---|---|
| 0 | 5 | 3 | 1 |
| 1 | 9 | 7 | 6 |
| 2 | 4 | 8 | 5 |
A + B| a | b | c | |
|---|---|---|---|
| 0 | 13.0 | 7.0 | NaN |
| 1 | 23.0 | 18.0 | NaN |
| 2 | NaN | NaN | NaN |
두 객체의 순서에 상관없이 인덱스가 정확히 매칭되며, 결과물은 정렬된 상태로 반환됩니다. ’Series’와 마찬가지로 산술 메서드를 사용하여 누락된 데이터를 원하는 값(fill_value)으로 채울 수 있습니다. 여기서는 A에 들어있는 모든 값의 평균으로 채워보겠습니다.
A.add(B, fill_value=A.values.mean())| a | b | c | |
|---|---|---|---|
| 0 | 13.00 | 7.00 | 10.25 |
| 1 | 23.00 | 18.00 | 15.25 |
| 2 | 17.25 | 13.25 | 14.25 |
다음 표는 Python 연산자와 그에 대응하는 Pandas 객체 메서드입니다.
| Python 연산자 | Pandas 메서드 |
|---|---|
+ |
add |
- |
sub, subtract |
* |
mul, multiply |
/ |
truediv, div, divide |
// |
floordiv |
% |
mod |
** |
pow |
Ufuncs: DataFrame과 Series 간의 연산
DataFrame과 Series 사이의 연산에서도 인덱스와 열 정렬은 동일하게 유지됩니다. 이는 2차원 배열과 1차원 NumPy 배열 간의 연산과 매우 흡사합니다. 먼저, 2차원 배열에서 그 배열의 한 행을 빼는 흔한 작업을 예로 들어보겠습니다.
A = rng.integers(10, size=(3, 4))
Aarray([[4, 4, 2, 0],
[5, 8, 0, 8],
[8, 2, 6, 1]])
A - A[0]array([[ 0, 0, 0, 0],
[ 1, 4, -2, 8],
[ 4, -2, 4, 1]])
NumPy의 브로드캐스팅(Broadcasting) 규칙(배열 연산: 브로드캐스팅 참고)에 따라, 2차원 배열에서 한 행을 빼면 행 단위로 뺄셈이 적용됩니다.
Pandas에서도 이와 유사하게 행 단위 연산이 기본으로 작동합니다.
df = pd.DataFrame(A, columns=["Q", "R", "S", "T"])
df - df.iloc[0]| Q | R | S | T | |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 4 | -2 | 8 |
| 2 | 4 | -2 | 4 | 1 |
대신 열 단위로 연산하고 싶다면, 앞서 언급한 객체 메서드를 사용하면서 axis 키워드를 지정하면 됩니다.
df.subtract(df["R"], axis=0)| Q | R | S | T | |
|---|---|---|---|---|
| 0 | 0 | 0 | -2 | -4 |
| 1 | -3 | 0 | -8 | 0 |
| 2 | 6 | 0 | 4 | -1 |
다른 연산들과 마찬가지로, 이러한 DataFrame/Series 간 연산에서도 두 요소 사이의 인덱싱은 자동으로 정렬됩니다.
halfrow = df.iloc[0, ::2]
halfrowQ 4
S 2
Name: 0, dtype: int64
df - halfrow| Q | R | S | T | |
|---|---|---|---|---|
| 0 | 0.0 | NaN | 0.0 | NaN |
| 1 | 1.0 | NaN | -2.0 | NaN |
| 2 | 4.0 | NaN | 4.0 | NaN |
Pandas는 연산 시 인덱스와 열 정보를 보존하고 자동으로 정렬해주므로 데이터의 맥락이 항상 유지됩니다. 덕분에 가공되지 않은 NumPy 배열로 서로 다른 형태나 순서의 데이터를 다룰 때 흔히 발생하는 오류를 효과적으로 방지합니다.