NumPy Boolean Indexing과 Fancy Indexing

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Boolean Indexing?

배열에 조건을 걸어 True/False로 이루어진 배열을 반환

a = np.array([15, 0, 25, 8, 0, 40, 3, 12, 50, 0])

low_a = a < 10
low_a  # > array([False,  True, False,  True,  True, False,  True, False, False, True])

이때 다양한 연산도 가능하다.

a = np.array([15, 0, 25, 8, 0, 40, 3, 12, 50, 0])

(a >= 10 )&(a < 30)  #  > array([ True, False,  True, False, False, False, False,  True, False, False])

(stock >= 10 )|(stock < 30)  # > array([ True,  True,  True,  True,  True,  True,  True,  True,  True, True])

Fancy Indexing

인덱스가 담긴 배열을 사용해 원하는 위치의 원소들을 순서에 상관없이 추출

기본적으로 넘파이는 뷰 복사를 지원하지만 팬시인덱싱을 하는 경우에는 복사본을 생성하는 것이 특징

a = np.array([580, 220, 180, 150, 130, 160, 110, 450])
a_index = [0, 1, 7]

a_fancy = a[a_index]  # > array([580, 220, 450])

where

추가로 공부하다가 where 함수랑 boolean indexing 개념이 살짝 헷갈려서 정리해본다.

where은 조건에 부합하는 인덱스를 반환해주는 함수이다. 예시를 들자면

a = np.arange(5,15)
# array([5,6,7,8,9,10,11,12,13,14])

np.where(a > 10) 
# 11~14까지의 인덱스를 반환
# array([6,7,8,9]) > 인덱스를 반환해준거임!!

boolean indexing은 말그대로 boolean 값을 반환해주는 함수이다. 내가 어떤 조건을 걸면 그 조건에 만족하는 지 안하는 지에 대한 boolean 값을 반환해주는 것!

where은 조건에 부합하는 결과의 인덱스 값 반환!!

NumPy 브로드캐스팅 규칙

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


브로드캐스팅

일반적으로 NumPy는 모양이 다른 배열끼리의 연산은 불가능하다.

a = np.array([1,2,3])
b = np.array([1,2])
a + b
ValueError Traceback (most recent call last) in <module>()----> 1 a + bValueError: operands could not be broadcast together with shapes (3,) (2,)

에러에서도 보면 브로드캐스트가 되지 못했다는 것을 보여주고 있다. 그렇다는 것은 어떤 특정 조건이 맞아진다면 모양이 다른 배열끼리도 연산을 수행할 수 있다는 것을 의미한다.

  1. 차원 수가 다른 경우: 작은 차원의 배열 앞쪽에 크기 1인 차원을 추가
  2. 각 차원을 뒤에서부터 비교: 각 차원의 크기가 같거나, 둘 중 하나가 1이어야 함
  3. 호환되지 않으면: 브로드캐스팅 불가능

코드로 보면 아래와 같다.

# 1차원 배열과 2차원 배열 
a = np.array([1, 2, 3])
b = np.array([[10, 120, 120], [80, 120, 100], [60, 90, 80]])

print(a*b)
# [[ 10 240 360]
#  [ 80 240 300]
#  [ 60 180 240]]

(3,3) 배열과 (3,)끼리 곱하려니 (3,) 행렬이 (1,3)행렬로 확장된 후, 다시 (3,3)로 확장 및 복사 되어 계산된 것을 볼 수 있다.

이처럼 브로드캐스팅을 통해 크기가 다른 배열들도 연산이 가능해진다. NumPy가 자동으로 작은 배열을 큰 배열의 형태에 맞춰 확장해주는 개념인 것이다!

# 스칼라와 배열
a = np.array([1,2,3,4])
b = 10

a + b  # > [11,12,13,14]

# 서로 다른 차원
a = np.array([[1],
              [2],
              [3]])  # (3,1)
b = np.array([10,20])  # (2.)

a + b  # (3,2)
# >  [[11, 21],
#     [12, 22],
#     [13, 23]]

브로드캐스팅이 일어나는 과정

