Tarea 1: Creando una sistema de Álgebra Lineal

En esta tarea seran guiados paso a paso en como realizar un sistema de arrays en Python para realizar operaciones de algebra lineal.

Pero antes... (FAQ)

Como se hace en la realidad? En la practica, se usan paqueterias funcionales ya probadas, en particular numpy, que contiene todas las herramientas necesarias para hacer computo numerico en Python.

Por que hacer esta tarea entonces? Python es un lenguage disenado para la programacion orientada a objetos. Al hacer la tarea desarrollaran experiencia en este tipo de programacion que les permitira crear objetos en el futuro cuando lo necesiten, y entender mejor como funciona numpy y en general, todas las herramientas de Python. Ademas, en esta tarea tambien aprenderan la forma de usar numpy simultaneamente.

Como comenzar con numpy? En la tarea necesitaremos importar la libreria numpy, que contiene funciones y clases que no son parte de Python basico. Recuerden que Python no es un lenguage de computo cientifico, sino de programacion de proposito general. No esta disenado para hacer algebra lineal, sin embargo, tiene librerias extensas y bien probadas que permiten lograrlo. Anaconda es una distribucion de Python que ademas de instalarlo incluye varias librerias de computo cientifico como numpy. Si instalaron Python por separado deberan tambien instalar numpy manualmente.

Antes de comenzar la tarea deberan poder correr:


In [62]:
import numpy as np

Lo que el codigo anterior hace es asociar al nombre np todas las herramientas de la libreria numpy. Ahora podremos llamar funciones de numpy como np.<numpy_fun>. El nombre np es opcional, pueden cambiarlo pero necesitaran ese nombre para acceder a las funciones de numpy como <new_name>.<numpy_fun>. Otra opcion es solo inlcuir import numpy, en cuya caso las funciones se llaman como numpy.<numpy_fun>. Para saber mas del sistema de modulos pueden revisar la liga https://docs.python.org/2/tutorial/modules.html

I. Creando una clase Array

Python incluye nativo el uso de listas (e.g. x = [1,2,3]). El problema es que las listas no son herramientas de computo numerico, Python ni siquiera entiende una suma de ellas. De hecho, la suma la entiende como concatenacion:


In [63]:
x = [1,2,3]
y = [4,5,6]
x + y


Out[63]:
[1, 2, 3, 4, 5, 6]

Vamos a construir una clase Array que incluye a las matrices y a los vectores. Desde el punto de vista computacional, un vector es una matriz de una columna. En clase vimos que conviene pensar a las matrices como transformacion de vectores, sin embargo, desde el punto de vista computacional, como la regla de suma y multiplicacion es similar, conviene pensarlos ambos como arrays, que es el nombre tradicional en programacion

Computacionalmente, que es un array? Tecnicamente, es una lista de listas, todas del mismo tamano, cada uno representando una fila (fila o columna es optativo, haremos filas porque asi lo hace numpy, pero yo previero columnas). Por ejemplo, la lista de listas

[[1,2,3],[4,5,6]]

Corresponde a la matriz $$ \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} $$

The numpy way


In [64]:
B = np.array([[1,2,3], [4,5,6]]) # habiendo corrido import numpy as np

Es posible sumar matrices y multiplicarlas por escalares


In [65]:
B + 2*B # Python sabe sumar y multiplicar arrays como algebra lineal


Out[65]:
array([[ 3,  6,  9],
       [12, 15, 18]])

Las matrices de numpy se pueden multiplicar con la funcion matmul dentro de numpy


In [66]:
np.matmul(B.transpose(), B) # B^t*B


Out[66]:
array([[17, 22, 27],
       [22, 29, 36],
       [27, 36, 45]])

Los arrays the numpy pueden accesarse con indices y slices

Una entrada especifica:


In [67]:
B[1,1]


Out[67]:
5

Una fila entera:


In [68]:
B[1,:]


Out[68]:
array([4, 5, 6])

Una columna entera:


In [69]:
B[:,2]


Out[69]:
array([3, 6])

