파이썬 넘파이 Numpy array 시퀀스, 난수 생성과 dtype, 차원변경

파이썬 넘파이(numpy) 라이브러리를 사용할 때 가장 기초인 array를 생성하는 방법, 그리고 데이터 타입과 ndarray 차원 변경에 대해 공부해본다.

넘파이(numpy)는 파이썬에서 사용할 수 있게 만들어 놓은 일종의 패키지이다. 애드온 같은 개념이랄까?

파이썬 모듈은 여러 함수들을 ~~.py 파일에 넣어놓고 필요시 불러와서 미리 작성한 함수를 사용할 수 있게 해놓은 것이고, (파일명이 모듈명) 패키지는 여러 모듈들을 디렉토리 형태로 구조화한 것이다. 모듈 종합선물세트 개념 (디렉토리명이 패키지명)

넘파이(numpy)의 특징

  • 파이썬에서 선형대수 (Linear Algebra) 연산을 지원하도록 제작된 라이브러리
  • 다차원 배열 자료구조 클래스인 ndarray를 지원한다.
  • 퓨리에 변환, 난수발생 같은 여러 수학적 기능을 사용 가능
  • C언어로 구현된 내부 반복문을 사용하여 파이썬 반복문보다 연산이 빠름

array 란?

넘파이(numpy)에서 데이터 처리시 기본이 되는 형태를 array라고 한다. 한국말로 하면 배열이다. 행렬(matrix)과 비슷하지만 엄밀히 말하면 다른 특징이 있는데 여기서는 넘어간다.

어레이의 특징

🔺 array 라는 것이 numpy 에서 사용하는 자료형태 라고만 알아두자. 위와 같이 1D (1차원) array , 2D (2차원) array , 3D (3차원) array 등 다차원으로 생성할 수가 있다.

리스트에 대해 학습할 때 리스트 안에 원소로 또 리스트를 넣으면서 2차원 구조를 만들고 행렬처럼 활용하는 것에 대해 알아보았다.

어레이도 형태를 보면 리스트와 별반 차이가 없어 보인다. 심지어 괄호도 [ ] 를 사용해서 똑같다. 앞에 array 라고 붙는 점만 다르고 생긴것만 봐서는 리스트와 같은데 어떤 차이점이 있는걸까.

실제로 numpy array를 생성해보면서 리스트와의 차이점을 살펴보자.

array 생성방법

일단 array를 생성하려면 파이썬에서 numpy를 불러와야 한다.

numpy 호출방법

🔺 이렇게 맨 처음에 numpy를 불러와놓아야 이후에 다른 함수들의 사용이 가능하다. as np라고 지정했기 때문에 이후부터는 np 라고만 쓰면 numpy를 지칭하는 말이 된다.

a = np.array([10,20,30,40])
a

array([10, 20, 30, 40])   # 결과

🔺 간단하게 1차원 어레이를 생성해보았다. np.함수명 형태로 사용하고 어레이는 ([]) 괄호안에 데이터를 생성해 준다. 어레이로 생성한 변수를 확인해보면 array([데이터]) 형태로 되어있다.

b = np.array([[10,20,30], [40,50,60]])
b

array([[10, 20, 30],
       [40, 50, 60]])   # 결과

🔺 리스트 두개를 넣는 형태로 입력하면 2차원 어레이가 만들어진다.

이렇게 만들어진 array는 몇 가지 속성을 가지고 있고 다음과 같은 함수들을 이용하여 확인할 수 있다.

array 속성확인

b.ndim      # 차원 수 확인
2            # 결과

b.shape        # vertical, horizontal 별로 차원수 확인
(2, 3)        # 결과

🔺 shape가 중요한데, 쉽게말해 행렬에서 몇행 몇열로 이루어졌는지를 나타낸다고 보면 된다. 위에서 b를 2행 3열의 6개 데이터로 생성하였기 때문에 결과가 (2, 3) 으로 나온다. 이 결과값은 튜플 형태로 반환된다.

ndim은 shape의 앞에값만 나오는 셈이기 때문에 사실상 별 의미가 없다. 

b.size        # 전체 데이터 개수
6            # 결과

🔺 size라고 하면 어레이 내에 전체 데이터의 개수가 표시된다.

b.dtype            # 데이터 타입
dtype('int64')    # 결과

b.itemsize        # 데이터 타입 바이트단위
8                # 결과

