어레이 연산

어레이를 이용한 연산의 기본을 배운다.

  • 어레이 연산은 기본적으로 항목별로 이루어진다.

In [1]:
import numpy as np

어레이 기본연산

사칙연산 등 기본연산이 작동하는 방식을 살펴본다.

덧셈

  • 어레이와 숫자의 덧셈

    • 해당 숫자를 어레의 모든 항목에 동일하게 더한다.
    • 최종 결과는 기존 어레이와 동일한 모양(shape)의 어레이이다.

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

In [3]:
a + 1


Out[3]:
array([2, 3, 4, 5])

덧셈은 교환법칙이 성립한다. 즉, 더하는 순서를 바꿔도 결과는 마찬가지이다.


In [4]:
1 + a


Out[4]:
array([2, 3, 4, 5])
  • 어레이아 어레이의 덧셈

    • 동일한 모양(shape)어레이들을 더할 수 있다.
    • 결과값은 동일한 모양의 어레이이며, 각 항목별로 덧셈이 실행된다.

어레이 a에 동일한 모양의 어레이 b를 더해보자.


In [5]:
b = np.ones(4) + 1
b


Out[5]:
array([ 2.,  2.,  2.,  2.])

In [6]:
a + b


Out[6]:
array([ 3.,  4.,  5.,  6.])

곱셈

  • 어레이와 숫자의 곱셈

    • 해당 숫자를 어레의 모든 항목에 동일하게 곱한다.
    • 최종 결과는 기존 어레이와 동일한 모양(shape)의 어레이이다.

In [7]:
a * 3


Out[7]:
array([ 3,  6,  9, 12])
  • 어레이아 어레이의 곱셈

    • 동일한 모양(shape)어레이들을 곱할 수 있다.
    • 결과값은 동일한 모양의 어레이이며, 각 항목별로 곱셈이 실행된다.

In [8]:
a * b


Out[8]:
array([ 2.,  4.,  6.,  8.])

주의 사항

두 어레이의 곱은 항목별로 이루어진다. 반면에 2차원 어레이의 곱을 행렬의 곱으로 처리하고 할 경우에는 dot 메소드를 활용할 수 있다.


In [9]:
a = np.ones((3,3))
a


Out[9]:
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])

In [10]:
b = a + a
b


Out[10]:
array([[ 2.,  2.,  2.],
       [ 2.,  2.,  2.],
       [ 2.,  2.,  2.]])

In [11]:
a * b


Out[11]:
array([[ 2.,  2.,  2.],
       [ 2.,  2.,  2.],
       [ 2.,  2.,  2.]])

In [12]:
a.dot(b)  # 행렬의 곱으로 연산한다.


Out[12]:
array([[ 6.,  6.,  6.],
       [ 6.,  6.,  6.],
       [ 6.,  6.,  6.]])

기타 연산: 나숫셈, 지수승 등등

기본적으로 덧셈, 곱셈의 경우와 동일하게 처리된다.

나눗셈


In [13]:
a/3.


Out[13]:
array([[ 0.33333333,  0.33333333,  0.33333333],
       [ 0.33333333,  0.33333333,  0.33333333],
       [ 0.33333333,  0.33333333,  0.33333333]])

In [14]:
5./a


Out[14]:
array([[ 5.,  5.,  5.],
       [ 5.,  5.,  5.],
       [ 5.,  5.,  5.]])

In [15]:
a / b


Out[15]:
array([[ 0.5,  0.5,  0.5],
       [ 0.5,  0.5,  0.5],
       [ 0.5,  0.5,  0.5]])

지수승


In [16]:
a ** 3


Out[16]:
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])

In [17]:
2**a


Out[17]:
array([[ 2.,  2.,  2.],
       [ 2.,  2.,  2.],
       [ 2.,  2.,  2.]])

In [18]:
a ** b


Out[18]:
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])

이제 어레이를 이용하여 방정식을 계산할 수 있다.

방정식


In [19]:
2 ** a - (b * b - 4)


