GIL's LAB

Numpy와 Pandas에서의 배열 연산 본문

파이썬/데이터 분석을 위한 파이썬

Numpy와 Pandas에서의 배열 연산

GIL~ 2021. 12. 10. 11:53

 

넘파이의 가장 큰 장점으로 매우 빠른 배열 간 연산을 꼽을 수 있습니다. 배열 간 연산이란 크기가 같은 두 배열에 대해, 같은 위치에 있는 요소끼리의 수행하는 연산을 의미합니다. 예를 들어, 크기가 n인 두 배열 x = (x1, x2, ..., xn)y = (y1, y2, ..., yn)에 대해 임의의 연산자 ◇를 사용한 배열 연산은 다음과 같이 정의됩니다.

 

x ◇ y = (x1 ◇ y1, x2 ◇ y2, ..., xn ◇ yn)

 

넘파이는 C로 작성되었기에, 파이썬의 반복문을 사용하는 것보다 훨씬 빠른 속도의 배열 연산을 자랑합니다. 이 포스팅에서는 넘파이의 배열 연산에 대해 알아보겠습니다.

 

유니버설 함수

유니버설 함수(universal functions, ufuncs)+, -, *, /, ** 등 파이썬에서 두 숫자형 변수에 대해 정의된 연산자를 ndarray에도 정의한 것이라 할 수 있습니다. 다시 말해, 유니버설 함수는 벡터 및 행렬 간 연산을 수행하는 함수입니다. 유니버설 함수는 반복문보다 배열 간 연산을 훨씬 빠르고 쉽게 구현할 수 있다는 장점이 있습니다.

 

반복문과의 속도 비교

유니버설 함수가 반복문에 비해 얼마나 빠른지 간단한 실험을 통해 알아보겠습니다. 먼저, x1x2에 백만 개의 난수로 구성된 ndarray를 정의해줍니다.

import numpy as np
x1 = np.random.random(10 ** 6) # 크기 100만의 난수 배열 생성
x2 = np.random.random(10 ** 6)

다음으로 유니버설 함수와 반복문을 이용하여 x1x2의 같은 위치에 있는 요소끼리 더하는 데 걸리는 시간을 각각 측정하겠습니다. 유니버설 함수를 사용했을 때 소요된 시간을 측정해보니, 아래와 같이 평균 5.63ms임을 확인했습니다.

%%timeit
s = x1 + x2

[실행 결과]

5.63 ms ± 440 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

이번에는 for문을 이용했을 때의 시간을 계산해보겠습니다. s를 빈 리스트로 만든 뒤, x1x2를 각각 e1e2로 순회하면서 e1e2의 합을 s에 추가했습니다.

%%timeit
s = []
for e1, e2 in zip(x1, x2):
    s.append(e1 + e2)

[실행 결과]

415 ms ± 16.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

 

배열 산술 연산

유니버설 함수를 사용한 배열 산술 연산 예시를 보겠습니다. 유니버설 함수는 파이썬의 기본 산술 연산자를 사용하기 때문에 특별히 함수명을 기억할 필요도 없습니다.

x = np.arange(4)
y = np.linspace(1, 10, 4)
print("x     =", x)
print("y     =", y)
print("x + 5 =", x + 5)
print("x + y =", x + y)
print("x - 5 =", x - 5)
print("x * 2 =", x * 2)
print("x * y =", x * y)
print("x / 2 =", x / 2)
print("-x     = ", -x)
print("x ** 2 = ", x ** 2)
print("x % 2  = ", x % 2)

[실행 결과]

x     = [0 1 2 3]
y     = [ 1.  4.  7. 10.]
x + 5 = [5 6 7 8]
x + y = [ 1.  5.  9. 13.]
x - 5 = [-5 -4 -3 -2]
x * 2 = [0 2 4 6]
x * y = [ 0.  4. 14. 30.]
x / 2 = [0.  0.5 1.  1.5]
-x     =  [ 0 -1 -2 -3]
x ** 2 =  [0 1 4 9]
x % 2  =  [0 1 0 1]

 

비교 연산

유니버설 함수로 배열 간 비교 연산도 가능합니다. , 대응되는 요소끼리 비교를 한 결과가 부울 배열로 반환됩니다.

x = np.array([1,2,3])
y = np.array([1,3,-2])

print("x == y: ", x == y)
print("x > y:", x > y)
print("x>=1:", x>=1)

[실행 결과]