Un subbloque (notar que un slice n:m es n,n+1,...,m-1


In [70]:
B[0:2,0:2]


Out[70]:
array([[1, 2],
       [4, 5]])

En numpy podemos saber la dimension de un array con el campo shape de numpy


In [71]:
B.shape


Out[71]:
(2, 3)

Numpy es listo manejando listas simples como vectores


In [72]:
vec = np.array([1,2,3])
print(vec)


[1 2 3]

Comenzando desde cero...


In [73]:
class Array:
    "Una clase minima para algebra lineal"    
    def __init__(self, list_of_rows): 
        "Constructor"
        self.data = list_of_rows
        self.shape = (len(list_of_rows), len(list_of_rows[0]))

In [74]:
A = Array([[1,2,3], [4,5,6]])
A.__dict__ # el campo escondido __dict__ permite acceder a las propiedades de clase de un objeto


Out[74]:
{'data': [[1, 2, 3], [4, 5, 6]], 'shape': (2, 3)}

In [75]:
A.data


Out[75]:
[[1, 2, 3], [4, 5, 6]]

In [76]:
A.shape


Out[76]:
(2, 3)

El campo data de un Array almacena la lista de listas del array. Necesitamos implementar algunos metodos para que sea funcional como una clase de algebra lineal.

  1. Un metodo para imprimir una matriz de forma mas agradable
  2. Validador. Un metodo para validar que la lista de listas sea valida (columnas del mismo tamano y que las listas interiores sean numericas
  3. Indexing Hacer sentido a expresiones A[i,j]
  4. Iniciar matriz vacia de ceros Este metodos es muy util para preacolar espacio para guardar nuevas matrices
  5. Transposicion B.transpose()
  6. Suma A + B
  7. Multiplicacion escalar y matricial 2 A y AB
  8. Vectores (Opcional)

Con esto seria posible hacer algebra lineal

Metodos especiales de clase...

Para hacer esto es posible usar metodos especiales de clase __getitem, __setitem__, __add__, __mul__, __str__. Teoricamente es posible hacer todo sin estos metodos especiales, pero, por ejemplo, es mucho mas agradable escribir A[i,j] que A.get(i,j) o A.setitem(i,j,newval) que A[i,j] = newval.

1. Un metodo para imprimir mejor...

Necesitamos agregar un metodo de impresion. Noten que un array de numpy se imprime bonito comparado con el nuestro


In [77]:
Array([[1,2,3], [4,5,6]])


Out[77]:
<__main__.Array at 0x104eeac88>

In [78]:
print(Array([[1,2,3], [4,5,6]]))


<__main__.Array object at 0x104eeacc0>

In [79]:
np.array([[1,2,3], [4,5,6]])


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

In [80]:
print(np.array([[1,2,3], [4,5,6]]))


[[1 2 3]
 [4 5 6]]

Por que estas diferencias? Python secretamente busca un metodo llamado __repr__ cuando un objeto es llamado sin imprimir explicitamente, y __str__ cuando se imprime con print explicitamente. Por ejemplo:


In [81]:
class TestClass:
    def __init__(self):
        pass # this means do nothing in Python
    def say_hi(self):
        print("Hey, I am just a normal method saying hi!")
    def __repr__(self):
        return "I am the special class method REPRESENTING a TestClass without printing"
    def __str__(self):
        return "I am the special class method for explicitly PRINTING a TestClass object"

In [82]:
x = TestClass()

In [83]:
x.say_hi()


Hey, I am just a normal method saying hi!

In [84]:
x


Out[84]:
I am the special class method REPRESENTING a TestClass without printing

In [85]:
print(x)


I am the special class method for explicitly PRINTING a TestClass object

Ejercicios


In [126]:
class Array:
    "Una clase minima para algebra lineal"    
    def __init__(self, list_of_rows): 
        "Constructor y validador"
        # obtener dimensiones
        self.data = list_of_rows
        nrow = len(list_of_rows)
        #  ___caso vector: redimensionar correctamente
        if not isinstance(list_of_rows[0], list):
            nrow = 1
            self.data = [[x] for x in list_of_rows]
        # ahora las columnas deben estar bien aunque sea un vector
        ncol = len(self.data[0])
        self.shape = (nrow, ncol)
        # validar tamano correcto de filas
        if any([len(r) != ncol for r in self.data]):
            raise Exception("Las filas deben ser del mismo tamano")
                    
    # Ejercicio 1
    def __repr__(self):
        str2print = "Array"   
        for i in range(len(self.data)):
            if(i==0):
                str2print += str(self.data[i]) + "\n"
            if(i>0):
                str2print += "     " + str(self.data[i]) + "\n" 
        return str2print
    
    def __str__(self):
        str2print = ""   
        for i in range(len(self.data)):
            str2print += str(self.data[i]) + "\n"
        return str2print
    
    #Ejercicio2
    def __getitem__(self, idx):
        return self.data[idx[0]][idx[1]]
    
    
    def __setitem__(self, idx, valor):
        self.data[idx[0]][idx[1]] = valor
        
    # Ejercicio 3
    def zeros(x, y):
        array_de_ceros = Array([[0 for col in range(y)] for row in range(x)])
        return array_de_ceros
        
    def eye(x):
        array_eye = Array([[0 for col in range(x)] for row in range(x)])  
        for i in range(x):
            for j in range(x):
                if i == j:
                    array_eye[i,j] = 1
        return array_eye
    
    # Ejercicio 4
    def transpose(self):
        #Obtener dimensiones
        num_row = len(self.data)
        num_col = len(self.data[0])  
        #Crear matriz receptora
        mat_transpuesta = Array([[0 for col in range(num_row)] for row in range(num_col)])
        #Transponer
        for i in range(num_row):
            for j in range(num_col):
                mat_transpuesta[j,i] = self.data[i][j]         
        return mat_transpuesta
    
    def __add__(self, other):
        "Hora de sumar"
        if isinstance(other, Array):
            if self.shape != other.shape:
                raise Exception("Las dimensiones son distintas!")
            rows, cols = self.shape
            newArray = Array([[0. for c in range(cols)] for r in range(rows)])
            for r in range(rows):
                for c in range(cols):
                    newArray.data[r][c] = self.data[r][c] + other.data[r][c]
            return newArray
        elif isinstance(2, (int, float, complex)): # en caso de que el lado derecho sea solo un numero
            rows, cols = self.shape
            newArray = Array([[0. for c in range(cols)] for r in range(rows)])
            for r in range(rows):
                for c in range(cols):
                    newArray.data[r][c] = self.data[r][c] + other
            return newArray
        else:
            return NotImplemented # es un tipo de error particular usado en estos metodos
        
    #Ejercicio 5
    ##No me salió :(

    #Ejercicio 6
    def __mul__(self, other):
        if isinstance(other, Array):
            #Validar las dimensiones
            if self.shape[1] != other.shape[0]:
                raise Exception("Las matrices no son compatibles!")
            #Obtener las dimensiones
            num_rowsA = self.shape[0]
            num_rowsB = other.shape[0]
            num_colsB = other.shape[1]
            #Crear matriz receptora
            newArray = Array([[0 for col in range(num_colsB)] for row in range(num_rowsA)])
            #Multiplicar
            for i in range(num_rowsA):
                for j in range(num_colsB):
                    for k in range(num_rowsB):
                        newArray[i,j] = newArray[i,j] + self.data[i][k] * other.data[k][j]
            return newArray
        #Matriz, entero
        elif isinstance(other, (int, float, complex)):
            #Obtener las dimensiones
            rows, cols = self.shape
            #Crear matriz receptora
            newArray = Array([[0 for col in range(cols)] for row in range(rows)])
            #Multiplicar
            for row in range(rows):
                for col in range(cols):
                    newArray.data[row][col] = self.data[row][col] * other
            return newArray
        else:
            return NotImplemented
        
    def __rmul__(self, other):
        if isinstance(other, (int, float, complex)):
            rows, cols = self.shape
            newArray = Array([[0 for col in range(cols)] for row in range(rows)])
            for row in range(rows):
                for col in range(cols):
                    newArray.data[row][col] = self.data[row][col] * other
            return newArray
        else:
            return NotImplemented

Prueba de las clases


In [127]:
X = Array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]])