Out[19]:
array([[ 2.,  2.,  2.],
       [ 2.,  2.,  2.],
       [ 2.,  2.,  2.]])

주의사항

두 개의 어레이를 이용한 연산은 기본적으로 모양(shape)이 동일할 경우에만 작동한다. 동일하지 않을 경우 ValueError 오류가 발생한다.


In [21]:
a = np.arange(4)
a + np.arange(1, 3)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-13cf95dcce1a> in <module>()
      1 a = np.arange(4)
----> 2 a + np.arange(1, 3)

ValueError: operands could not be broadcast together with shapes (4,) (2,) 

리스트 조건제시법과의 비교

리스트의 경우 항목별로 연산을 하려면 리스트 조건제시법을 사용해야 한다. 그런데 어레이의 항목별 계산의 처리 속도가 훨씨 빠르다.

예를 들어, 길이가 1만인 어레의 각 항목에 1을 더하는 연산을 실행하면, 리스트의 조건제시법을 사용하는 경우보다 50배 정도 빠르게 계산이 됨을 아래와 같이 확인할 수 있다.


In [22]:
a = np.arange(10000)

In [23]:
%timeit a + 1


The slowest run took 7.55 times longer than the fastest. This could mean that an intermediate result is being cached 
100000 loops, best of 3: 8.47 µs per loop

In [24]:
b = range(10000)

In [25]:
%timeit [i + 1 for i in b]


1000 loops, best of 3: 473 µs per loop

어레이 논리연산

비교 및 논리연산을 살펴본다. 여기서도 기본적으로 항목별 연산을 사용한다.

비교 연산

두 어레의 각 항목별로 '같다', '크다', '작다' 등의 비교연산을 실행한다. 실행하면 동일한 크기의 불값 어레이가 리턴된다.


In [26]:
a = np.arange(1, 5)
a


Out[26]:
array([1, 2, 3, 4])

In [27]:
b = np.arange(4, 0, -1)
b


Out[27]:
array([4, 3, 2, 1])

In [28]:
a == b


Out[28]:
array([False, False, False, False], dtype=bool)

In [29]:
a > b


Out[29]:
array([False, False,  True,  True], dtype=bool)

항목별 비교가 아닌 어레이 자체의 동일성 여부를 확인하려면 np.array_equal 함수를 이용해야 한다.


In [30]:
np.array_equal(a, b)


Out[30]:
False

논리 연산

불값 연산에서 기본적으로 사용되는 and, or 등의 연산을 불값 어레이에 대해서 항목별로 적용할 수 있다.

먼저 불값 연산을 다시 확인해보자. 보다 자세한 사항은 아래 사이트를 이용할 수 있다.

http://200315193.tistory.com/263


In [31]:
True and False


Out[31]:
False

In [32]:
True or False


Out[32]:
True

이제 어레이를 대상으로 하여 논리연산을 실행해 보자.

TrueFalse 만으로 구성된 어레이를 이용한다. 아래 정의에서 01을 이용하였지만 dtype을 이용하여 불값으로 변경되었음에 주의하라.


In [33]:
a = np.array([1, 1, 0, 0], dtype=bool)

b = np.array([1, 0, 1, 1], dtype=bool)

ab의 각 항목별로 논리연산을 다음과 같이 실행한다.

  • np.logical_or는 각 항목별로 or 함수를 실행한다.
  • np.logical_and는 각 항목별로 and 함수를 실행한다.

In [34]:
np.logical_or(a, b)


Out[34]:
array([ True,  True,  True,  True], dtype=bool)

In [35]:
np.logical_and(a, b)


Out[35]:
array([ True, False, False, False], dtype=bool)

함수호출

sin, log, exp 등 하나의 숫자를 입력받아 계산하는 함수에 숫자들의 어레이를 인자로 입력할 수 있다. 그러면, 해당 어레이의 항목별로 함수호출이 이루어지며, 기존 어레이의 모양(shape)과 동일한 모양의 어레이가 리턴된다.


In [36]:
a = np.arange(5)
a


