"""10.5 차원 축소"""

# 차원 축소를 하는 이유
# 1) 잡음에 해당하는 차원 제거, 밀접한 연관된 차원 합쳐 주어 데이터 정제
# 2) 저차원으로 축소시킨 데이터에서는 고차원의 데이터에서 사용할 수 없었던 기법 사용 가능

# 차원 축소는 고차원 데이터에서 편차를 잘 잡아낼 수 있는 부분집합을 찾아낼 때 유용
# 여기서는 2차원의 데이터를 다루어 볼 것


""" 0. 사전 작업 """

from functools import partial, reduce
from linear_algebra import shape, get_row, get_column, make_matrix, \
    vector_mean, vector_sum, dot, magnitude, vector_subtract, scalar_multiply
from stats import standard_deviation, mean
from gradient_descent import maximize_batch
import math

def scale(data_matrix):
    num_rows, num_cols = shape(data_matrix)
    means = [mean(get_column(data_matrix,j))
             for j in range(num_cols)]
    stdevs = [standard_deviation(get_column(data_matrix,j))
              for j in range(num_cols)]
    return means, stdevs

X = [
    [20.9666776351559,-13.1138080189357],
    [22.7719907680008,-19.8890894944696],
    [25.6687103160153,-11.9956004517219],
    [18.0019794950564,-18.1989191165133],
    [21.3967402102156,-10.8893126308196],
    [0.443696899177716,-19.7221132386308],
    [29.9198322142127,-14.0958668502427],
    [19.0805843080126,-13.7888747608312],
    [16.4685063521314,-11.2612927034291],
    [21.4597664701884,-12.4740034586705],
    [3.87655283720532,-17.575162461771],
    [34.5713920556787,-10.705185165378],
    [13.3732115747722,-16.7270274494424],
    [20.7281704141919,-8.81165591556553],
    [24.839851437942,-12.1240962157419],
    [20.3019544741252,-12.8725060780898],
    [21.9021426929599,-17.3225432396452],
    [23.2285885715486,-12.2676568419045],
    [28.5749111681851,-13.2616470619453],
    [29.2957424128701,-14.6299928678996],
    [15.2495527798625,-18.4649714274207],
    [26.5567257400476,-9.19794350561966],
    [30.1934232346361,-12.6272709845971],
    [36.8267446011057,-7.25409849336718],
    [32.157416823084,-10.4729534347553],
    [5.85964365291694,-22.6573731626132],
    [25.7426190674693,-14.8055803854566],
    [16.237602636139,-16.5920595763719],
    [14.7408608850568,-20.0537715298403],
    [6.85907008242544,-18.3965586884781],
    [26.5918329233128,-8.92664811750842],
    [-11.2216019958228,-27.0519081982856],
    [8.93593745011035,-20.8261235122575],
    [24.4481258671796,-18.0324012215159],
    [2.82048515404903,-22.4208457598703],
    [30.8803004755948,-11.455358009593],
    [15.4586738236098,-11.1242825084309],
    [28.5332537090494,-14.7898744423126],
    [40.4830293441052,-2.41946428697183],
    [15.7563759125684,-13.5771266003795],
    [19.3635588851727,-20.6224770470434],
    [13.4212840786467,-19.0238227375766],
    [7.77570680426702,-16.6385739839089],
    [21.4865983854408,-15.290799330002],
    [12.6392705930724,-23.6433305964301],
    [12.4746151388128,-17.9720169566614],
    [23.4572410437998,-14.602080545086],
    [13.6878189833565,-18.9687408182414],
    [15.4077465943441,-14.5352487124086],
    [20.3356581548895,-10.0883159703702],
    [20.7093833689359,-12.6939091236766],
    [11.1032293684441,-14.1383848928755],
    [17.5048321498308,-9.2338593361801],
    [16.3303688220188,-15.1054735529158],
    [26.6929062710726,-13.306030567991],
    [34.4985678099711,-9.86199941278607],
    [39.1374291499406,-10.5621430853401],
    [21.9088956482146,-9.95198845621849],
    [22.2367457578087,-17.2200123442707],
    [10.0032784145577,-19.3557700653426],
    [14.045833906665,-15.871937521131],
    [15.5640911917607,-18.3396956121887],
    [24.4771926581586,-14.8715313479137],
    [26.533415556629,-14.693883922494],
    [12.8722580202544,-21.2750596021509],
    [24.4768291376862,-15.9592080959207],
    [18.2230748567433,-14.6541444069985],
    [4.1902148367447,-20.6144032528762],
    [12.4332594022086,-16.6079789231489],
    [20.5483758651873,-18.8512560786321],
    [17.8180560451358,-12.5451990696752],
    [11.0071081078049,-20.3938092335862],
    [8.30560561422449,-22.9503944138682],
    [33.9857852657284,-4.8371294974382],
    [17.4376502239652,-14.5095976075022],
    [29.0379635148943,-14.8461553663227],
    [29.1344666599319,-7.70862921632672],
    [32.9730697624544,-15.5839178785654],
    [13.4211493998212,-20.150199857584],
    [11.380538260355,-12.8619410359766],
    [28.672631499186,-8.51866271785711],
    [16.4296061111902,-23.3326051279759],
    [25.7168371582585,-13.8899296143829],
    [13.3185154732595,-17.8959160024249],
    [3.60832478605376,-25.4023343597712],
    [39.5445949652652,-11.466377647931],
    [25.1693484426101,-12.2752652925707],
    [25.2884257196471,-7.06710309184533],
    [6.77665715793125,-22.3947299635571],
    [20.1844223778907,-16.0427471125407],
    [25.5506805272535,-9.33856532270204],
    [25.1495682602477,-7.17350567090738],
    [15.6978431006492,-17.5979197162642],
    [37.42780451491,-10.843637288504],
    [22.974620174842,-10.6171162611686],
    [34.6327117468934,-9.26182440487384],
    [34.7042513789061,-6.9630753351114],
    [15.6563953929008,-17.2196961218915],
    [25.2049825789225,-14.1592086208169]
]


