# Segunda parte: Aplicaciones en Python

"""
Explanation: Primera parte: Concimiento básico de Algebra Lineal
Pregunta 1:¿Por qué una matriz equivale a una transformación lineal entre espacios vectoriales?
Porque na matriz realiza las operaciones básicas de suma, resta y multiplicación sobre los vectores canonicos
Al aplicar una matriz a un vector estoy obteniendo los cambios resultantes de estas operaciones sobre el vectr.
Pregunta 2:¿Cuál es el efecto de transformación lineal de una matriz diagonal y el de una matriz ortogonal?
Una matriz diagonal es un reescalamiento mientras que una ortogonal es una rotación.
Pregunta 3:¿Qué es la descomposición en valores singulares de una matriz?
La descomposición en valores singulares de una matriz es una aproximación a 
$A = UDV^{t}$
Donde: $U$ y $V$ son los valores singulares de $A$ y $D$ es una matriz diagonal. 
Pregunta 4: ¿Qué es diagonalizar una matriz y que representan los eigenvectores?
La diagonalización de una matriz cuadrada $A_{m x m}$ es llevar el sistema de ecuaciones a la forma canonica, es decir encontrar sus eigenvectores.
Los eigenvectores representan los valores propios de la ecuación.
Pregunta 5: Intuitivamente  qué son los eigenvectores?
Los eigenvectores son la base ortogonal del espacio de la matriz. Ellos hacen las rotaciones y son reescalados por lambda.
Pregunta 6: Cómo interpretas la descomposición en valores singulares como una composición de tres tipos de transformaciones lineales simples?
La descomposición SVD lo que hace es rotar, reescalar y volver a rotar, siendo que en el caso de que la matriz A sea cuadrada, la segunda rotación llevará a la matriz a su posición original.
Pregunta 7: ¿Qué relación hay entre la descomposición en valores singulares y la diagonalización?
Los elementos $U,D,V$ ,resultantes de una descomposición en valores singulares, son los eigenvectores de la matriz A. Donde $D$ es una diagonalizacion.
Pregunta 8:¿Cómo se usa la descomposición en valores singulares para dar un aproximación de rango menor a una matriz?
En la descomposición $A = UDV^{t}$ , $D$ puede verse como una multiplicacion de matrices menores $\begin{bmatrix}D_{1} & 0\0 & D_{2}\end{bmatrix}$ con el rango de D_{1} y D_{2} menor al rango de D.
Entonces $A^{}=U_{1}D_{1}V_{1}^{t}$ aquel que resuelve el problema de $ min  ||A-A^{}||$  s.a. $rank(A^{*})<rank(A)$
Pregunta 9: Describe el método de descenso gradiente
El metodo de optimización de la función $f(x)$ por descenso gradiente busca, de manera local:
1)encontrar los valores $x^{}$ tal que $\nabla f(x^)$ sea la dirección de mayor decenso(o asenso en caso de maximizar) 
2)moverse en la dirección encontrada en un factor $\beta$ conocido como coeficiente de aprendizaje (o learning rate) de manera que $\hat{x}=x-\beta*\nabla f(x)$
Pregunta 10: Menciona dos problemas de optimizacion con restricciones y dos sin restricciones que te parezcan interesantes como científico de datos
Sin restricciones:
1) Maximizar el tiempo de uso de cierta aplicación móvil (un juego) y el life spam de esta.
2) Maximizar la probabilidad de encontrar vida en otros planetas.
Con restricciones
1) Mejorar el acceso a servicios públicos mejorando la planeación urbana.
2) Optimización de despliegue de paneles solares o molinos para energía eolica dada una superficie limitada y condiciones climatológicas
End of explanation
"""


%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time

from PIL import Image

##Aquí abro la imagen y la convierto a gris
im = Image.open("/Users/usuario/Documents/MaestriaCD/Propedeutico/PropedeuticoDataScience2017/Tarea/Simpsons.png")
gris =im.convert("1")

plt.figure(figsize=(9, 6))



##convierto los datos de la imagen en una matriz con valores
mat_val = np.array(gris)


plt.title("Imagen Original")
plt.imshow(mat_val, cmap='gray');


###hago el SVD
U, D, V = np.linalg.svd(mat_val)

##Reconstruyo y grafíco la imagen de manera completa
img_rec = np.matrix(U) * np.diag(D) * np.matrix(V)

plt.title("Imagen reconstruida de manera completa")
plt.imshow(img_rec, cmap='gray');

##Escogiendo K=100 para reconstruir la imagen con solo K elementos del SVD
k=100

rec =np.matrix(U[:,:k]) *np.diag(D[:k])* np.matrix (V[:k,:])
plt.title("Imagen reconstruida con  100  valores")
plt.imshow(rec, cmap='gray')


##Escogiendo K=150 para reconstruir la imagen con solo K elementos del SVD
k=150

rec =np.matrix(U[:,:k]) *np.diag(D[:k])* np.matrix (V[:k,:])
plt.title("Imagen reconstruida con  150  valores")
plt.imshow(rec, cmap='gray')


##Escogiendo K=200 para reconstruir la imagen con solo K elementos del SVD
k=200

rec =np.matrix(U[:,:k]) *np.diag(D[:k])* np.matrix (V[:k,:])
plt.title("Imagen reconstruida con  200  valores")
plt.imshow(rec, cmap='gray')


"""
Explanation: Se mostrará una aplicación de la SVD a la compresión de imágenes y reducción de ruido.
End of explanation
"""


import numpy as np 

