numpy

jwKim·2023년 7월 10일
0

1. numpy

1.1. numpy에 대하여

1.1.1. numpy 개발 배경

python은 표현할 수 있는 숫자에도 크기 제한이 있고, 속도가 매우 느린 편이기에 숫자 처리에는 적합하지 않은 프로그래밍 언어이다. 그러나 유연한 문법, mutable의 개념, 데이터 타입을 사용자가 직접 만들수 있다는 점 등 강점이 많았기 떄문에 python의 속도를 보완하여 숫자 연산을 하는 방향으로 numpy가 개발 되었다.

1.1.2. numpy의 기본 구조

numpy는 기본적으로 C로 만들어졌다. 우리 눈에는 array가 2차원, 3차원 등 n개의 차원이 있어보이지만, 실제로 내부적으로는 메모리에서 1차원으로 관리한다. 이 때 C의 pointer 개념을 그대로 들고왔는데, pointer의 위치를 옮겨가며 데이터를 뽑아낸다. 아래는 array에서 데이터를 조회할 때 pointer가 얼만큼 움직여야하는지를 나타내는 코드이다.

>>> c = np.arange(24).reshape(4, 6)
>>> c.strides
(24, 4)

pointer를 한 칸 옆으로 움직일 때에는 메모리에서 4 byte 옆으로 이동하고, 한 row를 넘어가기 위해서는 24 byte를 이동해야 한다는 뜻을 가지고 있다.

이 덕분에 numpy는 매우 빠른 연산속도를 가질 수 있게 되었지만, 데이터타입을 numpy 데이터타입으로 바꿔야 하므로 데이터 변환에 리소스가 들어간다. 따라서 작은 데이터셋에서는 데이터타입 변환 때문에 다른 라이브러리에서 느릴 수도 있다.

numpy 객체를 만들 때에는 내부에 __array__가 정의되어 있으면 numpy 객체로 사용이 가능하다. 많은 데이터타입에서 __array__를 보유하고 있지만, 대표적인 예시로 pandas의 Series 객체를 확인해보자.

>>> s = pd.Series([1, 2, 3])
>>> '__array__' in dir(s)
True

>>> np.sum(s)
6

s에는 __array__가 있기 때문에 np.sum() 안에서 사용 가능하다.(np.sum 은 인자로 array를 받는다고 설명에 나와있음)

1.1.3. ndarray의 데이터타입과 특징

numpy에서 사용하는 데이터타입은 ndarray이다. 이는 'n dimention array'를 뜻한다. 이 의미만 보더라도 numpy를 통해서 2차원, 3차원 등 다차원 데이터를 다룰 수 있음을 알 수 있다. 프로그래밍(array)과 수학적 개념(1차원, 2차원, ...)을 합쳐버렸다.

ndarray는 아래와 같은 특징을 갖는다.(container 구분 참고)

  • sequence : 요소에 순서가 있어서 indexing과 slicing이 가능하다.
  • homer : ndarray 안에는 하나의 데이터타입을 가진 데이터들이 있어야 한다.
  • mutable : 재할당하지 않아도 값을 변화시킬 수 있다.

또 ndarray가 가지는 중요한 특징은 shape이 같은 ndarray끼리는 같은 위치에 있는 요소만 계산할 수 있다.(=element wise) 이를 가능하게 해주는 것이 vectorization이다. 데이터를 vector로 표현해서 엘리먼트의 위치끼리 연산하므로 다른 라이브러리보다 빠른 처리가 가능하다.

1.2 numpy 사용하기

1.2.1. 기초 개념

numpy는 사용자들이 인스턴스화 방식이 너무 어려워서 조금 더 편하게 데이터를 정의할 수 있게 지원한다.(=팩토리 메소드) 그래서 데이터타입은 ndarray지만 array()를 통해서도 데이터를 선언할 수 있다.

a = np.ndarray([1, 2, 3]) # 원래는 이렇게 해야 함 -> 인스턴스화
b = np.ndarray([4, 5, 6]) # 이렇게 하는 것도 지원 -> 팩토리 메소드