""" 1. 각 차원의 평균이 0이 되도록 데이터를 바꿈"""

def de_mean_matrix(A):
    """A의 모든 값에서 각 열의 평균을 빼준 행렬을 반환
    반환된 행렬의 모든 열의 평균은 0"""
    nr, nc = shape(A)
    column_means, _ = scale(A)
    return make_matrix(nr, nc, lambda i, j: A[i][j] - column_means[j])


""" 2. 방향 찾아내기 """

# 1) 길이가 1로 정규화된, 방향을 나타내는 벡터
def direction(w):
    mag = magnitude(w)
    return [w_i / mag for w_i in w]
# 예를 들어, direction([1.2])의 값은 [1/루트5, 2/루트5]

# 2) 길이가 1로 정규화된 방향을 나타내는 벡터가 나타내는 방향으로 데이터의 편차를 계산
def directional_variance_i(x_i, w): # 각 데이터에 대해 계산
    """w가 나타내는 방향에서 x_i 행의 편차를 반환"""
    return dot(x_i, direction(w)) ** 2

def directional_variance(X, w): # 전체 데이터에 대해 계산
    """w가 나타내는 방향에서 데이터 전체의 편차를 반환"""
    return sum(directional_variance_i(x_i, w) for x_i in X)

# 3) 편차를 최대화 시키는 방향을 찾기 위한 경사 하강법 사용
# 편차를 최대화 시키는 이유는, 모든 데이터를 최대한 포괄하는 방향을 구하기 위함
def directional_variance_gradient_i(x_i, w): # 한 데이터가 벡터 w의 방향에 기여하는 부분
    """방향의 경사(w의 기울기)에 x_i가 기여하는 부분"""
    projection_length = dot(x_i, direction(w))
    return [2 * projection_length * x_ij for x_ij in x_i]

def directional_variance_gradient(X, w): # 전체 데이터가 벡터 w의 방향에 기여하는 부분
    return vector_sum(directional_variance_gradient_i(x_i,w) for x_i in X)