Out[36]:
array([0, 1, 2, 3, 4])

예제: 사인함수 호출


In [37]:
np.sin(a)


Out[37]:
array([ 0.        ,  0.84147098,  0.90929743,  0.14112001, -0.7568025 ])

예제: 로그함수 호출

로그함수의 경우 입력값이 0이면 함수가 정의되어 있지 않으므로 해당 부분에 대해 RunTime 경고가 발생한다.


In [38]:
np.log(a)


/Users/gslee/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:1: RuntimeWarning: divide by zero encountered in log
  if __name__ == '__main__':
Out[38]:
array([       -inf,  0.        ,  0.69314718,  1.09861229,  1.38629436])

예제: 지수함수 호출


In [39]:
np.exp(a)


Out[39]:
array([  1.        ,   2.71828183,   7.3890561 ,  20.08553692,  54.59815003])

예제: 임의의 함수 호출

사용자가 임의로 정의한 함수를 호출할 수도 있다.


In [40]:
def f(x):
    return x ** x

f(a)


Out[40]:
array([  1,   1,   4,  27, 256])

전치행렬(Transposed Matrix)

선형대수학에서 많이 다루는 행렬의 전치(transposition)를 구하는 방법이다. 행렬의 전치는 통계 분야에서 유용하게 활용된다.

전치행렬은 기존의 행렬에서 행과 열의 역할을 교환해서 만들어진다. 즉, 기존의 행렬과 비교해서 대각선을 축으로 하여 대칭인 행렬이 전치행렬이다.

예제:

다음의 2차원 행렬을 이용하여 전치행렬을 다루는 법을 연습한다.

$$\left [ \begin{matrix} 0 & 1 & 1 \\ 0 & 0 & 1 \\ 0 & 0 & 0\end{matrix} \right ]$$

위 행렬의 전치는 다음과 같다.

$$\left [ \begin{matrix} 0 & 0 & 0 \\ 1 & 0 & 0 \\ 1 & 1 & 0\end{matrix} \right ]$$

위 행렬을 생성하기 위해 triu 함수를 이용한다. help(triu)를 이용하여 해당 함수의 활용법을 확인해야 한다.


In [41]:
a = np.triu(np.ones((3, 3)), 1)
a


Out[41]:
array([[ 0.,  1.,  1.],
       [ 0.,  0.,  1.],
       [ 0.,  0.,  0.]])

전치행렬을 T 메소드를 활용하여 쉽게 구할 수 있다.


In [42]:
a.T


Out[42]:
array([[ 0.,  0.,  0.],
       [ 1.,  0.,  0.],
       [ 1.,  1.,  0.]])

전치행렬은 내부적으로 '뷰(view)' 방식을 이용하여 처리된다. 따라서 전치행렬의 값을 변경하면 원래 행력의 값도 변경됨에 주의해야 한다.


In [43]:
a.T[0, 1] = 5
a.T


Out[43]:
array([[ 0.,  5.,  0.],
       [ 1.,  0.,  0.],
       [ 1.,  1.,  0.]])

In [44]:
a


Out[44]:
array([[ 0.,  1.,  1.],
       [ 5.,  0.,  1.],
       [ 0.,  0.,  0.]])

추가 조심사항

전치행렬이 뷰 방식에 따라 사용되기에 아래와 같은 함수를 호출할 때 조심해야 한다.

아래 코드의 경우 두 개의 행렬을 각 항목별로 더하는 방식을 취한다. 따라서 행렬 aa.T를 인자로 입력하면 대칭행렬이 리턴되는 것을 기대할 수 있다. 하지만 결과는 다르게 나온다. 이유는 a.T 가 뷰 방식을 따르고 중첩 for문이 진행되는 동안 항목 값들이 계속해서 변하기 때문이다.


In [45]:
def mat_add(a, b):
    for i in range(a.shape[0]):
        for j in range(a.shape[1]):
            a[i,j] += b[i,j]
    return a

In [46]:
c = np.arange(9).reshape((3,3))
c


Out[46]:
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

