일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- pandas
- 주가데이터
- 주요 파라미터
- 데이터분석
- 랜덤포레스트
- 데이터사이언티스트
- 베이지안 최적화
- 데이터사이언스
- 데이터사이언스학과
- 머신러닝
- 퀀트 투자 책
- 대학원
- 사이킷런
- 주식데이터
- 파이썬
- 경력 기술서
- 퀀트
- 코딩테스트
- sklearn
- 커리어전환
- 파라미터 튜닝
- AutoML
- 경력기술서 첨삭
- 데이터 사이언티스트
- 자기소개서
- 판다스
- 이력서 첨삭
- 데이터 사이언스
- 하이퍼 파라미터
- 하이퍼 파라미터 튜닝
- Today
- Total
GIL's LAB
Numpy와 Pandas에서의 배열 연산 본문
넘파이의 가장 큰 장점으로 매우 빠른 배열 간 연산을 꼽을 수 있습니다. 배열 간 연산이란 크기가 같은 두 배열에 대해, 같은 위치에 있는 요소끼리의 수행하는 연산을 의미합니다. 예를 들어, 크기가 n인 두 배열 x = (x1, x2, ..., xn)와 y = (y1, y2, ..., yn)에 대해 임의의 연산자 ◇를 사용한 배열 연산은 다음과 같이 정의됩니다.
x ◇ y = (x1 ◇ y1, x2 ◇ y2, ..., xn ◇ yn)
넘파이는 C로 작성되었기에, 파이썬의 반복문을 사용하는 것보다 훨씬 빠른 속도의 배열 연산을 자랑합니다. 이 포스팅에서는 넘파이의 배열 연산에 대해 알아보겠습니다.
유니버설 함수
유니버설 함수(universal functions, ufuncs)는 +, -, *, /, ** 등 파이썬에서 두 숫자형 변수에 대해 정의된 연산자를 ndarray에도 정의한 것이라 할 수 있습니다. 다시 말해, 유니버설 함수는 벡터 및 행렬 간 연산을 수행하는 함수입니다. 유니버설 함수는 반복문보다 배열 간 연산을 훨씬 빠르고 쉽게 구현할 수 있다는 장점이 있습니다.
반복문과의 속도 비교
유니버설 함수가 반복문에 비해 얼마나 빠른지 간단한 실험을 통해 알아보겠습니다. 먼저, x1과 x2에 백만 개의 난수로 구성된 ndarray를 정의해줍니다.
import numpy as np
x1 = np.random.random(10 ** 6) # 크기 100만의 난수 배열 생성
x2 = np.random.random(10 ** 6)
다음으로 유니버설 함수와 반복문을 이용하여 x1과 x2의 같은 위치에 있는 요소끼리 더하는 데 걸리는 시간을 각각 측정하겠습니다. 유니버설 함수를 사용했을 때 소요된 시간을 측정해보니, 아래와 같이 평균 5.63ms임을 확인했습니다.
%%timeit
s = x1 + x2
[실행 결과]
5.63 ms ± 440 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
이번에는 for문을 이용했을 때의 시간을 계산해보겠습니다. s를 빈 리스트로 만든 뒤, x1과 x2를 각각 e1과 e2로 순회하면서 e1과 e2의 합을 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, )인 배열도 연산이 이뤄졌습니다. 정확히는 b가 a의 각 행에 더해졌습니다. 이처럼 크기가 다른 배열 간 연산이 가능한 이유는 브로드캐스팅(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]가 두 번 복제되어 연산이 이뤄졌습니다.
판다스에서의 배열 연산
판다스에서의 배열 연산은 넘파이와 매우 흡사하지만, 인덱스 중심의 연산이 이뤄진다는 점에서 차이가 있습니다.
간단한 예제 코드를 살펴보겠습니다. 먼저, 두 개의 시리즈 S1과 S2를 정의합니다.
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입니다. 왜 이러한 결과가 나왔는지 알아보겠습니다. 먼저, S1과 S2의 인덱스 합집합이 [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)
'파이썬 > 데이터 분석을 위한 파이썬' 카테고리의 다른 글
tweepy를 이용한 트윗 데이터 수집 방법 (2) | 2022.08.28 |
---|---|
스트링 접근자: str accessor (0) | 2022.08.22 |
배열에서 k번째로 값이 큰(작은) 값 찾기 및 순위 계산 방법 (0) | 2022.08.21 |
Matplotlib을 이용한 그래프 업데이트 방법 (0) | 2021.12.29 |
Pandas의 Apply 메서드 (1) | 2021.10.05 |