🔺 dtype은 어레이 내의 데이터 형태를 알려준다. int64라는 것은 64비트 정수형이라는 뜻이다. 이 경우 2^63 까지 데이터를 저장할 수 있으며 범위를 벗어나면 엉뚱한 값으로 바뀌어버린다. 64비트 운영체제를 쓰고 있다면 보통 이런 자료형으로 생성된다.

itemsize는 바이트 단위로 보는거라 비트단위를 8로 나눈 것이라 큰 의미가 없다.

array 속성에서는 shape , dtype 두가지만 잘 사용하면 충분하다.

array 초기값 설정

어레이를 생성하는 다른 방법들을 알아본다. 어레이를 만들면서 안에 내용을 비워놓을 수도 있고, 특정 값으로 초기화할 수도 있다.

a = np.empty((2,3))
a

array([[4.6584765e-310, 0.0000000e+000, 0.0000000e+000],
       [0.0000000e+000, 0.0000000e+000, 0.0000000e+000]])

🔺 이렇게 empty로 2행3열짜리 빈 어레이를 생성하였다. 그런데 첫번째 자리에 뭔가가 남아있는 것을 볼 수 있다.

이것은 어레이를 사용할 주소는 할당했지만 그곳에 남아있던 데이터를 청소하지 않은 상태로 있는 것이다. 식당에 가서 몇명이요 하고 자리에 앉았는데 앞사람이 먹던 그릇이 아직 치워지지 않은 상황과도 같다.

# 예시1

np.zeros((2,3))

array([[0., 0., 0.],
       [0., 0., 0.]])

# 예시2

np.zeros((2,3), dtype='int64')

array([[0, 0, 0],
       [0, 0, 0]])

🔺 이게 마음에 안든다면 zeros를 이용하면 아예 모든 원소가 0으로 세팅된 어레이를 만들어 준다.

tuple 형태의 shape로 크기만 지정해주니 각 원소가 0. 으로 만들어졌는데 예시2 에서는 dtype으로 정수형을 지정해 주었더니 0 이라고 소수점 없이 만들어졌다.

np.ones((2,3))

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

🔺 ones를 이용하면 각 원소값을 초기상태 1인 어레이를 생성할 수 있다.

한가지 주의할 점은 이런 형태로 어레이를 생성할 때 shape 부분을 저렇게 (2,3) 과 같이 괄호로 감싸주어서 튜플로 넣어주어야 한다. 그냥 2,3 이렇게 하면 뒤에 3을 dtype으로 인식해서 다른 결과가 나올 수 있다.

a = np.full((2,3), fill_value=5)
a

array([[5, 5, 5],
       [5, 5, 5]])

🔺 full 함수를 이용하면 아예 지정한 값으로 채워서 초기값을 만들어 줄 수 있다.

시퀀스로 생성

파이썬에서 range 함수로 특정 구간의 숫자를 쭉 불러오듯이 array도 시퀀스 값으로 생성할 수 있다. 

arange 함수를 사용하는데 arrrange 어레인지가 아님에 주의하자. 어레이라는 뜻의 a를 앞에 붙여서 에이 레인지 이다.

np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])    # 결과

np.array(range(10))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])       # 결과

🔺 arange로 0~9 까지 생성하라고 한 것과, array 안에 range를 써서 0에서 9까지를 준 것과 같은 결과이다.

np.arange(1.5,21.3,2.3)    # (start , stop , step)

array([ 1.5,  3.8,  6.1,  8.4, 10.7, 13. , 15.3, 17.6, 19.9])   # 결과

🔺 range와 마찬가지로 시작과 끝, 그리고 간격을 얼마씩 띄울지 지정해줄 수 있는데,

range와 다른점은 이렇게 나눠 떨어지지 않는 실수형 수치로 step을 지정할 수 있다는 점이다.

np.array(range(1.5,21.3,2.3))

🔺 이렇게 바꿔쓰면 ‘float’ object cannot be interpreted as an integer 에러 문구가 출력된다.

np.linspace(0,9,10)    # (start, stop, num)
array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])    # 결과

🔺 얼마씩 띄워줄지 step을 정하는게 arange라면, linspace를 사용하면 총 몇 구간으로 나눌지 지정할 수 있다. 예시는 0에서 9까지 총 10개로 나누라는 의미이다. 얼마씩 띄워야 할지 모르겠는데 데이터 몇개가 필요한 상황에서 이걸 쓰면 적합하다.

난수로 생성