어레이 c는 다음 행렬에 대응한다.

$$\left [ \begin{matrix} 0 & 1 & 2 \\ 3 & 4 & 5 \\ 6 & 7 & 8 \end{matrix} \right ]$$

따라서 c.T는 다음 행렬에 대응한다.

$$\left [ \begin{matrix} 0 & 3 & 6 \\ 1 & 4 & 7 \\ 2 & 5 & 8 \end{matrix} \right ]$$

이제 mat_add(c, c.T)는 아래의 대칭행렬이 될 것으로 기대한다.

$$\left [ \begin{matrix} 0 & 4 & 8 \\ 4 & 8 & 12 \\ 8 & 12 & 16 \end{matrix} \right ]$$

하지만 결과가 다르게 나온다.


In [47]:
mat_add(c, c.T)


Out[47]:
array([[ 0,  4,  8],
       [ 7,  8, 12],
       [14, 19, 16]])

이유가 무엇일까? 먼저 0번 행의 경우는 예상과 일치한다. 그런데 1번 행부터 값이 달라졌다. 이는, 예를 들어 (i, j) = (1, 0) 의 경우 c.T[1, 0]의 값이 c[0, 1]인데 0번 행을 계산하면서 값이 1에서 이미 4로 변경되었기 때문이다. 따라서

c[1,0] + c.T[1,0] = c[1,0] + c[0,1] = 3 + 4 = 7

이 되는 것이다.

축약함수(Reduction): 어레이 축약하기

어레이는 데이터 분석을 위해 유용하게 사용되는 자료형이다. 자료를 분석하기 위해 기본적으로 사용되는 축약함수들을 살펴본다.

합(sum) 구하기

어레이에 사용되는 숫자들의 합을 계산한다.

  • 어레이에 사용되는 모든 숫자들의 합을 계산할 수 있다.
  • 각 차원별 숫자들의 합을 구할 수 있다.

    • 축(axis)을 이용한다.
    • 2차원 어레이의 경우, 예를 들어, 행별 열별 합을 따로따로 계산을 수 있다.
  • 1차원 어레이의 경우 전체 합만 계산할 수 있다.

  • np.sum 함수를 이용하거나, 어레이 클래스의 sum 메소드를 이용할 수 있다.

1차원 어레이 합


In [48]:
x = np.arange(1, 5)
x


Out[48]:
array([1, 2, 3, 4])

In [49]:
np.sum(x)


Out[49]:
10

In [50]:
x.sum()


Out[50]:
10

2차원 어레이의 합 구하기


In [51]:
x = np.arange(6).reshape((2, 3))
x


Out[51]:
array([[0, 1, 2],
       [3, 4, 5]])

In [52]:
x.sum()


Out[52]:
15

축(axis)를 이용하여 행별, 열별 합을 구할 수 있다.

  • 열별 합을 구하기 위해서는 axis=0을 이용한다.

In [53]:
x.sum(axis=0)


Out[53]:
array([3, 5, 7])

위의 결과는 아래의 세 과정을 거쳐서 생성된다.

먼저, 0번 열의 합은 다음과 같이 구해진다.


In [54]:
x[:, 0].sum()


Out[54]:
3

다음으로, 1번 열의 합은 다음과 같이 구해진다.


In [55]:
x[:, 1].sum()


Out[55]:
5

다음으로, 2번 열의 합은 다음과 같이 구해진다.


In [56]:
x[:, 2].sum()


Out[56]:
7

즉, axis=0을 사용할 경우 1차원의 경우에는 전체(':')를 사용하고, 나머지 차원에 대해서는 모든 가능성, 즉, 각 열에 대해 따로따로 계산을 하는 것과 동일하다. 위 예제의 경우는 0번 열, 1번 열, 2번 열의 3개의 가능성이 있다.

  • 행별 합을 구하기 위해서는 axis=1을 이용한다.

In [57]:
x.sum(axis=1)


Out[57]:
array([ 3, 12])

앞서의 경우와 마찬가지로 아래와 같이 axis=1을 설명할 수 있다.