In [128]:
X


Out[128]:
Array[1, 2, 3, 4, 5]
     [6, 7, 8, 9, 10]
     [11, 12, 13, 14, 15]

In [129]:
print(X)


[1, 2, 3, 4, 5]
[6, 7, 8, 9, 10]
[11, 12, 13, 14, 15]


In [130]:
X[0,2]


Out[130]:
3

In [131]:
X


Out[131]:
Array[1, 2, 3, 4, 5]
     [6, 7, 8, 9, 10]
     [11, 12, 13, 14, 15]

In [132]:
X[0,0] = 10

In [133]:
X


Out[133]:
Array[10, 2, 3, 4, 5]
     [6, 7, 8, 9, 10]
     [11, 12, 13, 14, 15]

In [134]:
Array.zeros(5,5)


Out[134]:
Array[0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0]

In [135]:
Array.eye(4)


Out[135]:
Array[1, 0, 0, 0]
     [0, 1, 0, 0]
     [0, 0, 1, 0]
     [0, 0, 0, 1]

In [136]:
X.transpose()


Out[136]:
Array[10, 6, 11]
     [2, 7, 12]
     [3, 8, 13]
     [4, 9, 14]
     [5, 10, 15]

In [137]:
B = Array.eye(5)

In [138]:
B


Out[138]:
Array[1, 0, 0, 0, 0]
     [0, 1, 0, 0, 0]
     [0, 0, 1, 0, 0]
     [0, 0, 0, 1, 0]
     [0, 0, 0, 0, 1]

In [139]:
X*B


Out[139]:
Array[10, 2, 3, 4, 5]
     [6, 7, 8, 9, 10]
     [11, 12, 13, 14, 15]