import numpy as np

"""
Explanation: numpy: Manipulación de Vectores y Matrices
La biblioteca numpy se importa
End of explanation
"""


# Una matriz fila (matriz 1x3)
a = np.array( [ [3,2,1] ] )
type(a), a.size, a.ndim, a.shape, a.dtype, a.data

a

# Para un vector se excluyen los cochetes externos
a2 = np.array([3,2,1])
type(a2), a2.size, a2.ndim, a2.shape, a2.dtype

a2

# Agregando un eje a un vector shape=(3,) se convierte en una matriz con shape=(3,1)
a2[:, np.newaxis]

# Una matriz 2x3
b = np.array( [ [4.,5,6], 
                 [7,8,9] ] )
type(b), b.size, b.ndim, b.shape, b.dtype

# Tambien se puede utilizar una lista de tuplas
b2 = np.array( [ (4.,5,6), (7,8,9) ] )
type(b2), b2.size, b2.ndim, b2.shape, b2.dtype

# Un vector a partir de un rango de valores
c = np.arange(10)
type(c), c.size, c.ndim, c.shape, c.dtype

c

# El metodo tile permite crear una matriz replicando los elementos
np.tile(c, (2,1))

# Un vector a partir de un rango indicando inicio, fin y paso
d = np.arange(10,50,4)
d

"""
Explanation: se acostumbra llamarla np para facilitar la escritura de referencias a los objetos que contiene la librería.
Vectores y matrices
El objeto más importante en numpy es el arreglo n-dimensional (ndarray) que permite representar facilmente vectores, matrices y tensores.
Se puede crear un ndarray a partir de listas, por ejemplo:
End of explanation
"""


# Observar que se incluyen los extremos
e = np.linspace(10,50,12)
e

# O en caso de necesitar construir una escala logaritmica
f = np.logspace(1,3,10) # desde 10**1 hasta 10**3
f

"""
Explanation: La función linspace permite crear vectores dividiendo un intervalo uniformemente
End of explanation
"""


d.reshape((2,5))

b.reshape((6,1))

"""
Explanation: Un ndarray se puede reorganizar preservando los datos utilizando el método reshape
End of explanation
"""


# La matriz de ceros
np.zeros( (3,4) )

# La matriz de unos
np.ones((2,6))

# Una matriz sin inicializar
np.empty((2,3))

# La matriz identidad
np.eye(5)

"""
Explanation: Existen métodos para instanciar estructuras de uso frecuente
End of explanation
"""


p = np.empty((2,3))
q = np.ones((2,3))

# Apilamiento vertical
np.vstack((p,q))

# Apilamiento horizontal
np.hstack((p,q))

"""
Explanation: Las operaciones de apilamiento (stacking) permiten unir estructuas verticalmente u horizontalmente
End of explanation
"""


# Tipo por defecto: int64
a = np.array( [ [3,2,1] ] )
a.dtype

# Usar otros tipos
a2 = np.array( [ [3,2,1] ], np.int32 )
a2.dtype

a3 = np.array( [ [3,2,1] ], np.float )
a3.dtype

a4 = np.array( [ [3,2,1] ], np.float32 )
a4.dtype

b = np.linspace(1,10,20)
b.dtype

b

b2 = np.linspace(1,10,20, dtype=np.int64)
b2.dtype

# Observar que los valores se redondean hacia abajo
b2

# Los tipos string por defecto utilizan formato unicode. Observar que los numeros se toman como strings
c = np.array( [ ['hola',12], ['adios',21] ] )
c

"""
Explanation: Tipo de datos
NumPy asume un tipo por defecto, basado en los datos con los que se inicializa el arreglo.
Se puede especificar el tipo de dato en caso de desear un tipo particular.
End of explanation
"""


hoy = np.datetime64('2017-09-26')
manana = np.datetime64('2017-09-27')
hoy, manana

dosDias = np.array( [hoy, manana] )
dosDias

unaSemana = np.arange('2017-09-24', '2017-10-01', dtype='datetime64')
unaSemana

# Fecha y hora
ya = np.datetime64('2017-09-26T15:21:01')
ya

# Rango de fechas con pasos de 1 hora
horas = np.arange('2017-09-26T00:00', '2017-09-26T23:59', dtype='datetime64[h]' )
horas

# Un rango de dias
dias = np.arange('2017-02', '2017-03', dtype='datetime64[D]')
dias

# Datos tipo complejo
complejos = np.array([ 1+2j, -1-2j, 3., -7j ])
complejos.dtype

# Datos tipo booleano
booleanos = np.array([ True, False ])
booleanos.dtype

cadenas = np.array(['one','two','three'])
cadenas.dtype

"""
Explanation: El tipo datetime64 permite representar fechas y horas. También se puede utilizar en los métodos arange()
End of explanation
"""


dias[0]

dias[-1]

dias[0:3]

dias[-3:-1]

dias[:4]

dias[-3:]

dias[1:10:2]

"""
Explanation: Indexación & Slicing
Para indicar un rango de indices se utiliza la notación inicio:fin:paso es cada una de las dimensiones. Si se omite el paso se asume que es 1, si se omite el inicio se asume que es desde el indice 0 y si se omite el fin se asume que es hasta la longitud de la correspondiente dimensión. Un índice negativo se asume que se resta de la longitud de la dimensión correspondiente.
Como un primer ejemplo, considerese el arreglo unidimensional dias el ejemplo anterior:
End of explanation
"""


m = np.arange(1,21).reshape((4,5))
m

# Para seleccionar una de las columnas
m[:,2]

# Para seleccionar una de las filas
m[2,:]

# Un rango dentro de la columna
m[1:3,2]

# Un rango dentro de la fila
m[2,3:5]

# El último elemento de la última fila
m[-1,-1]

# Una submatriz
m[2:, 2:4]

# Otra forma de dar la misma submatriz
m[-2:, -3:-1]

"""
Explanation: Considerese ahora una matriz bidimensional 4x5
End of explanation
"""


m[:,[1,3]]

"""
Explanation: Se puede indicar la lista de elementos a tomar de una fila o una columna
End of explanation
"""


m

# Convertir una matriz en un vector .flatten o .ravel
m.flatten()

m.T.ravel()

m.shape

m.reshape((2,10))

# NumPy puede inferir la dimensión faltante
m.reshape((2,-1))

m.T.reshape((10,2))

"""
Explanation: Manipulando la forma de un ndarray
End of explanation
"""


