x = np.arange(1, 101)
x

y = np.arange(101, 201)
y

%%time #c로 할 경우
z = np.zeros_like(x)
for i, (xi, yi) in enumerate(zip(x, y)):
    z[i] = xi + yi

z

"""
Explanation: NumPy 연산
벡터화 연산
NumPy는 코드를 간단하게 만들고 계산 속도를 빠르게 하기 위한 벡터화 연산(vectorized operation)을 지원한다. 벡터화 연산이란 반복문(loop)을 사용하지 않고 선형 대수의 벡터 혹은 행렬 연산과 유사한 코드를 사용하는 것을 말한다.
예를 들어 다음과 같은 연산을 해야 한다고 하자.
$$ 
x = \begin{bmatrix}1 \ 2 \ 3 \ \vdots \ 100 \end{bmatrix}, \;\;\;\;
y = \begin{bmatrix}101 \ 102 \ 103 \ \vdots \ 200 \end{bmatrix},
$$
$$z = x + y = \begin{bmatrix}1+101 \ 2+102 \ 3+103 \ \vdots \ 100+200 \end{bmatrix}= \begin{bmatrix}102 \ 104 \ 106 \ \vdots \ 300 \end{bmatrix}
$$
만약 NumPy의 벡터화 연산을 사용하지 않는다면 이 연산은 루프를 활용하여 다음과 같이 코딩해야 한다.
End of explanation
"""


%%time
z = x + y

z

"""
Explanation: 그러나 NumPy는 벡터화 연산을 지원하므로 다음과 같이 덧셈 연산 하나로 끝난다. 위에서 보인 선형 대수의 벡터 기호를 사용한 연산과 코드가 완전히 동일하다.
End of explanation
"""


x = np.arange(10)
x

a = 100
a * x

"""
Explanation: 연산 속도도 벡터화 연산이 훨씬 빠른 것을 볼 수 있다.
Element-Wise 연산
NumPy의 벡터화 연산은 같은 위치의 원소끼리 연산하는 element-wise 연산이다. NumPy의 ndarray를 선형 대수의 벡터나 행렬이라고 했을 때 덧셈, 뺄셈은 NumPy 연산과 일치한다
스칼라와 벡터의 곱도 마찬가지로 선형 대수에서 사용하는 식과 NumPy 코드가 일치한다.
End of explanation
"""


x = np.arange(10)
y = np.arange(10)
x * y

np.dot(x, y)

x.dot(y)

"""
Explanation: NumPy 곱셉의 경우에는 행렬의 곱, 즉 내적(inner product, dot product)의 정의와 다르다. 따라서 이 경우에는 별도로 dot이라는 명령 혹은 메서드를 사용해야 한다.
End of explanation
"""


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

a == b

a >= b

"""
Explanation: 비교 연산도 마찬가지로 element-wise 연산이다. 따라서 벡터 혹은 행렬 전체의 원소가 모두 같아야 하는 선형 대수의 비교 연산과는 다르다.
End of explanation
"""


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

np.array_equal(a, b) #내부적으로 for문이 돌아가서 빠름

np.array_equal(a, c)

"""
Explanation: 만약 배열 전체를 비교하고 싶다면 array_equal 명령을 사용한다.
End of explanation
"""


a = np.arange(5)
a

np.exp(a) 

10**a

"""
Explanation: 만약 NumPy 에서 제공하는 지수 함수, 로그 함수 등의 수학 함수를 사용하면 element-wise 벡터화 연산을 지원한다.
End of explanation
"""


np.log(a)

np.log10(a) #상용로그를 사용

"""
Explanation: 1 + 2 
이건 int(1).add(int(2)) 이거임
End of explanation
"""


import math
a = [1, 2, 3]
math.exp(a) #이때 하고싶으면 for문을 써야됨

"""
Explanation: 만약 NumPy에서 제공하는 함수를 사용하지 않으면 벡터화 연산은 불가능하다.
End of explanation
"""


x = np.arange(5)
y = np.ones_like(x)
x + y

x + 1

"""
Explanation: 브로드캐스팅
선형대수와 상관 없는 Numpy만의 개념(shape가 다른데 반복해서 같게 해줌)
선형 대수의 행렬 덧셈 혹은 뺄셈을 하려면 두 행렬의 크기가 같아야 한다. 그러나 NumPy에서는 서로 다른 크기를 가진 두 ndarray 배열의 사칙 연산도 지원한다. 이 기능을 브로드캐스팅(broadcasting)이라고 하는데 크기가 작은 배열을 자동으로 반복 확장하여 크기가 큰 배열에 맞추는 방벙이다.
예를 들어 다음과 같이 벡터와 스칼라를 더하는 경우를 생각하자. 선형 대수에서는 이러한 연산이 불가능하다.
$$ 
x = \begin{bmatrix}0 \ 1 \ 2 \ 3 \ 4 \end{bmatrix}, \;\;\;\; 
x + 1 = \begin{bmatrix}0 \ 1 \ 2 \ 3 \ 4 \end{bmatrix} + 1 = ?
$$
그러나 NumPy는 브로드캐스팅 기능을 사용하여 스칼라를 벡터와 같은 크기로 확장시켜서 덧셈 계산을 한다.
$$ 
\begin{bmatrix}0 \ 1 \ 2 \ 3 \ 4 \end{bmatrix} \overset{\text{numpy}}+ 1 = 
\begin{bmatrix}0 \ 1 \ 2 \ 3 \ 4 \end{bmatrix} +  \begin{bmatrix}1 \ 1 \ 1 \ 1 \ 1 \end{bmatrix} = 
\begin{bmatrix}1 \ 2 \ 3 \ 4 \ 5 \end{bmatrix}
$$
End of explanation
"""


