# Original book version 
def vector_sum(vectors):
    return reduce(vector_add, vectors)

vectors = [v,w,v,w,v,w]
vector_sum(vectors)

# Modified version by sc82.choi at Gachon - *은 여러개의 argument를 list로 전환해줌
def vector_sum_modified(vectors):
    return [sum(value) for value in zip(*vectors)]

vectors = [v,w,v,w,v,w]
vector_sum_modified(vectors)

%timeit vector_sum(vectors)
%timeit vector_sum_modified(vectors)
%timeit np.sum([v,w,v,w,v,w], axis=0)

# Numpy operation
np.sum([v,w,v,w,v,w], axis=0) 
# axis=0 는 row [v,w,v,w,v,w]를 하나의 matrix로 생각했을 때, column별로 sum operation을 하라는 의미
# axis=1 는 row [v,w,v,w,v,w]를 하나의 matrix로 생각했을 때, row별로 sum operation을 하라는 의미

"""
Explanation: 여러개의 Vector를 list로 만들어서 sum 연산을 하는 경우
End of explanation
"""


# Original book verstion
def scalar_multiply(c, v):
    return [c * v_i for v_i in v]

v = [5, 6, 7, 8]
scalar = 3

scalar_multiply(scalar, v)

# Numpy version: Numpy는 배열의 크기가 다르더라도 기본적인 vector연산을 가능하도록 지원해준다. 이를 broadcasting이라고 함

scalar * np.array(v)

%timeit scalar_multiply(scalar, v)
%timeit scalar * np.array(v)

"""
Explanation: Scalar * Vector의 연산 ex) 2 * [1,2,3,4] = [2,4,6,8]
End of explanation
"""


# Original book version

def vector_mean(vectors):
    """compute the vector whose i-th element is the mean of the
    i-th elements of the input vectors"""
    n = len(vectors)
    return scalar_multiply(1/n, vector_sum(vectors))

v = [1,2,3,4]
w = [-4,-3,-2,-1]

vector_mean([v,v,v,v])

# Original book version
np.mean([v,v,v,v], axis=0)
# axis=0 는 row [v,w,v,w,v,w]를 하나의 matrix로 생각했을 때, column별로 mean operation을 하라는 의미
# axis=1 는 row [v,w,v,w,v,w]를 하나의 matrix로 생각했을 때, row별로 mean operation을 하라는 의미

%timeit vector_mean([v,v,v,v])
%timeit np.mean([v,v,v,v], axis=0)

"""
Explanation: vector 의 평균 구하기: 크기가 같은 vector를 matrix형태로 입력했을 경우, 각 row별 평균을 구함
End of explanation
"""


# Original book version
def dot(v, w):
    """v_1 * w_1 + ... + v_n * w_n"""
    return sum(v_i * w_i for v_i, w_i in zip(v, w))

v = [1,2,3,4]
w = [-4,-3,-2,-1]

dot(v, w)

# Numpy version
np.dot(v,w)

%timeit dot(v, w)
%timeit np.dot(v, w)

"""
Explanation: Vector dot product: 크기가 같은 두개의 vector가 있으면, 같은 column에 해당하는 값을 곱한 후 모든 값을 더함
End of explanation
"""


# Original book version
def sum_of_squares(v):
    """v_1 * v_1 + ... + v_n * v_n"""
    return dot(v, v)

v = [1,2,3,4]
sum_of_squares(v) # v * v = [1,4,9,16]

# Numpy version
np.dot(v,v) # or sum(np.square(v))

"""
Explanation: 하나의 vector에서 값 element들을 제곱하여 더한 후 값을 반환함
End of explanation
"""


# Orginal book version
def magnitude(v):
    return math.sqrt(sum_of_squares(v))

magnitude(v)

# Numpy version
np.linalg.norm(v)

%timeit magnitude(v)
%timeit np.linalg.norm(v)

"""
Explanation: magnitude: 하나의 vector를 dot_product한 후 양의 제곱근을 구함
End of explanation
"""


#original version
def squared_distance(v, w):
    return sum_of_squares(vector_subtract(v, w))

def distance(v, w):
    return math.sqrt(squared_distance(v, w))

v = [1,2,3,4]
w = [-4,-3,-2,-1]

squared_distance(v,w) 

distance(v,w)

# Numpy version
np.linalg.norm(np.subtract(v,w)) # or np.sqrt(np.sum(np.subtract(v,w)**2))


%timeit distance(v, w)
%timeit np.linalg.norm(np.subtract(v,w))