위 코드 예시에서처럼 서로 다른 차원의 배열이 더해진다고 생각해보자. > (3,1)배열과 (2,)배열

  1. (2,)배열이 (1,2)배열로 차원이 추가된다
  2. (3,1)배열과 (1,2) 배열을 비교해 브로드캐스팅이 됨을 확인
  3. 첫번째 차원: (3,1) 베열 > (3,3)으로 확장
  4. 두번쨰 차원: (1,2) 배열 > (2,2)으로 확장
  5. 최종: (3,2) 형태로 확장

브로드캐스팅의 장점

  • 메모리 효율성: 실제 배열을 복사하지 않고 가상으로 확장하기에
  • 코드 간결성: 반복문 없이 배열 전체의 연산이 가능
  • 성능: C로 구현된 최적화된 연산을 활용

NumPy View와 Copy

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


View 와 Copy

  • View: 원본 배열의 메모리를 공유하는 새로운 배열로 뷰를 수정하면 원본도 함께 변경된다.
  • Copy: 원본 배열과 완전히 독립된 새로운 배열로 복사본을 수정해도 원본은 변경되지 않는다.
# view 경우 > 일반적으로 copy를 해주지 않으면 모든 기본 복사는 다 view 복사다.
origin = np.arange(5)  # [0,1,2,3,4]
view_arr = origin  # [0,1,2,3,4]

view_arr[0] = 99  # [99,1,2,3,4]
origin  # [99,1,2,3,4]

# copy 경우 
origin = np.arange(5)  # [0,1,2,3,4]
view_arr = origin.copy()  # [0,1,2,3,4]

view_arr[0] = 99  # [99,1,2,3,4]
origin  # [0,1,2,3,4]

np.array vs np.asarray ?

그럼 우리가 Numpy를 통해 배열을 만들때 사용한 np.array는 어떤 개념일까?

  • np.arraay(): 항상 새로운 배열을 복사하여 만든다
  • np.asarray(): 이미 numpy 배열이라면 복사하지 않고 그대로 사용해 메모리 효율을 높인다.

즉, 둘은 동일한 기능을 하는 함수임은 맞지만

np.array는 copy=True가 기본값이고, asarray는 copy=False가 기본값인 것이다.

a = np.ones([2,3])
a_array = np.array(a)
a_asarray = np.asarray(a)

a[0] = 0

a_array
# array([[1., 1., 1.],
#        [1., 1., 1.]])

a_asarray
# array([[0., 0., 0.],
#        [1., 1., 1.]])

NumPy 행렬과 축에 대해

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


행렬?

수, 문자, 함수등을 네모꼴 괄호 안에 배치하여 놓은 것

Axis?

2차원 배열 (행렬) 기준:

  • axis=0: 의 방향 (아래쪽으로). 각 열(column)에 대해 연산합니다.
  • axis=1: 의 방향 (오른쪽으로). 각 행(row)에 대해 연산합니다.

NumPy 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


NumPy

NumPy는 Python에서 과학 연산을 위해 사용하는 가장 기본적인 패키지 중 하나라고 한다. 다차원 배열과 행렬 연산에 필요한 다양한 함수와 메소드를 제공한다.

NumPy 설지

pip install NumPy

# 다 설치되고나면 
import numpy as np  # import 해주고
np.__version__  # 버전 확인

NumPy 배열 생성

NumPy에서 가장 기본적인 데이터 구조는 배열이다. NumPy 배열은 동일한 타입의 데이터를 담는 다차원 배열로 ndarray 클래스를 사용해 생성할 수 있다.

import numpy as np

# 1차원 배열 생성
a = np.array([1,2,3])
# 2차원 배열 생성
b = np.array([[1,2,3], [4,5,6]])
# 3차원 배열 생성
c = np.array([[[1,2], [3,4]], [[1,2], [3,4]]])

ndim

배열의 차원(Dimension)을 알기 위한 함수

a.ndim  # > 결과값: 1
b.ndim  # > 결과값: 2
c.ndim  # > 결과값: 3

shape

배열의 크기(Shape)를 확인

a.shape  # 결과값: (3,)
b.shape  # 결과값: (2,3)
c.shape  # 결과값: (2,2,2)

size