In [58]:
x[0, :].sum()


Out[58]:
3

In [59]:
x[1, :].sum()


Out[59]:
12

3차원 어레이의 합 구하기

3차원 이상의 어레이의 합을 이해하기는 간단하지 않다. 특히, 축(axis) 이용한 계산은 쉽게 그림이 그려지지 않는다. 하지만 앞서 2차원 어레이의 합을 설명한 것을 이해한다면 어느정도 감을 잡을 수 있다.


In [60]:
x = np.arange(30).reshape((2,3,5))
x


Out[60]:
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, 24],
        [25, 26, 27, 28, 29]]])

축을 이용하지 않는 합은 간단하다. 전체 항목의 합을 구한다.


In [61]:
x.sum()


Out[61]:
435

이제 axis=0, axis=1, axis=2의 역할을 이해하기 위해 axis=1을 이해하는 방법을 설명한다.


In [62]:
y = x.sum(axis=1)
y


Out[62]:
array([[15, 18, 21, 24, 27],
       [60, 63, 66, 69, 72]])

In [63]:
y.shape


Out[63]:
(2, 5)

y2x5행렬에 대응한다. 왜 그럴까? 앞서 2차원 어레이에서 axis=1을 이해한 방식과 동일하게 이해하면 된다.

  • axis=1이기 때문에 2차원 값에 해당하는 부분은 전체를 대항으로 슬라이싱 하고 나머지는 각각의 경우를 모두 따져야 한다.
  • 그런데 1차원에 해당하는 부분에서는 2가지 경우가 있고 3차원에 해당하는 부분에서는 5가지의 경우가 있다.
  • 즉, 총 2x5 의 경우가 발생한다.
  • 따라서 리턴값이 (2,5) 모양을 갖는 어레이가 된다.

2x5 에 해당하는 경우의 몇 가지 예는 아래와 같다.


In [64]:
x[0, :, 0].sum()


Out[64]:
15

In [65]:
x[0, :, 1].sum()


Out[65]:
18

In [66]:
x[0, :, 2].sum()


Out[66]:
21

등등.

예제

통계 분석을 위해 어레이를 사용하는 예제이다.

먼저 아래 사이트에서 populations.txt 파일을 다운 받는다. 파일을 다운 받으려면 링크에 마우스를 가져간 다음 마우스 오른쪽 버튼을 눌러 링크에 연결된 파일을 다운받으면 된다.

https://github.com/scipy-lectures/scipy-lecture-notes/blob/master/data/populations.txt

해당 파일은 1900년부터 1920년까지 캐나다 북부지역에서 서식한 산토끼(hare)와 스라소니(lynx)의 숫자, 그리고 채소인 당근(carrot)의 재배숫자를 순수 텍스트 데이터로 담고 있다.

파일의 내용을 확인하려면 리눅스 터미널 명령어인 cat를 이용한다.


In [67]:
!cat data/populations.txt


# year	hare	lynx	carrot
1900	30e3	4e3	48300
1901	47.2e3	6.1e3	48200
1902	70.2e3	9.8e3	41500
1903	77.4e3	35.2e3	38200
1904	36.3e3	59.4e3	40600
1905	20.6e3	41.7e3	39800
1906	18.1e3	19e3	38600
1907	21.4e3	13e3	42300
1908	22e3	8.3e3	44500
1909	25.4e3	9.1e3	42100
1910	27.1e3	7.4e3	46000
1911	40.3e3	8e3	46800
1912	57e3	12.3e3	43800
1913	76.6e3	19.5e3	40900
1914	52.3e3	45.7e3	39400
1915	19.5e3	51.1e3	39000
1916	11.2e3	29.7e3	36700
1917	7.6e3	15.8e3	41800
1918	14.6e3	9.7e3	43300
1919	16.2e3	10.1e3	41300
1920	24.7e3	8.6e3	47300