"""
Explanation: Distance 구하기: vector간의 거리를 구하는 공식

파타고라스의 정리 처럼, 두 점(두 벡터) 사이의 거리는 (x1 - y1)^2 + (x2 - y2)^2 의 제곱근을 구해주면 됨
피타고라스의 정리 처럼 두 점이 이차원 평면이 아닌 n차원의 vector로 구성되는 차이점이 있음
정식 명칭은 Eculidian distance 라고 함
End of explanation
"""


def shape(A):
    num_rows = len(A)
    num_cols = len(A[0]) if A else 0
    return num_rows, num_cols

def get_row(A, i):
    return A[i]

def get_column(A, j):
    return [A_i[j] for A_i in A]

example_matrix = [[1,2,3,4,5], [11,12,13,14,15], [21,22,23,24,25]]

shape(example_matrix)

get_row(example_matrix, 0)

get_column(example_matrix,3)

# Numpy version 
np.shape(example_matrix)

example_matrix = np.array(example_matrix)
example_matrix[0] #row slicing

example_matrix[:,3] #column slicing

"""
Explanation: Matrix  indexing

shape: matrix의 크기를 구함
get_row: matrix에서 하나의 row을 추출함
get_column: matrix에서 하나의 column을 추출함
End of explanation
"""


def make_matrix(num_rows, num_cols, entry_fn):
    """returns a num_rows x num_cols matrix 
    whose (i,j)-th entry is entry_fn(i, j)"""
    return [[entry_fn(i, j) for j in range(num_cols)]
            for i in range(num_rows)]  

def is_diagonal(i, j):
    """1's on the 'diagonal', 0's everywhere else"""
    return 1 if i == j else 0

identity_matrix = make_matrix(5, 5, is_diagonal)

identity_matrix

# Numpy version
np.identity(5)

friendships = [[0, 1, 1, 0, 0, 0, 0, 0, 0, 0], # user 0
               [1, 0, 1, 1, 0, 0, 0, 0, 0, 0], # user 1
               [1, 1, 0, 1, 0, 0, 0, 0, 0, 0], # user 2
               [0, 1, 1, 0, 1, 0, 0, 0, 0, 0], # user 3
               [0, 0, 0, 1, 0, 1, 0, 0, 0, 0], # user 4
               [0, 0, 0, 0, 1, 0, 1, 1, 0, 0], # user 5
               [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 6
               [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 7
               [0, 0, 0, 0, 0, 0, 1, 1, 0, 1], # user 8
               [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]] # user 9

def matrix_add(A, B):
    if shape(A) != shape(B):
        raise ArithmeticError("cannot add matrices with different shapes")
        
    num_rows, num_cols = shape(A)
    def entry_fn(i, j): return A[i][j] + B[i][j]
        
    return make_matrix(num_rows, num_cols, entry_fn)

A = [[ 1., 0., 0.], [ 0., 1., 2.]]
B = [[ 5., 4., 3.], [ 2., 2., 2.]]

matrix_add(A,B)

# Numpy version

np.add(A,B) # vector 마찬가지로 크기 같은 matrix 형태의 list가 돌아오면 자동으로 변환함

def make_graph_dot_product_as_vector_projection(plt):
    v = [2, 1]
    w = [math.sqrt(.25), math.sqrt(.75)]
    c = dot(v, w)
    vonw = scalar_multiply(c, w)
    o = [0,0]

    plt.arrow(0, 0, v[0], v[1], 
              width=0.002, head_width=.1, length_includes_head=True)
    plt.annotate("v", v, xytext=[v[0] + 0.1, v[1]])
    plt.arrow(0 ,0, w[0], w[1], 
              width=0.002, head_width=.1, length_includes_head=True)
    plt.annotate("w", w, xytext=[w[0] - 0.1, w[1]])
    plt.arrow(0, 0, vonw[0], vonw[1], length_includes_head=True)
    plt.annotate(u"(v•w)w", vonw, xytext=[vonw[0] - 0.1, vonw[1] + 0.1])
    plt.arrow(v[0], v[1], vonw[0] - v[0], vonw[1] - v[1], 
              linestyle='dotted', length_includes_head=True)
    plt.scatter(*zip(v,w,o),marker='.')
    plt.axis([0,3,0,2]) # 짤리는 부분이 있어서 변경
    plt.show()

%pylab inline
make_graph_dot_product_as_vector_projection(plt)

"""
Explanation: Matrix  operation

make_matirx: entry_fn을 기준으로 num_rows와 num_cols을 생성함
id_diagonal: 대각행렬이 1 matrix를 만들기 위해, row와 column 의 값이 같으면 1 아니면 0을 반환
matrix_add: matrix간 덧셈
End of explanation
"""