배열에 있는 총 원소의 개수

a = np.array([1,2,3])
a.size  # > 3

배열 연산

NumPy 배열은 다른 배열 또는 스칼라와의 연산을 지원한다.

스칼라: 스칼라의 어원은 라틴어 scala에서 왔다고 하며 이는 “저울” 이란 뜻이라고 한다. 즉, 어떤 양(크기)을 나타내는 개념인 것이다. 선형대수학에서의 스칼라의 정의 또한 “벡터 공간에서 벡터를 곱할 수 있는 양”이다. 쉽게 생각해서 실수가 선형대수학에선 스칼라인 것이다.

스칼라에 대해 찾아봤는데, 이 말이 그나마 제일 이해하기 쉬웠던 것 같다.(문과생 웁니다..)

결국 스칼라는 실수라는건데.. 실수는 유리수와 무리수 모든것을 말하는거니.. 이쯤..이해하고..일단 패스..

a = np.array([1,2,3])
b = np.array([4,5,6])

# 덧셋
c = a + b  # > [5,7,9]
# 곱셈
d = a * b  # > [4,10,18]
# 스칼라와 연산 
e = a + 1  # > [2,3,4]

추가로 NumPy 배열 연산 안에는 sum, mean, min, max 등로 지원해준다.

a = np.array([1,2,3])

# 합
b = np.sum(a)  # > 6
# 평균
c = np.mean(a)  # > 2.0
# 최소값
d = np.min(a)  # > 1
# 최대값
e = np.max(a)  # > 3

### 배열 인덱싱과 슬라이싱

 a = np.array([1,2,3,4,5])

# 인뎅싱
 b = a[0]  # > 1
 c = a[3]  # > 4

# 슬라이싱 
 d = a[1:4]  # > [2,3,4]
 e = a[:3]  # > [1,2,3]
 f = a[1:]  # > [2,3,4,5]

NumPy 에서는 다차원 배열에서의 인덱싱, 슬라이싱 또한 가능하다.

a = np.array([[1,2,3], [4,5,6]])
# 위 배열의 출력값은 아래처럼 나온다
# array([[1,2,3]
#       [4,5,6]])

# 인덱싱
b = a[0, 0]  # 1
c = a[1, 2]  # 6

# 슬라이싱
d = a[0, 1:3]  # [2, 3]
e = a[:, 1]    # [2, 5]
f = a[:, :2]   # [[1, 2], [4, 5]]

(다차원 배열에서의 슬라이싱이 좀 어렵다.. 이해는 간다만..ㅎ 버퍼링이 좀 생김ㅎㅋ)

arange (start, stop, step)

정해진 step만큼 일정하게 떨어진 숫자를 배열로 반환

start의 기본값은 0, step의 기본값은 1로 직접 지정해주지 않으면 기본값을 행한다. stop 값은 반드시 전달되어야 한다!

np.arange[3]  # > [0,1,2]
np.arange[1,10,2]  # > [1,3,5,7,9]

dot

배열간의 행렬 곱셈을 배열로 반환

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

np.dot(a,b)
# array[[19 22]
#       [43 50]]

이때 궁금증은 *연산자와의 차이는 무엇일까?

  • dot은 배열간의 행렬 곱셈을 한다면 *은 요소별 곱셈을 한다.

위에서 다룬 dot의 경우 행해진 곱셈 연산은 아래와 같다.

# [[1*5 + 2*7, 1*6 + 2*8]
#  [3*5 + 4*7, 3*6+ 4*8]]

그치만 *은 아래와 같다

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

a * b
# array[[ 5 12]
#       [21 32]]

reshape

NumPy 배열의 형태를 변경

reshape을 사용하기 위해서는 기존 배열과 변환 후 배열의 원소개수가 동일해야한다. 즉 (2,4)와 (4,2)의 reshape은 가능하다. reshape 메서드를 사용하는 주 목적이 차원을 유지한 상태에서 배열의 형태를 변환하는 것이라고 한다.

# a라는 np array 생성 > 2차원 배열 
a = np.array([[1, 2], [3, 4], [5, 6]])
# array([[1, 2],
#        [3, 4],
#        [5, 6]])
a.shape  # > (3,2) 
a.ndim  # > 2