앞서 설명한 대로 각 연도별로 산토끼, 스라소니, 당근의 개체수를 담고 있다. 첫째 행처럼 '#'로 시작하면 파이썬에 의해 주석처리 된다. 'cat' 명령어는 파이썬 명령어가 아니라서 주석처리하지 않는 것 뿐이다.

이제 해당 파일을 파이썬으로 불러 들이자. 파이썬의 open 함수 대신에 넘파이의 loadtxt 함수를 이용한다.


In [68]:
data = np.loadtxt('data/populations.txt')

data를 확인해보면 첫째 행을 제외한 행과 열이 2차원 어레이로 저장되었음을 확인할 수 있다.


In [69]:
data


Out[69]:
array([[  1900.,  30000.,   4000.,  48300.],
       [  1901.,  47200.,   6100.,  48200.],
       [  1902.,  70200.,   9800.,  41500.],
       [  1903.,  77400.,  35200.,  38200.],
       [  1904.,  36300.,  59400.,  40600.],
       [  1905.,  20600.,  41700.,  39800.],
       [  1906.,  18100.,  19000.,  38600.],
       [  1907.,  21400.,  13000.,  42300.],
       [  1908.,  22000.,   8300.,  44500.],
       [  1909.,  25400.,   9100.,  42100.],
       [  1910.,  27100.,   7400.,  46000.],
       [  1911.,  40300.,   8000.,  46800.],
       [  1912.,  57000.,  12300.,  43800.],
       [  1913.,  76600.,  19500.,  40900.],
       [  1914.,  52300.,  45700.,  39400.],
       [  1915.,  19500.,  51100.,  39000.],
       [  1916.,  11200.,  29700.,  36700.],
       [  1917.,   7600.,  15800.,  41800.],
       [  1918.,  14600.,   9700.,  43300.],
       [  1919.,  16200.,  10100.,  41300.],
       [  1920.,  24700.,   8600.,  47300.]])

위 데이터를 이용하여 1900년부터 1920년 사이에 각 개체별로 어떤 변화가 발생하였는지를 분석하고자 한다. 그러기 위해서는 각 개체별로 따로 어레이로 저장하는 것이 필요하다. 즉, 전치(transposition)을 이용해야 한다.

data.T는 4개의 행으로 이루어진 2차원 어레이를 나타내며, 각각의 행은 차례대로 년도, 산토끼 수, 스라소니 수, 당근 수를 연도별로 담고 있다.

넘파이는 각각의 행에 대해 변수를 선언할 수 있는 기능을 제공한다. (이는 튜플의 경우 각각의 항목에 변수를 선언할 수 있는 것과 동일하다.)


In [70]:
year, hares, lynxes, carrots = data.T

예를 들어 year 변수에는 연도들로 이루어진 1차원 어레이가 할당된다.


In [71]:
year


Out[71]:
array([ 1900.,  1901.,  1902.,  1903.,  1904.,  1905.,  1906.,  1907.,
        1908.,  1909.,  1910.,  1911.,  1912.,  1913.,  1914.,  1915.,
        1916.,  1917.,  1918.,  1919.,  1920.])

hares 변수에는 각 연도별 개체수로 이루어진 1차원 어레이가 할당된다.


In [72]:
hares


Out[72]:
array([ 30000.,  47200.,  70200.,  77400.,  36300.,  20600.,  18100.,
        21400.,  22000.,  25400.,  27100.,  40300.,  57000.,  76600.,
        52300.,  19500.,  11200.,   7600.,  14600.,  16200.,  24700.])

lynxes, carrots 경우도 동일하게 작동한다.

이제 위 데이터를 그래프를 이용하여 확인해보자. 그러기 위해 먼저 pyplot 모듈을 임포트한다.


In [73]:
from matplotlib import pyplot as plt

In [74]:
%pylab inline


Populating the interactive namespace from numpy and matplotlib
WARNING: pylab import has clobbered these variables: ['f']
`%matplotlib` prevents importing * from pylab and numpy

아래 그림은 연도별 산토끼의 개체수, 스라소니의 개체수, 당근의 재배수를 각각 파란색, 녹색, 빨간색 그래프를 이용하여 동시에 표현한 것이다.