numpy에서는 random 함수를 이용하여 난수를 생성할 수 있다. 가만, 원래 파이썬에서도 import random 으로 모듈을 불러와서 랜덤수 생성하는 기능이 있었는데? 파이썬 random 모듈의 경우 난수 한개만 뽑아준다.

그에반해 넘파이 랜덤에서는 여러개의 난수를 추출한다던지, 무작위로 뽑거나 정규분포에 따라 뽑거나 등 여러 부가 기능들이 있다.

np.random.random()
0.6118145490624346

🔺 기본적으로 이렇게 난수를 뽑는다. 아무 지정을 하지 않았다면 0에서 1 사이의 한 개를 균등분포로 골라준다.

앞서 어레이에서는 튜플 형태로 shape을 지정한다고 하였는데, 그걸 통해 난수도 원하는 개수만큼 뽑으라고 할 수 있다.

np.random.random((3,4))
array([[0.20249413, 0.88356173, 0.16374184, 0.63512163],
       [0.7679576 , 0.11491201, 0.63242971, 0.18718458],
       [0.93986475, 0.43323222, 0.28706937, 0.35655228]])


np.random.rand(3,4)
array([[0.00608712, 0.21273306, 0.85735276, 0.04692226],
       [0.15912471, 0.23076848, 0.02870694, 0.23559088],
       [0.25579195, 0.416907  , 0.97596287, 0.21847092]])

🔺 shape (3,4) 로 3행 4열짜리 어레이를 생성하였다. 이 때 random.random 안에 넣으려면 shape을 튜플 괄호로 묶어주어야 하고, random.rand 으로 생성할 때에는 위와같이 따로 묶지 않고 써도 된다.

import matplotlib.pyplot as plt
_ =plt.hist(np.random.rand(100000), bins=100)
난수 생성 결과

🔺 위는 0~1 사이를 10만번 추출했을 경우에 가로축을 100으로 분할하여 각 구간별 뽑힌 횟수를 나타낸것이다. 반복 횟수를 증가시킬수록 그래프가 평탄해지는 것을 볼 수 있다.

이렇게 matplotlib를 불러와서 그래프로 도식화 할 수 있는데, 이를 통해 어떤 식으로 랜덤수가 뽑히는지 직관적으로 파악할 수 있다.

_ =plt.hist(np.random.randn(100000), bins=100)

🔺 random.randn 함수를 사용하면 평균=0, 표준편차=1인 표준 정규분포를 따르는 확률분포에서 무작위 수를 추출한다. 10만번을 뽑았을 때의 결과는 위와 같다.

지금 이렇게 그래프를 왜 하나하나 그려보고 있냐면, np.random.randn((3,4)) 와 같이 3행4열 shape로 랜덤수를 뽑아서 어레이를 만들었는데, 이것이 과연 어떠한 확률을 가지는 뽑기에서 나온 수인지 알고 써야하기 때문이다.

np.random.normal(loc=10, scale=5, size=(20))  # 정규분포 지정후 20개 추출
array([ 1.17064331, 14.17190513,  4.15605158,  6.45745833,  9.32037375,
       16.74334114,  7.90703334,  6.08674039,  9.72739287,  9.84093829,
        4.25715153, 10.08984624,  9.00100931, 20.12174194,  3.08327032,
        7.42069585,  4.91183549,  2.7277655 ,  0.77234553, 13.72264388])


_ =plt.hist(np.random.normal(loc=10, scale=5, size=(100000)), bins=100) # 10만번 그래프그리기

🔺 random.normal은 평균과 표준편차를 지정해서 정규분포를 만들고 그 안에서 추출하도록 한다.

  • loc = 평균
  • scale = 표준편차
  • size = 횟수
np.random.randint(1,10,5)   # (start, end, size)
array([3, 1, 1, 6, 8])

🔺 random.randint는 범위 내에서 size만큼 무작위로 정수를 추출한다. 이 때도 역시 끝 숫자는 포함하지 않는다.

np.random.seed(0)
np.random.random()

0.5488135039273248

🔺 seed를 지정하면 난수표를 이용한 랜덤수 생성을 동일하게 만들 수 있다. 즉, seed에 0이라고 입력했을 때 나오는 수는 항상 동일하다.

서로 다른 사람끼리 프로그래밍을 테스트 한다던지 데모를 시연 한다던지 할 때, 이렇게 난수발생을 똑같이 맞춰놓고 확인해볼 수 있다.

기존 배열로 생성