b = a.reshape(2,3)
# array([[1, 2, 3],
#        [4, 5, 6]])
b.shape  # > (2,3)
b.ndim  # > 2

tranpose

NumPy 배열의 차원을 변경하여 전치된 배열을 반환

전치: 행렬을 대각선으로 뒤집는 연산자..(뭔..)로 행렬의 행과 열 인덱스를 바꾸어 다른 행렬을 생성하는 것을 의미한다.

설명이 어려우니.. 코드로 확인해보자!

original_array = np.array([[1, 2, 3], [4, 5, 6], 7, 8, 9]])
# array([[1, 2, 3],
#        [4, 5, 6],
#        [7, 8, 9]])

transposed_array = np.transpose(original_array)
# array([[1, 4, 7],
#        [2, 5, 8],
#        [3, 6, 9]])

직접 찍어보니 알 것 같다! 행과 열을 바꾼다라는 개념..!

중요한 포인트는 tranpose 또한 reshape과 같이 배열 요소 개수가 같아야하는 것은 동일하다! 즉 2x3 배열을 전치해서 3x2 배열을 반환하는 것!

zeros, ones

  1. zeros: 모든 원소가 0인 배열을 생성
  2. ones: 모든 원소가 1인 배열을 생성
a = np.zeros((2,3))
# array[[0. 0. 0.]
#       [0. 0. 0]]

b = np.ones((2,3))
# array[[1. 1. 1.]
#       [1. 1. 1.]]

where

where은 조건에 부합하는 인덱스를 반환해주는 함수

예시를 들자면

a = np.arange(5,15)
# array([5,6,7,8,9,10,11,12,13,14])

np.where(a > 10) 
# 11~14까지의 인덱스를 반환
# array([6,7,8,9]) > 인덱스를 반환해준거임!!

linspace

지정해준 범위 내에서 균등한 간격으로 원하는 개수의 배열을 생성

a = np.linspace(0,1,5)  # [0.  0.25 0.5  0.75 1.  ]

random.random

0과 1사이의 균등분포에서 난수를 생성해 배열을 만듦. 함수의 인수로 생성할 배열의 크기를 지정 가능.

a = np.random.random(3,3)
#array([[0.5254058 , 0.39879012, 0.61469921],
#       [0.72924439, 0.10349449, 0.84673281],
#       [0.61774079, 0.71552584, 0.50629848]])

random.randn

평균이 0이고 표준편차가 1인 정규분포를 따르는 난수를 생성해 배열을 만듦. 함수의 인수로 생성할 배열의 크기를 지정 가능.

a = np.random.randn(3,3)
# array([[-1.90644415,  1.58210398, -2.03024185],
#        [ 0.07563648,  0.10644284, -0.73066284],
#        [ 0.07573001,  0.19376196, -1.18768222]])

정규분포, 표준편차.. 다 까먹었다. 나중에 따로 공부해서 정리해야겠다.

Numpy 난수 함수

난수: 정의된 범위 내에서 무작위로 추출된 수

  • seed() : 난수 발생기의 seed를 지정
  • shuffle() : 리스트나 배열의 순서를 뒤섞음
  • choice(): 배열에서 무작위로 추출
  • rand() : 균등분포에서 표본을 추출
  • randint() : 주어진 최소/최대 범위 안에서 임의의 난수를 추출
  • randn() : 표준편차가 1이고 평균값이 0인 정규분포에서 표본을 추출
  • normal() : 정규분포(가우시안)에서 표본을 추출

randn 과 normal?

두 함수 모두 정규분포에서 난수를 생성한다는 공통점이 있지만 약간의 차이점도 존재한다.

  1. randn(): 표준편차가 1이고 평균이 0인 표준정규분호에서 난수 생성
  2. normal(): 평균값과 표준편차를 직접 지정할 수 있음

따라서 randm() 함수는 normal(0,1)과 동일한 역할을 한다!

주요 유니버설 함수

  • sqrt(): 제곱근
  • log(): 자연로그
  • greater_equal(): 비교
  • logical_and(): 논리