axes, plot, legned 함수의 활용방법에 대해서는 우선 너무 많은 신경을 쓰지 않아도 된다. 여기서는 기본 사용법만 알면 된다.


In [75]:
plt.axes([0.2, 0.1, 0.5, 0.8])
plt.plot(year, hares, year, lynxes, year, carrots)
plt.legend(('Hare', 'Lynx', 'Carrot'), loc=(1.05, 0.5))


Out[75]:
<matplotlib.legend.Legend at 0x107c61610>

위 그림을 보고 어떤 분석을 할 수 있는가?

  • 산토끼의 개체수와 스라소니의 개체수, 그리고 당근의 재배수의 관계에 주의하면 어느정도 분석을 할 수 있을 것이다.

이제, 예를 들어, 1900년부터 1920년 사이에서 산토끼의 개체수와 스라소니의 개체수 각각의 평균을 구해보자.

산토끼 개체수의 평균만을 확인하고자 한다면 다음과 같이 할 수 있다.


In [76]:
hares.mean()


Out[76]:
34080.952380952382

그리고 스라소니 개체수의 평균만을 확인하고자 한다면 다음과 같이 할 수 있다.


In [77]:
lynxes.mean()


Out[77]:
20166.666666666668

하지만 두 동물의 개체수를 한꺼번에 구할 수 있다. 앞서 sum을 다루면서 배운 axis 기능을 이용하면 된다.


In [78]:
data.T[1:3].mean(axis=1)


Out[78]:
array([ 34080.95238095,  20166.66666667])

아래와 같이 구할 수도 있다.


In [79]:
data.mean(axis=0)[1:3]


Out[79]:
array([ 34080.95238095,  20166.66666667])

연습문제

1900년부터 1920년 사이에 산토끼의 개체수가 최고점을 찍었을 때 스라소니의 개체수는 몇 마리였는가를 확인하라. 또한 스라소니의 개체수가 최저점이었을 때 산토끼의 개체수는 몇 마리였는지를 확인하라.

힌트: max, min, argmax, agrmin 메소드를 축(axis)과 함께 이용하라.

예제

직선상에서 원점에서 출발하여 임의로 오른쪽, 왼쪽으로 한 칸씩 스텝을 움직이는 게임을 1000명을 대상으로 진행한다.
게임은 1000명 모두가 각각 200 스텝을 옮길 때까지 진행되며, 각 참가자는 서로 독립적으로 임의적으로 오른쪽, 왼쪽 스텝을 정한다.

질문: 1000명의 참가자가 t 번의 스텝을 밟았을 때 원점으로부터의 거리가 평균 얼마인가? 그 평균값을 미리 예상할 수 있는가?

위 질문에 답하기 위해 (1000, 200) 모양의 2차원 어레이를 이용한다. 해당 어레이는 1, -1이 무작위로 채워져야 하며, 예를 들어 아래와 같은 모양을 갖는다.

더불어, 참가자들의 스텝이 위와 같을 때 t 스텝 이후의 각 참가자들의 원점으로부터의 거리는 아래 그림에서처럼 행별로 t 스텝까지의 합을 계산하는 것으로부터 구할 수 있다.

이제 1과 -1로 무작위적으로 채워진 (1000, 200) 모양의 2차원 어레이를 생성해보자. 먼저, 참가자들의 수를 n_stories, 최대 스텝의 수를 t_max라 하자.


In [80]:
n_stories = 1000
t_max = 200

t 는 스텝을 밟는 단위를 담고 있는 어레이이다.


In [81]:
t = np.arange(t_max)

1과 -1을 무작위적으로 채운 어레이를 구하려면 다음과 정의하면 된다.


In [82]:
steps = 2 * np.random.random_integers(0, 1, (n_stories, t_max)) - 1

실제로 steps 살펴보면 1과 -1이 임의로 채워져 있다.


In [83]:
steps