x == y:  [ True False False]
x > y: [False False  True]
x>=1: [ True  True  True]

 

브로드캐스팅

앞서 살펴본 np.arange(4)5를 더하는 예시를 다시 살펴보겠습니다.

x = np.arange(4)
print(x + 4)

[실행 결과]

[4 5 6 7]

크기가 (4, )x와 크기가 (1, )4가 문제없이 연산됨을 확인했습니다. 이번엔 다른 예시를 살펴보겠습니다.

a = np.array([[1, 2], [3, 4]])
b = np.array([-1, -2])
print(a + b)

[실행 결과]

[[0 0]
 [2 2]]

크기가 (2, 2)a와 크기가 (2, )인 배열도 연산이 이뤄졌습니다. 정확히는 ba의 각 행에 더해졌습니다. 이처럼 크기가 다른 배열 간 연산이 가능한 이유는 브로드캐스팅(broadcasting) 덕분입니다.

 

브로드캐스팅은 다른 크기의 배열에 유니버설 함수를 적용하는 규칙 집합으로, 큰 차원의 배열에 맞게 작은 배열이 확장되는 것을 말합니다. 앞서 살펴본 두 개 예시를 아래 그림을 통해 자세히 살펴보겠습니다

 

위 그림은 [1, 2, 3, 4][5]가 더해지는 과정입니다. 두 배열의 크기가 다른데, [5]가 네 번 복제되면 더할 수 있습니다. , 브로드캐스팅에 의해서 5[5, 5, 5, 5]로 변환되어 [1, 2, 3, 4]와 더해져 [6, 7, 8, 9]가 되었습니다. 아래 그림은 [[1, 2], [3, 4]][-1, -2]가 더해지는 과정입니다. 여기서는 [-1, -2]가 두 번 복제되어 연산이 이뤄졌습니다.

 

 

판다스에서의 배열 연산

판다스에서의 배열 연산은 넘파이와 매우 흡사하지만, 인덱스 중심의 연산이 이뤄진다는 점에서 차이가 있습니다.

간단한 예제 코드를 살펴보겠습니다. 먼저, 두 개의 시리즈 S1S2를 정의합니다.

import pandas as pd
S1 = pd.Series([1,2,3,4], index = [1,2,3,4])
S2 = pd.Series([4,3,2,1], index = [4,3,2,1])

두 시리즈는 배열순서만 다를 뿐, 인덱스가 i (i = 1, 2, 3, 4)면 데이터도 i라는 점에서 같습니다. 이제 S1에서 S2를 빼보겠습니다.

display(S1 - S2)

[실행 결과]

1    0
2    0
3    0
4    0
dtype: int64

실행 결과를 보면, 모든 인덱스의 데이터가 0입니다. 그 이유는 인덱스를 기준으로 계산이 이뤄졌기 때문입니다. 정확히는 아래 그림에 도식화된 바와 같이, 두 시리즈의 인덱스의 합집합을 순회하면서 두 시리즈의 차이를 계산합니다.

조금 더 극명한 상황을 보겠습니다. 데이터 크기는 일치하나, 인덱스가 전혀 다른 경우입니다.

S1 = pd.Series([1,2,3,4], index = [1,2,3,4])
S2 = pd.Series([4,3,2,1], index = [5,6,7,8])
result = S1 - S2
display(result)

[실행 결과]

1   NaN
2   NaN
3   NaN
4   NaN
5   NaN
6   NaN
7   NaN
8   NaN
dtype: float64

실행 결과를 보면, result의 인덱스가 [1, 2, 3, 4, 5, 6, 7, 8]이고 데이터는 모두 NaN입니다. 왜 이러한 결과가 나왔는지 알아보겠습니다. 먼저, S1S2의 인덱스 합집합이 [1, 2, 3, 4, 5, 6, 7, 8]이므로 result의 인덱스가 [1, 2, 3, 4, 5, 6, 7, 8]인 것입니다 또한, 지금 순회하는 인덱스가 1이면 S1에서는 인덱스가 1인 데이터 1을 찾지만, S2에서는 인덱스가 1인 데이터가 없어 NaN을 반환합니다. , result에서 인덱스 1인 데이터는 1 NaN이므로 NaN이 된 것입니다.

 

시리즈의 인덱스를 무시하고 같은 위치에 있는 요소끼리 연산하려면 시리즈의 데이터끼리 연산해야 합니다

display(S1.values - S2.values)

[실행 결과]

array([-3, -1,  1,  3], dtype=int64)

 

Comments