import pandas as pd

# importar numpy para usarlo en los ejemplos
import numpy as np

"""
Explanation: Pandas: DataFrames
Se importa la biblioteca pandas, para disponer de todos los objetos aquí definidos:
End of explanation
"""


# Se crean un par de series. Observar que no es necesario que los indices coincidan
s1 = pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd'])
s2 = pd.Series(np.random.randn(4), index=['b', 'c', 'd', 'e'])

# Se crea un diccionario con las 2 columnas (Series)
dict = { 'one': s1, 'two': s2 }

# Se crea un objeto DataFrame a partir de estos datos
df1 = pd.DataFrame(dict)
df1

"""
Explanation: DataFrames
Un DataFrame es una generalización del concepto de Serie. Contiene un índice (numérico, nominal, fechas, etc.) y varias series cuyos datos se 'alinean' de acuerdo con los índices. Se puede asimilar al concepto de una tabla de varias columnas, donde los índices referencian las filas y los títulos de las columnas las series.
En el lenguaje del aprendizaje de máquina a las columnas se les denomina características (features) del dataset.
Siendo una colección de series, una primera forma de instanciar un DataFrame es a partir de varias series, por ejemplo:
End of explanation
"""


df2 = pd.DataFrame( np.random.randn(7,4), columns=[ 'arbol', 'fruta', 'flor', 'raiz' ] )
df2

"""
Explanation: Observar que a los indices no presentes en alguna de las series se les asigna el valor NaN (Not a Number)
Otra alternativa es crear el DataFrame a partir de un ndarray de numpy. En este caso los nombres de las columnas se deben especificar por medio del parametro columns. Por ejemplo:
End of explanation
"""


dict = {'one' : [1., 2., 3., 4.],
     'two' : [4., 3., 2., 1.]}
df3 = pd.DataFrame(dict, index=['qwe','rty','uio','pas'])
df3

"""
Explanation: Se puede crear a partir de un diccionario de listas o de ndarrays. El argumento index permite especificar los indices de la tabla.
End of explanation
"""


listdict = [ {'b':1,'c':2,'d':3}, {'a':1.1,'c':1.2,'d':1.3}, {'c':2.1,'d':2.2,'b':2.2}, {'a':0,'b':0.1,'c':0.2,'d':0.3} ]
df4 = pd.DataFrame(listdict, index=['2017-10-01','2017-11-01','2017-12-01','2018-01-01'])
df4

"""
Explanation: o asi mismo de una lista de diccionarios. El argumento index permite especificar los indices de la tabla.
End of explanation
"""


# Tipo de dato de cada columna
df4.dtypes

# Los valores del dataframe. Observar que retorna un ndarray
df3.values

# Nombres de las columnas
df4.columns

# Indices de la tabla
df4.index

# Esta vacia la estructura?
df4.empty

# Obtener el tamaño del DataFrame
df4.size

# Obtener la forma del DataFrame
df4.shape

"""
Explanation: Observar en el ejemplo anterior como los datos se alinean por los nombres de las columnas.
Atributos de los DataFrames
End of explanation
"""


df3.get('one')

df3['one']

"""
Explanation: Acceso a los elementos de un DataFrame
Se puede utilizar el método .get o la notación de indices, por ejemplo:
End of explanation
"""


df4[ ['b','c'] ]

"""
Explanation: Se puede recuperar un subconjunto de columnas
End of explanation
"""


df4.loc[ ['2017-12-01', '2017-10-01'], :]

"""
Explanation: Recuperar un subconjunto de filas (índices)
End of explanation
"""


df4.loc[ ['2017-12-01', '2017-10-01'], ['d','b'] ]

"""
Explanation: O que tal un subconjunto de filas y de columnas
End of explanation
"""


df4.iloc[0,0], df4.iloc[2,3]

"""
Explanation: Tambien es posible referenciar los elementos del DataFrame por el numero de fila y columna que les corresponde
End of explanation
"""


df2.iloc[5:,:]

df2.iloc[3:,:2]

df2.iloc[4:6, 1:2]

"""
Explanation: Por rangos de numeros de filas o de columnas
End of explanation
"""


df2.at[4,'fruta'] = 555
df2.iat[4,0] = 666
df2.loc[4,:]

"""
Explanation: Para asignar valores a una celda del dataframe, se puede utilizar .at con los nombres de indices y columnas, o .iat con las posiciones numericas correspondientes
End of explanation
"""


df5 = df4.copy()
df5

"""
Explanation: Hacer una copia de un DataFrame
End of explanation
"""


df5.fillna(value=-999, inplace=True)
df5

"""
Explanation: Reemplazar los NaN por un valor deseado. El parametro inplace=True hace que se modifique el dataframe, en lugar de devolver una copia con los cambios.
End of explanation
"""


df5 = df4.copy()
df5.dropna(inplace=True)
df5

"""
Explanation: Otra alternativa para manejar la presencia de NaNs es suprir las filas,columnas donde aparezcan
End of explanation
"""


df4.drop(['2017-12-01', '2017-11-01'])

"""
Explanation: Para eliminar algunas filas del DataFrame se utiliza el método .drop
End of explanation
"""


df4.head(2)

df4.tail(3)