a = np.tile(np.arange(0, 40, 11), (3, 1)).T #( 3, 1)은 뭐지    => 알게된 것 : (0, 40, 11) 이 행! 3이 열 1이 높이(깊이)
a

a1 = np.tile(np.arange(0, 40, 10), (2, 2)).T #( 3, 1)은 뭐지
a1

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

a + b

a = np.arange(0, 40, 10)[:, np.newaxis] #컬럼벡터를 만들고 차원을 늘린 것임
a

a + b

"""
Explanation: 브로드캐스팅은 더 차원이 높은 경우에도 적용된다. 다음 그림을 참조하라.
<img src="https://datascienceschool.net/upfiles/dbd3775c3b914d4e8c6bbbb342246b6a.png" style="width: 60%; margin: 0 auto 0 auto;">
End of explanation
"""


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

np.sum(x)

x.sum() #function, method로도 존재함

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

x.min()

x.max()

x.argmin()  # index of minimum 최소값의 위치를 출력

x.argmax()  # index of maximum

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

x.mean() # 평균

np.median(x) # 중앙값 1~100명. 50등의 점수

np.all([True, True, False]) # 불리언일 경우 주로 사용

np.any([True, True, False]) # 하나라도 존재하면 ㅇㅋ

a = np.zeros((100, 100), dtype=np.int) 
a

np.any(a != 0) # 0이 아닌 것이 하나라도 있습니까?

np.all(a == a) #equal array와 비슷함

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

((a <= b) & (b <= c)).all() #all도 메쏘드로 출력 가능

"""
Explanation: 차원 축소 연산
ndarray의 하나의 행에 있는 원소를 하나의 데이터 집합으로 보고 평균을 구하면 각 행에 대해 하나의 숫자가 나오게 된다. 예를 들어 10x5 크기의 2차원 배열에 대해 행-평균을 구하면 10개의 숫자를 가진 1차원 벡터가 나오게 된다. 이러한 연산을 차원 축소(dimension reduction) 연산이라고 한다.
ndarray 는 다음과 같은 차원 축소 연산 명령 혹은 메서드를 지원한다.

최대/최소: min, max, argmin, argmax
통계: sum, mean, median, std, var
불리언: all, any
End of explanation
"""


x = np.array([[1, 1], [2, 2]])
x

x.sum()

x.sum(axis=0)   # columns (first dimension) row만 남기고 column을 없애라

x.sum(axis=1)   # rows (second dimension)

"""
Explanation: 연산의 대상이 2차원 이상인 경우에는 어느 차원으로 계산을 할 지를 axis 인수를 사용하여 지시한다. axis=0인 경우는 행 연산, axis=1인 경우는 열 연산 등으로 사용한다. 디폴트 값은 0이다.
<img src="https://datascienceschool.net/upfiles/edfaf93a7f124f359343d1dcfe7f29fc.png", style="margin: 0 auto 0 auto;">
axis를 주지 않으면 행, 열로 2번 실행함(그러나 메쏘드마다 조금 다를 수 있음)
End of explanation
"""


y = np.array([[1, 2, 3], [5, 6, 1]])
np.median(y, axis=-1) # last axis

np.median(y, axis=0)

"""
Explanation: axis=-1 : 마지막 차원을 남겨라
End of explanation
"""


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

np.sort(a)

np.sort(a, axis=0)

np.sort(a, axis=1)

"""
Explanation: 정렬
sort 명령이나 메서드를 사용하여 배열 안의 원소를 크기에 따라 정렬하여 새로운 배열을 만들 수도 있다. 2차원 이상인 경우에는 마찬가지로 axis 인수를 사용하여 방향을 결정한다.
대부분 메쏘드와 명령어가 동일한 기능을 하는데 이 친구들은 다름
명령어를 하게되면 새로운 행렬을 만들어줌 np.sort(a)
메쏘드를 하면 행렬 자체가 바뀜(a.sort) -> in-place 메쏘드(메모리 할당할 수 있는 시간을 절약할 수 있다는 장점이 있음. 기본적으로 인플레이스를 자주 쓰지만 원래의 값이 변하니까 주의해야됨
End of explanation
"""


a.sort(axis=1)
a

"""
Explanation: sort 메서드는 해당 객체의 자료 자체가 변화하는 in-place 메서드이므로 사용할 때 주의를 기울여야 한다.
End of explanation
"""


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

a[j] # array indexing! 을 하면 값을 알 수 있음

a = np.array([80, 72, 99, 35])
j = np.sort(a)
j

j1 = np.argsort(a)
j1

a = np.array([ 72, 99, 35])
j = np.argsort(a)
j

"""
Explanation: 만약 자료를 정렬하는 것이 아니라 순서만 알고 싶다면 argsort 명령을 사용한다.
등수를 아는 인덱스만 알려줌. 실제 sorting하진 않음
End of explanation
"""