원래 사용하던 어레이와 shape만 같고 내용은 초기화된 어레이를 생성하고 싶을 때 사용하는 기능들이다. 앞서했던 empty, zeros, ones, full에다가 _like를 붙인 함수들을 사용한다.

b = np.array([[10,20,30], [40,50,60]])
b

array([[10, 20, 30],
       [40, 50, 60]])   # 결과

🔺 이렇게 2행 3열짜리 shape (2,3)인 어레이가 있다고 할 때

c = np.empty_like(b)
c

array([[94103137661152,              0,              0],
       [             0,              0,              0]])

🔺 empty_like : b와 똑같은 배열로 내용 값은 초기화한 상태로 생성

d = np.zeros_like(b)
d

array([[0, 0, 0],
       [0, 0, 0]])

🔺 zeros_like : b와 똑같은 배열로 내용 값을 0으로 초기화하여 생성

e = np.ones_like(b)
e

array([[1, 1, 1],
       [1, 1, 1]])

🔺 ones_like : b와 똑같은 배열로 내용 값을 1로 초기화하여 생성

f = np.full_like(b,255)
f

array([[255, 255, 255],
       [255, 255, 255]])

🔺 full_like : b와 똑같은 배열로 내용 값을 지정된 수로 초기화하여 생성

이미지와 같은 데이터를 작업할 때 그것과 똑같은 틀을 만들어 놓는 상황에서 주로 사용한다.

dtype 변경

어레이를 생성할 때 데이터 타입을 별도 코멘트가 없다면 자동으로 지정되는데, 작업중에 변경이 필요할 경우 다음과 같은 함수들로 바꿔줄 수 있다.

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

((4,), dtype('int64'))

🔺 1,2,3,4 데이터를 가진 어레이를 하나 만들어보았다. 이 때 shape은 1차원이므로 (4, ) 라고 나오고 dtype은 64비트 정수형이다.

b = a.astype(np.float64) # 방법 1
b, b.dtype

(array([1., 2., 3., 4.]), dtype('float64')) # 결과 1

c = np.float64(a)  # 방법 2
c, c.dtype

(array([1., 2., 3., 4.]), dtype('float64')) # 결과 2

🔺 dtype을 변경해주는 방법은 두 가지가 있는데,

1) astype 함수를 사용하고 안에 np.dtype 형태로 지정

2) np.dtype(변수명) 형태

예시를 보면 둘 다 결과는 동일하다.

<dtype 종류>

unitXX : 부호없는 정수

intXX : 정수형

floatXX : 실수형

complexXX : 복소수형

(XX는 비트수)

차원 변경

다음으로 생성된 어레이를 재정렬하는 방법이다.

a = np.arange(12)
a, a.shape

(array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]), (12,))

🔺 이렇게 1차원으로 12개 원소를 가지는 어레이를 하나 만들고

a.reshape(4,3)   # 방법 1
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])


np.reshape(a,(4,3))   # 방법 2
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

🔺 reshape을 이용하여 몇행 몇열로 바꿀건지 지정해줄 수 있다. 방법 1과 2 두가지가 있으며 결과는 동일하다.

이 때 당연하게도, shape의 모양이 원래의 원소 개수를 수용할 수 있게 칸수가 맞아야 에러가 나지 않는다.

a.reshape(4,-1)
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])


a.reshape(-1,3)
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

🔺 reshape을 지정할 때 -1 이라고 하면 미지정 하겠다는 뜻이다. 총 12개인데 4행 이면 뒤에는 당연히 3열, 반대로 3열을 먼저 지정하면 앞에는 당연히 4행이 될 것이다.

이렇게 어차피 뻔한 경우에, 굳이 계산하게 싫은 경우에 -1을 써서 알아서 분할하도록 할 수 있다.

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

       [[ 6,  7],
        [ 8,  9],
        [10, 11]]])

🔺 이렇게 3차원이상의 다차원 어레이로 재배열 할 수도 있다. shape에서 제일 앞에오는 부분이 가장 큰 괄호의 분할이라고 보면 된다.

a = np.arange(12).reshape(3,4)
a

array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])


a.T

array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])

🔺 어레이명.T 라고 하면 행열이 바뀐 전치 배열로 돌려준다. 엑셀에서 행열 전환해서 복사하는 것과 같은 기능이다.

여기까지 파이썬 numpy 라이브러리에서 가장 기본적인 array를 만들고 타입과 배열 형태를 변환하는 방법에 대해 알아보았다.