"""
Explanation: Observar en el ejemplo anterior que toda fila o toda columna que contenga un NaN es eliminada.
Se pueden recuperar los elementos del comienzo o del final del DataFrame
End of explanation
"""


df4[ df4>2 ]

df4.isnull()

# Otra forma de hacer el .fillna
df4[ df4.isnull() ] = 0
df4

# .all aplica la condicion booleana a todos los elementos de cada columna
(df4>0).all()

# .any comprueba la condicion boolean si al menos un elemento de cada serie la cumple
(df4==0).any()

"""
Explanation: Indexado lógico
En forma similar a como lo permiten las series es posible indexar los elementos de un DataFrame usando condiciones lógicas. Por ejemplo:
End of explanation
"""


df5**2 - df4/2

"""
Explanation: Operaciones aritméticas
Para DataFrames cuyos elementos son numéricos, se dispone de todos los metodos para realizar las operaciones aritmeticas estandar: .add .sub .mul .div .pow .mod .abs .dot . Asi mismo, se pueden utilizar los operadores sobrecargados: + - * / **
End of explanation
"""


df4<=df4**2

"""
Explanation: Operadores relacionales
Los operadores relacionales se encuentran disponibles como métodos o a través de los operadores sobrecargados. Los métodos son .eq .ne .ge .gt .le .lt y sus correspondientes operadores == != >= > <= <
End of explanation
"""


df4.describe()

df4.mean()

df4.var()

df4.cov()

"""
Explanation: Operaciones estadísticas
Las operaciones estadísticas con un DataFrame son: .describe .count .mix .max .mean .median .mode .var .std .cov .corr .sem
End of explanation
"""


df4.to_csv('Dataframe4.csv')
df6 = pd.read_csv('Dataframe4.csv', index_col=0)
df6

"""
Explanation: Notar que efectivamente la diagonal de la matriz de covarianza son las varianzas
Operaciones de entrada/salida
Salvar, recupear un DataFrame desde un archivo separado por comas
End of explanation
"""


import matplotlib.pyplot as plt
df6.plot()
plt.show()

"""
Explanation: Se puede obtener la grafica de las series que componen el DataFrame en MatPlotLib
End of explanation
"""


df6.hist()
plt.show()

"""
Explanation: Obtener el histograma de cada serie
End of explanation
"""


t = pd.read_excel('titanic.xls')
t.shape

t.head()

"""
Explanation: Leer tablas Excel
Pandas también ofrece métodos para leer tablas de Excel. Para este fin se debe tener instalado el módulo xlrd.
End of explanation
"""


indices = np.arange( '2017-01-01', '2017-01-05', dtype=np.datetime64 )
df1 = pd.DataFrame(
    { 'col1':pd.Series([10,20,30,40], index=indices),
      'col2':pd.Series([50,60,70,80], index=indices) } )
df1

df2 = pd.DataFrame(
    { 'col3':pd.Series([70,80,90,100], index=np.arange( '2017-01-02', '2017-01-06', dtype=np.datetime64 ) ) } )
df2

"""
Explanation: Operación join
La operación join permite combinar dos dataframes alineando sus indices. Un left-join preserva los índices del DataFrame izquierdo, un right-join preserva los índices del DataFrame derecho, un inner-join preserva la intersección de índices de ambos DataFrames y el outer-join preserva la union de los índices.
Considere los siguientes DataFrames que no coinciden en todos sus índices:
End of explanation
"""


left = df1.join(df2)
left

right = df1.join(df2, how='right')
right

inner = df1.join(df2, how='inner')
inner

outer = df1.join(df2, how='outer')
outer

"""
Explanation: Los distintos tipos de join serían
End of explanation
"""


pd.concat( [df1,df2] )

indices = np.arange( '2017-01-05', '2017-01-08', dtype=np.datetime64 )
df3 = pd.DataFrame(
    { 'col1':pd.Series([41,42,43], index=indices),
      'col2':pd.Series([81,82,83], index=indices) } )
df3

pd.concat( [df1,df3] )

"""
Explanation: Concatenación de DataFrames
La operación de concatenación permite agregar filas a una tabla, alineando las columnas. Acepta una lista de DataFrames a concatenar.
End of explanation
"""


df1.append([df2,df3])

"""
Explanation: La operación append también realiza una función similar a la concatenación
End of explanation
"""


indices = np.arange( '2017-01-01', '2017-01-05', dtype=np.datetime64 )
df1 = pd.DataFrame(
    { 'col1' :pd.Series([10,20,30,40], index=indices),
      'col2' :pd.Series([50,60,70,80], index=indices),
      'llave':pd.Series([1,2,3,4],     index=indices) } )
df1

indices = np.arange( '2017-01-02', '2017-01-06', dtype=np.datetime64 )
df2 = pd.DataFrame(
    { 'col3' :pd.Series([70,80,90,100], index=indices ),
      'llave':pd.Series([2,1,4,3],     index=indices)    } )
df2

pd.merge(df1, df2, on='llave')

"""
Explanation: Merge
La operación merge es una generalización del join donde se utiliza un atributo cualquiera para efecto de alinear las filas de las tablas. Los índices se ignoran y son descartados por la operación merge.
End of explanation
"""