# 4) 제1주성분 구하기
def first_principal_component(X):
    """제 1 주성분은 directional_varience를 최대화 시키는 정규화된 방향벡터"""
    guess = [1 for _ in X[0]]
    unscaled_maximizer = maximize_batch(
        partial(directional_variance, X),           # is now a function of w
        partial(directional_variance_gradient, X),  # is now a function of w
        guess)
    return direction(unscaled_maximizer)

print(first_principal_component(X)) # 1번째 주성분


""" 3. 제 1 주성분에 해당하는 방향을 찾은 후, 데이터를 주성분에 투영시켜 해당 주성분위에서 어디에 위치하는가 찾기"""

# 1) 한 데이터를 주성분의 방향에 투영시키기
def project(v, w):
    """v를 w방향으로 투영, 즉 해당 데이터가 해당 주 성분 위의 어디에 위치하는가를 구함"""
    coefficient = dot(v, w)
    return scalar_multiply(coefficient, w) # 이 결과 벡터가 나오며, 이 벡터의 direction은 주 성분의 direction과 일치한다

print(project([20.9666776351559,-13.1138080189357], first_principal_component(X))) # 투영되어 나온 벡터
print(magnitude(project([20.9666776351559,-13.1138080189357], first_principal_component(X)))) # 투영되어 나온 벡터의 길이, 4_4)에서 다시 쓰일 것
print(direction(project([20.9666776351559,-13.1138080189357], first_principal_component(X)))) # 투영되어 나온 벡터의 direction은 주 성분의 direction과 일치한다

# 2) 다른 주성분을 구하기 위해 투영된 데이터 제거
def remove_projection_from_vector(v, w): # 하나의 데이터에 대해 제거
    """v에서 v를 w로 투영시킨 결과를 빼줌"""
    return vector_subtract(v, project(v, w))

def remove_projection(X, w): # 모든 데이터에 대해 제거
    """X의 각 행을 w로 투영시키고 각 행에서 투영시킨 값을 빼줌"""
    return [remove_projection_from_vector(x_i, w) for x_i in X]

print(remove_projection(X, first_principal_component(X))) # 이 결과 원래 데이터에서 제 1주성분 제거한 후 남아 있는 데이터가 나옴(책의 그림 10-8 참고)

# 3) 제2주성분 구하기
print(first_principal_component(remove_projection(X, first_principal_component(X)))) # 이 결과 2번째 주성분을 구할 수 있다


""" 4. 반복적인 과정을 통해 원하는 만큼 많은 주성분 구하기 """

# 1) num of components는 반환할 주성분의 개수를 뜻함
def principal_component_analysis(X, num_components):
    components = []
    for _ in range(num_components):
        component = first_principal_component(X)
        components.append(component)
        X = remove_projection(X, component)

    return components

print(principal_component_analysis(X, 1)) # 숫자 2 이므로 주성분 2개를 반환

# 2) 주어진 데이터를 저차원 공간에서 생성 시키기
# 벡터 하나를 하나의 주성분 위에다 투영
def transform_vector(v, components):
    return [dot(v, w) for w in components]

print(transform_vector([20.9666776351559,-13.1138080189357], [[0.8483153298152323, -0.529491360836486]]))
# X의 가장 첫 원소를 제 1 주성분 위에다 투영했을 경우
# 이는 3에서 구한 magnitude(project([20.9666776351559,-13.1138080189357], first_principal_component(X)))의 값과 같다

# 데이터 전체를 여러 주성분 위에다 투영
def transform(X, components):
    return [transform_vector(x_i, components) for x_i in X]

print(transform(X, [[0.8483153298152323, -0.529491360836486], [0.5294915482155131, 0.8483152128591935]]))
# 데이터 전체를 앞에서 구한 두 개의 주성분 위에다 투영했을 경우
# [X의 첫번째 원소를 제 1 주성분 위에 투영햇을 경우, X의 첫번째 원소를 제 2 주성분 위에다 투영했을 경우] 와 같은 순서쌍으로 나온다.