def pseudo (A):
    
    X =np.array(A)
    U, D, V = np.linalg.svd(X, full_matrices=False)
    
    V_t = np.transpose(V)
    U_t = np.transpose(U)
    D_diag=np.diag(D)
    rows, col =D_diag.shape
    D_inv = np.zeros((rows,col))
    
    ##Aquí calculo la inversa de D, invirtiendo los valores y poniendo 0 en vez de 1/0
    
    for i in range(0,max(rows,col)):
        if D_diag[i,i]!= 0 :
            D_inv[i,i]=1/D_diag[i,i]
        else :
            D_inv[i,i]= 0
            
    ##aquí reconstruyo la pseudoinversa de A
    pseudo = np.dot(np.dot(V_t, D_inv), U_t)
    
    return pseudo

"""
Explanation: Pregunta: Qué tiene que ver este proyecto con compresión de imágenes?
Se puede utilizar la descomposición SVD con una aproximacion de grado K optima para guardar
la imagen en un tamaño menor al original, pudiendo despues reconstruirla a partir de los elementos  de SVD.
Pseudoinversa y sistemas de ecuaciones.
Hacer una función que me de la pseudoinversa de una matriz A
End of explanation
"""



def solve (A,y):
    A= np.array(A)
    Y=np.array(y)
    
    ##  reviso el tamaño
    rows, col =A.shape
    vec_rows, vec_col = Y.shape   
    
    if rows != vec_rows:
        raise Exception ("El tamaño de la matriz y el vector no coinciden")
    else:
        inv=pseudo(A)
        solve= np.dot(inv, Y)
        
    return (solve)

A=[[1,1],[0,0]]
b=[[1],[1]]
solve(A,b)

pseudo(A)

"""
Explanation: Hacer una función que me resuelva un sistema de ecuaciones Ax=b
End of explanation
"""


import math
A=[[1,1],[0,1*math.exp(-32)]]

print(pseudo(A))

solve(A,b)

"""
Explanation: A es una matriz es que representa al sistema de ecuaciones $ X_{1}+X_{2}=b_{1}$ y $b_{2}=0$, su imagen es $b = \begin{bmatrix}b_{1}\0 \end{bmatrix}$. La solución no es única ya que hay una variable libre.
Dada la forma de la pseudoinversa de A $\begin{bmatrix}.5 & 0\.5 & 0 \end{bmatrix}$ la función siempre regresará una respuesta $\begin{bmatrix}x_{1}=b_{1}/2\x_{2}=b_{2}/2 \end{bmatrix}$ aunque $b_{2} != 0$
End of explanation
"""


##Programando un script para descargar el archivo .csv de Github y convertirlo en un data frame

import numpy as np
import pandas as pd
import statsmodels.formula.api as sm
import matplotlib.pyplot as plt


#Script para descargar archivo y convertirlo en Data Frame con Pandas

#url="/Users/usuario/Documents/MaestriaCD/Propedeutico/PropedeuticoDataScience2017/study_vs_sat.csv"
url="https://raw.githubusercontent.com/mauriciogtec/PropedeuticoDataScience2017/master/Tarea/study_vs_sat.csv"
data = pd.read_csv(url)

data=pd.read_csv(url)
data= pd.DataFrame(data)

"""
Explanation: En este caso la matriz tiene una solución única ya que $b_{2}=exp-32$ y las x´s son diferentes a las del caso anterior.
OLS
End of explanation
"""



##Deefine una función que me de una predicción para cada valor de sat_score
##

def prediction (S,alpha,beta):
    
    prediction=np.zeros(len(data))
    
    for i in range(len(data)):
        score=S[i]
        prediction[i]=alpha+beta*score
      
    return prediction

##puedo usar datos que se me ocurran

alpha=-353.164879499
beta= 25.3264677779 
S=data["study_hours"]


##Entonces usando esta información puedo hacer la predicción usando mi función

score_pred=prediction(S,alpha,beta)
print(score_pred)



##Definiendo el numpy array con 1 en el primer vector
##y sat_score en el segundo

Origen= data['Origen'] = np.ones(( len(data), ))
X=data[["Origen","study_hours"]]
print(X)


###Calculando X^+ * study_hours para obtener la alpha y beta
X_inv=pseudo(X)
alpha_aprox,beta_aprox=np.dot(X_inv,data["sat_score"])
print(alpha_aprox, beta_aprox)


##ahora, haciendo la formula de estimadores de OLS de veras para calcular alpha t beta
X_t=np.transpose(X)
Sxx=np.dot(X_t,X)
Sxy=np.dot(X_t,data["sat_score"])
Sxx_inv= pseudo(Sxx)

alpha,beta= np.dot(Sxx_inv,Sxy)

print(alpha,beta)



##visualizando los datos correctos vs las aproximaciones

alpha=353.164879499
beta= 25.3264677779 
S=data["study_hours"]

prediccion=prediction(S, alpha,beta)


colors = ['red',  'blue']

pred=plt.plot(prediccion, 'bo', markersize=10)  # blue circle with size 10 
val=plt.plot(data["sat_score"], 'ro', ms=10,)
plt.legend((pred, val),
           ('Valores de la predicción',"Valor Real"),
           scatterpoints=1,
           loc='lower left',
           ncol=3,
           fontsize=8)

plt.show()


"""
Explanation: Al hacer una aproximación de la forma $sat_score= \alpha + \betastudy_hours + \epsilon$ podemos plantear la minimización de errores al cuadrado de tal manera que $$min \epsilon^{2} = min_{\alpha,\hat{\beta}} satscore - \alpha-\hat{\beta}study hours$$
$$min_{\alpha,\hat{\beta}} y - X\hat{\beta}$$
En este caso $\alpha$ y $\beta$ resultantes de las condiciones de primer orden del problema de minimización son los gradientes que estamos buscando
End of explanation
"""