ndarray는 repr과 str(print 했을 때의 결과가 다르다.

# repr
>>> a
array([1, 2, 3])

# str
>>> print(a)
[1 2 3]

array라는 타입이 뜨는 것이 더 명확하므로 repr 방식으로 사용하는 것이 더 낫다.

ndarray는 언제나 직사각형 형태로 만든다. 아래 예시를 보자.

# 일부러 원소 하나 빼기
>>> b = np.array([[1,2,3], [4, 5]])
>>> b
array([list([1, 2, 3]), list([4, 5])], dtype=object)

1.2.2. ndarray 기본 메소드

ndarray는 dtype과 shape이 가장 중요하다. dtype과 shape이 같아야 연산이 가능하기 때문이다.(coercion과 broadcast를 지원하긴 함)

>>> a = np.arange(3)
>>> a.dtype # 데이터타입 -> default는 int32임
dtype('int32')

>>> a.shape # 행렬 모양 확인
(3,)

>>> a.size # 원소 개수 확인
3

>>> a.ndim # 차원 확인
1

ndarray가 갖는 메소드는 수학적 계산을 위해 그 개수가 매우 많다. 그래서 모든 것을 다 기억할 수 없기 때문에 자체적으로 help 기능을 제공한다.

# 'sum'이란 단어가 들어간 메소드 검색하기
np.lookfor('sum')

# 함수를 설명 검색
np.info(np.cumsum)

1.2.3. ndarray를 만드는 여러 함수들

수학적 컨셉에 맞춰 ndarray를 만드는 함수를 여럿 지원한다. 그런데 우리는 데이터분석과 모델링이 우선이기 때문에 ndarray를 생성하는 함수는 간단하게만 알아보겠다. (나열하기만 할 테니 필요하면 사용해보기)

# 0행렬 만드는 방법
np.zeros((3, 4))

# 1로 채우기
np.ones((3, 4))


# 단위 행렬 만들기
np.eye(4)

# 하삼각행렬
np.tri(3)

# 원소가 0부터 시작하는 ndarray 만들기 & reshape으로 원하는 형태로 변경
np.arange(12 * 100).reshape(3,4,-1)

1.2.4. 연산

python에서 numpy가 갖는 영향은 매우 크기 때문에 numpy만 사용할 수 있는 특별한 연산자가 있다. 바로 @이다. @는 내적 연산(혹은 행렬곱)을 의미하는데, 연산하려는 두 ndarray의 행과 열의 개수를 맞춰줘야 한다.(=shape을 일치시켜줘야함)

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

# 내적 함수,   내적 메소드, 내적 연산자
np.dot(a, b), a.dot(b),   a@b
aa1 = np.array([[1, 2, 3]])
bb1 = np.array([[1, 2, 3]])

# 2차원의 @는 행렬곱을 뜻한다!!!! => 그런데 정사각형 행렬이 아니므로 에러가 난다
aa1@bb1
# @ 연산자를 사용할 때에는 행과 열의 개수를 맞춰줘야 한다!
aa2 = np.arange(4).reshape(2,2)
bb2 = np.arange(4).reshape(2,2)

aa2@bb2

1.2.5. broadcast

기본적으로 numpy는 원소의 위치를 가지고 연산하는 element wise 연산을 지원한다고 했다. 그런데 이 연산은 어떻게 되는걸까?

>>> a = np.arange(12).reshape(3,4)
>>> a+10
array([[10, 11, 12, 13],
       [14, 15, 16, 17],
       [18, 19, 20, 21]])

a는 행렬이고, 1은 정수니까 shape이 맞지 않아 계산이 불가해야되는 것 아닐까? 이 때 암묵적으로 shape을 변경시켜주는 기능인 broadcast가 작동한다. 1을 a의 shape에 맞춰 변경해주는 것이다. 풀어서 쓰면 아래와 같다.

# a+10을 풀어서 쓰기 -> a의 shape만큼 공간을 만들고 10으로 모두 채운 후 덧셈 연산
a + np.full(a.shape, 10)

broacast는 행이나 열의 길이가 일치하고, 다른 차원은 원소기 1일 때에도 작동하는데, 1이 있는 방향으로 원소들을 복사한다.

>>> c = np.arange(3).reshape(3, 1)
>>> cc = np.arange(6).reshape(3, 2)
>>> c + cc 
 array([[0, 1],
        [3, 4],
        [6, 7]]))

c는 행만 있다. 그런데 덧샘 연산을 하니 행을 복사해 그 옆으로 붙여 연산을 진행한다.(이건 이것 저것 실습 해보면서 이해하는 수 밖에 없다.)

# broadcast의 꽃은 이 문제! 이해하기

>>> x = np.array([[1, 2]])
>>> y = np.array([[1], [2]])
>>> x + y
array([[2, 3],
       [3, 4]])