Out[83]:
array([[ 1, -1,  1, ...,  1, -1,  1],
       [-1, -1, -1, ...,  1, -1,  1],
       [-1, -1, -1, ..., -1, -1,  1],
       ..., 
       [ 1, -1, -1, ...,  1,  1, -1],
       [ 1, -1,  1, ...,  1, -1,  1],
       [-1,  1,  1, ..., -1,  1, -1]])

위 어레이가 1과 -1만으로 채워졌다는 사실은 np.unique 함수로 확인할 수 있다.


In [84]:
np.unique(steps)


Out[84]:
array([-1,  1])

t 스텝 후의 각 참가자들의 원점으로부터의 거리를 계산하기 위해서는 누적 합(cumulative sum)을 계산하는 cumsum 함수를 이용한다. 행별 누적 합을 계산하기 위해 축(axis) 키워드를 활용한다.


In [85]:
positions = np.cumsum(steps, axis=1)
positions


Out[85]:
array([[  1,   0,   1, ...,  14,  13,  14],
       [ -1,  -2,  -3, ...,   2,   1,   2],
       [ -1,  -2,  -3, ..., -30, -31, -30],
       ..., 
       [  1,   0,  -1, ...,  -8,  -7,  -8],
       [  1,   0,   1, ...,  -6,  -7,  -6],
       [ -1,   0,   1, ...,  -6,  -5,  -6]])

각 참가자들의 t 스텝 후의 누적 합의 평균을 구하기 위해 각각의 제곱의 평균의 제곱근 값을 이용한다.


In [86]:
sq_distance = positions ** 2
sq_distance


Out[86]:
array([[  1,   0,   1, ..., 196, 169, 196],
       [  1,   4,   9, ...,   4,   1,   4],
       [  1,   4,   9, ..., 900, 961, 900],
       ..., 
       [  1,   0,   1, ...,  64,  49,  64],
       [  1,   0,   1, ...,  36,  49,  36],
       [  1,   0,   1, ...,  36,  25,  36]])

예를 들어 200스텝이 완료 된 후에 참가자들의 원점으로부터의 거리의 평균은 다음과 같다.


In [87]:
mean_sq_distance = np.mean(sq_distance, axis=0)
np.sqrt(mean_sq_distance[-1])


Out[87]:
14.230108924389862

t 스텝 후의 참자자들의 원점으로부터의 거리를 그래프로 그리면 다음과 같다.


In [88]:
plt.figure(figsize=(4, 3))
plt.plot(t, np.sqrt(mean_sq_distance), 'y.')
plt.xlabel(r"$t$")
plt.ylabel(r"$\sqrt{\langle (\delta x)^2 \rangle}$")


Out[88]:
<matplotlib.text.Text at 0x107be3250>

그런데 위 그래프는 $f(t) = \sqrt(t)$ 함수의 그래프와 매우 비슷하다.


In [89]:
plt.figure(figsize=(4, 3))
plt.plot(t, np.sqrt(t), 'r-')
plt.xlabel(r"$t$")
plt.ylabel(r"$\sqrt{t}$")


Out[89]:
<matplotlib.text.Text at 0x1084d1e50>

실제로 두 그래프를 합치면 거의 구분되지 않는다.


In [90]:
plt.figure(figsize=(4, 3))
plt.plot(t, np.sqrt(mean_sq_distance), 'y.', t, np.sqrt(t), 'r-')
plt.xlabel(r"$t$")
plt.ylabel(r"$\sqrt{\langle (\delta x)^2 \rangle}$")


Out[90]:
<matplotlib.text.Text at 0x107cb2bd0>

결론

참가자들이 임의로 좌, 우를 선택할 때 $t$ 스텝 후에 원점으로부터의 거리는 평균적으로 $\sqrt{t}$ 이다.

연습문제

200 스텝을 모두 진행한 후에 원점으로부터 오른편에 위치한 참가자들의 원점으로부터의 평균거리를 계산하라.

힌트: NB-18-Numpy-array-indexing-slicing 에서 배운 불값 마스크를 이용한 팬시 인덱싱을 사용할 수 있다.