1.2.6. indexing

ndarray는 sequence 데이터이기 때문에 indexing이 가능하다. nedarray에서 사용되는 indexing 기법으로는 총 5 + 1가지가 있다.(나머지 1가지는 indexing이라고 하기엔 조금 애매함) indexing을 알아보기 위해 아래와 같은 데이터를 사용하자.

>>> a = np.arange(24).reshape(4, 6)
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])

1.2.6.1. 방법 1 - comma indexing

인덱싱 차원의 구분을 콤마(,)로 구분한다.

>>> a[1, 3]
9

1.2.6.2. 방법 2 - tuple indexing

tuple로 indexing 조건을 제시한다. 제시하는 기준은 [(elem1의 1차원 위치, elem2의 1차원 위치), (elem1의 2차원 위치, elem2의 2차원 위치)] 이다.

>>> a[(2, 1), (3, 5)]
array([15, 17])

위의 예시에서는 결국 (2,3)위치의 원소와 (1,5) 위치의 원소를 찾으라는 말이다.

1.2.6.2. 방법 3 - fency indexing

원래 indexing을 하면 차원이 낮아진다. 그런데 이 방법은 차원을 그대로 유지할 수 있는 장점이 있다. 대괄호를 두 번 써주면 된다. 이 방법을 사용하는 이유는 내가 원하는 순서대로 데이터를 뽑을 수 있기 때문이다.

# 1행, 2행을 뽑기
>>> a[[1,2]]
array([[ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17]])

# 2행, 1행 순으로
>>> a[[2, 1]] 
array([[12, 13, 14, 15, 16, 17],
       [ 6,  7,  8,  9, 10, 11]])

1.2.6.2. 방법 4 - boolean indexing

boolean indexing은 조건을 제시한 후 조건에 True인 결과만 반환한다.

>>> a[a > 5] # -> broadcast 기법 사용됨
array([ 6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])

1.2.6.2. 방법 5 - ... indexing

원래 indexing을 할 때 2차원 데이터는 2차원으로 뽑혀야한다. 그래서 아래 결과는 1차원만 indexing 조건으로 제시해도 2차원으로 데이터를 뽑은 것이다. 따라서 그 뒤에 [:]가 숨겨져있다는 것을 알 수 있다.

>>> a[0]
array([0, 1, 2, 3, 4, 5])

>>> a[0][:] # 뒤에 [:]가 숨겨져있다!
array([0, 1, 2, 3, 4, 5])

# 이 세개가 다 같다!!!!! 모든 열을 지칭하는 [:]는 생략이 된다!
>>> a[0], a[0][:], a[0, :]
(array([0, 1, 2, 3, 4, 5]),
 array([0, 1, 2, 3, 4, 5]),
 array([0, 1, 2, 3, 4, 5]))

이게 3차원으로 가면 좀 더 복잡해보일 수 있다.

>>> b = np.arange(60).reshape(3, 4, 5)
>>> b[0], b[0][:][:]
(array([[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14],
        [15, 16, 17, 18, 19]]),
 array([[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14],
        [15, 16, 17, 18, 19]]))

그런데 [:]가 두 번 이상 반복되는 것은 ...으로 축약할 수 있다!

>>> b[0, ...]
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])

>>> b[..., 1]
array([[ 1,  6, 11, 16],
       [21, 26, 31, 36],
       [41, 46, 51, 56]])

1.2.6.2. 방법 +1 - None과 np.newaxis

indexing과 비슷하게 사용되지만, indexing은 아닌 기법이 있다. 데이터를 조회할 때 축을 한 개 늘리는 것인데, None을 사용한다.

>>> a[None]
array([[[ 0,  1,  2,  3,  4,  5],
        [ 6,  7,  8,  9, 10, 11],
        [12, 13, 14, 15, 16, 17],
        [18, 19, 20, 21, 22, 23]]])
        
# None은 축을 하나 추가한다는 뜻! 뒤에 :이 생략되어있다.
a.shape, a[None].shape, a[None, :, :].shape
((4, 6), (1, 4, 6), (1, 4, 6))

그런데 None이라고 쓰니 사용자들이 와닿지 않는다. 그래서 numpy에서는 np.newaxis라는 함수를 만들어 명시적으로 새로운 축을 추가하는 방법을 제시했다.

>>> np.newaxis == None
True

따라서 None자리에 np.newaxis을 사용해보자.

0개의 댓글

관련 채용 정보