import numpy as np
import pandas as pd

# Este notebook fue elaborado con la versión 1.0.3 de Pandas
pd.__version__

"""
Explanation: Módulo 2: Pandas, análisis de datos con Python

En este notebook describiremos las características principales, y formas de trabajo con Pandas, la principal biblioteca de análisis de datos del ecosistema Python. Pandas está apoyado en NumPy, la biblioteca de análisis numérico básica de Python (este módulo asume que el lector conoce NumPy).
Referencias:

El notebook está basado principalmente en los tutoriales disponibles en la documentación de Pandas.
Para la comparación con SQL, es muy útil esta sección en particular de la documentación
Muy buenos ejercicios que cubren aspectos aquí presentados (y algunos más).
End of explanation
"""


s= pd.Series(np.random.randn(5), index=['a','b','c','d','e'])
s


"""
Explanation: 1. Series
Una de las estructuras básicas de Pandas es la serie: un array unidimensional etiquetado que puede contener cualquier tipo de datos de Python. Atención: en Pandas los datos y sus etiquetas van siempre juntos, a menos que esa relación se quiebre a propósito. Las etiquetas son llamadas en general index. Las Series pueden crearse a partir de ndarrays, diccionarios de Python o valores escalares.
Las series se comportan de forma muy similar a un ndarray, y son argumentos válidos de la mayoría de las funciones de NumPy. 
Creemos una serie de 5 números aleatorios, cada uno con su etiqueta asociada (el largo del índice debe ser el mismo que el del array). Si no se le indica índice, le va a poner [0, ..., len(data)-1]
End of explanation
"""


d = pd.Series({'b': 1, 'a': 0, 'c': 2})
d

"""
Explanation: También podemos crear una Series a partir de un diccionario de Python. Como no le especificamos índices, se genera a partir de las primeras componentes, ordenadas en el mismo orden de inserción en el diccionario:
End of explanation
"""


s[s > s.median()] # Seleccionamos los valores mayores a la mediana del array. 

"""
Explanation: Podemos especificar un índice para indicar el orden (y para meter elementos inexistentes). La forma estándar en Pandas de especificar la ausencia de datos es vía NaN.
Las series se comportan de forma muy similar a un array y, de hecho, la mayoría de las operaciones con ndarrays admiten series como argumentos (y manejan apropiadamente las etiquetas, para que sigan asociadas luego de realizada la operación):
End of explanation
"""


s['a']

s['e']=12

s

'e' in s

s.get(['f'],np.nan) # Si no ponemos el get, devuelve error

"""
Explanation: Alternativamente, podemos ver a las series como un diccionario (de largo fijo) que puede accederse y cambiar valores a través de su índice:
End of explanation
"""


s[1:] # sin el primer elemento

s[:-1] # sin el último elemento

s[1:]+s[:-1]

"""
Explanation: Al igual que en NumPy, las series admite operaciones vectorizadas. También es interesante ver que las operaciones sobre Series alinean en base a las etiquetas automáticamente (utilizando la unión de las etiquetas de las series involucradas). Cuando una etiqueta está en una serie pero no en la otra, el resultado se marca como NaN.
End of explanation
"""


s2=s.rename('My_index')
s2

"""
Explanation: Las Series tienen un nombre, que está en el atributo name, y que puede especificarse al crearlo, o cambiarse con rename()
End of explanation
"""


s.value_counts()

"""
Explanation: La función value_counts es muy interesante, porque, dada una Series,  nos devuelve una Series con la cantidad de valores diferentes (en nuestro ejemplo es trivial, porque todos los valores son diferentes).
End of explanation
"""


a = np.array([
    [65,60,60,45,60],
    [75,35,50,75,40],
    [85,80,30,20,75],
    [75,45,30,70,80],
    [80,55,90,40,45],
    [90,60,95,15,45],
    [60,55,45,55,40]
])
df=pd.DataFrame(a)
df

"""
Explanation: 2. DataFrames
Los DataFrames son la estructura más comúnmente utilizada en pandas. Pueden verse como un conjunto de columnas de diferentes tipos (como una planilla Excel), o como una matriz 2D con etiquetas asociadas. Al crearlas, se pueden especificar los "index" (etiquetas de las filas), y/o los "columns" (las etiquetas de las columnas).
Existen muchas formas de crear DataFrames: como un diccionario de Series o ndarrays, ndarrays de 2 dimensiones, una Serie o incluso otro DataFrame. 
Creemos un DataFrame a partir de un 2D-ndarray:
End of explanation
"""


df=pd.DataFrame(a,columns=['Daño','Precisión','Alcance','Cadencia','Movilidad'])
df

"""
Explanation: Obsérvese que los nombres de los index y los columns son creados automáticamente, pero probablemente querramos especificarlos en la creación. En el ejemplo anterior, nos gustaría ponerle nombres a las columnas (en este caso, cada fila tiene las características de un arma en el juego Call of Duty):
End of explanation
"""


df.index, df.columns


"""
Explanation: Podemos consultar los índices y las columnas:
End of explanation
"""


d = {'one': pd.Series([1., 2., 3.], index=['a', 'b', 'c']),
     'two': pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])} 
# Obsérvese que en la columna 'one' no tenemos nada en la fila 'd'
df2 = pd.DataFrame(d)
df2

pd.DataFrame(d, columns=['two','three'])

"""
Explanation: Veamos otra forma de crear DataFrames: a través de una lista de Series. Obsérvese qué pasa cuando se especifica un índice que no está en el diccionario.
End of explanation
"""


df.describe()

"""
Explanation: Los arrays son objetos, y tienen métodos asociados. Utilice el método dtype para conocer el tipo de los elementos de a
En la documentación pueden verse muchas formas de crear DataFrames. Si creamos un DataFrame a partir de una Series, obtendremos una sola columna, cuyo nombre es el nombre de la Series. 
Una forma de tener una idea general sobre nuestro DataFrame es utilizando el método describe
End of explanation
"""


df['Precisión']

df['Dummy']=df['Alcance']*df['Daño']
df['Es_preciso']=df['Precisión'] >= 60
df

"""
Explanation: 3.Operaciones básicas con DataFrames
Los DataFrames pueden verse (como dijimos antes) como diccionarios de Series, indexados por los nombres de las columnas. Pueden accederse y modificarse igual que los diccionarios comunes.
End of explanation
"""


df[['Precisión', 'Alcance']].mean()

"""
Explanation: Es posible calcular funciones numéricas sobre algunas columnas:
End of explanation
"""


del df['Dummy']
df

"""
Explanation: Para borrar una columna, usamos del:
End of explanation
"""


df2['one_trunc'] = df2['one'][:2] 
df2

"""
Explanation: Si los valores que se pasan para crear una columna no son suficientes, se completan con NaN
End of explanation
"""


df['Precisión']

df[['Precisión', 'Alcance']]

"""
Explanation: Como vimos, seleccionar una columna de un DataSeries es muy parecido a seleccionar un elemento de un diccionario, siendo la clave el nombre de la columna (también es posible seleccionar por más de una columna a la vez: en ese caso, en vez de pasarle el nombre de la columna, le pasamos una lista con los nombres de las columnas seleccionadas)
End of explanation
"""


df['Arma']=['M16 Evil Clown', 'S36 Evil Clown', 'BY15 SnowFlakes', 'MSMC Ancient Runes', 
            'XPR-50 April\'s Fool', 'DLQ33 DeepShark', 'M4LMG RibbonExplosion']
df.set_index('Arma', inplace=True)
df

df.index # El índice ahora cambió

"""
Explanation: Vamos a agregarle a nuestro DataFrame los nombres de las armas, y lo ponemos como índice.
End of explanation
"""


df.loc['BY15 SnowFlakes']

"""
Explanation: Para seleccionar una fila, existen varias formas diferentes. Si conocemos su index, utilizamos loc:
End of explanation
"""


df.iloc[0]

"""
Explanation: Si conocemos el índice de su posición, utilizamos iloc
End of explanation
"""


df[1:3]

"""
Explanation: Podemos hacer slicing de las filas igual que con los ndarrays, utilizando un rango en la selección (obsérvese que aquí se busca en las filas, no en las columnas, y que se devuelve un DataFrame):
End of explanation
"""


df>50

df[df>50] 

del df['Es_preciso']

"""
Explanation: Podemos seleccionar de un dataframe las celdas que cumplan cierta condición (igual que se podía hacer con los arrays), y utilizar el resultado para seleccionar celdas que cumplan la condición (aquí se marcarán con NaN las celdas que no hayan sido seleccionadas).
End of explanation
"""


d = np.array([[85,52,95,30,50,-1],[80,55,90,40,45,-1],[65,60,60,45,60,-1],[85,52,95,30,50,-1],[48,65,90,63,60,-1],
              [60,55,45,55,40,-1]
             ,[78,55,32,60,75,-1],[90,40,25,60,75,-1]])
arm_names=['Arctic.50 Bats','XPR-50 RedTriangle','M16 NeonTiger', 'Arctic.50 RedTriangle','BK57 JackFrost',
                           'M4MLG RedTriangle', 'AKS-74U NeonTiger','PDW-57 ZombieGene']
df2= pd.DataFrame(d,index=arm_names, columns=['Daño','Precisión','Alcance','Cadencia','Movilidad','Dummy'])
df3= df.append(df2, sort=False)
df3

del df3['Dummy']

"""
Explanation: Cuando se realizan operaciones entre DataFrames, al igual que con Series, se alinean tanto las indexes como las columns, devolviéndose siempre la unión de los indexes/columns de los DataFrames involucrados.
Por ejemplo, agreguemos algunas armas más a nuestra base de datos, utilizando el método append (creamos primero un nuevo DataFrame, y luego lo concatenamos al original, para obtener el nuevo DataFrame). En el nuevo DataFrame, agregaremos una columna Dummy para ver qué sucede en la concatenación.
End of explanation
"""


df3.iloc[0]

df3 - df3.iloc[0]

"""
Explanation: En la siguiente operaciones, vamos a restarle una Series al DataFrame. En ese caso, pandas alinea las columnas con los ìndices de la Series, y eso hace que se resten todos los elementos de la fila.
End of explanation
"""


df3['Movilidad']*1.5

"""
Explanation: Los DF se pueden multiplicar por escalares, y se pueden aplicar operadores booleanos, exactamente igual que a los ndarrays.
End of explanation
"""


df3[['Daño','Precisión']].head(5)

"""
Explanation: 4. Operaciones de selección avanzada.
En esta sección veremos formas de seleccionar columnas y filas de acuerdo a diferentes condiciones, e incluso a agruparlas (de forma similar a lo que se hace con SQL).
4.1 Selección de columnas
Es posible seleccionar todos los elementos de una o más columnas utilizando una lista de columnas como argumento. (El método head simplemente selecciona las primeras filas del resultado).
End of explanation
"""


df3[df3['Precisión']>50]

"""
Explanation: 4.2 Selección de filas por condición booleana
Si queremos poner una condición (como en la cláusula WHERE de SQL), utilizaremos la selección por valores Booleanos, que surgirán de una condición. Por ejemplo, para obtener todas las columnas de las armas con precisión mayor a 50:
End of explanation
"""


df3['Precisión']>50

"""
Explanation: Lo que estamos haciendo es construir una Series de valores booleanos, para que me devuelva todas las filas que tienen True:
End of explanation
"""


df3[df3['Precisión']>50][['Alcance', 'Cadencia']]

df3

"""
Explanation: ... y luego pasamos esta Series como argumento para seleccionar aquellas filas que valen True.
Para agregar más condiciones, podemos utilizar los operadores booleanos & (AND) y | (OR). Atención: los paréntesis son obligatorios porque tiene más precedencia el == que el &! Seleccionemos todas las armas que tienen precisión o daño mayores que 80:
Si queremos seleccionar solamente alguna de las columnas Y algunas de las filas, podemos especificarlas en el DataFrame resultado de la operación anterior.
End of explanation
"""


d={ 'M16 Evil Clown':'Fusil de Asalto', 
    'S36 Evil Clown':'Ametralladora', 
    'BY15 SnowFlakes':'Escopeta',
    'MSMC Ancient Runes':'Ametralladora Ligera', 
    'XPR-50 April\'s Fool':'Fusil de Precisión',
    'DLQ33 DeepShark':'Fusil de Precisión',
    'M4LMG RibbonExplosion':'Ametralladora',
    'Arctic.50 Bats':'Fusil de Precisión',
    'XPR-50 RedTriangle':'Fusil de Precisión',
    'M16 NeonTiger':'Fusil de Asalto', 
    'Arctic.50 RedTriangle':'Fusil de Precisión',
    'BK57 JackFrost':'Fusil de Asalto',
    'M4MLG RedTriangle':'Ametralladora Ligera', 
    'AKS-74U NeonTiger':'Subfusil',
    'PDW-57 ZombieGene':'Subfusil'}

df3['Tipo']=pd.Series(d)
df3

"""
Explanation: Creemos un nuevo DataFrame que tenga, para cada arma, su tipo. Para eso, construimos una Series a partir de un diccionario (donde los índices coincidan con los que tenemos), y simplemente lo asignamos a nueva nueva columna del DataFrame ya construido.
End of explanation
"""


df3[df3['Tipo']=='Fusil de Precisión']

"""
Explanation: Listemos solamente los Fusiles de Precisión
End of explanation
"""


df3[df3['Tipo']=='Fusil de Precisión'].sort_values(['Daño', 'Precisión'])

"""
Explanation: Es posible ordenar los resultados de una consulta (que es siempre un DataFrame):
End of explanation
"""


df3.groupby('Tipo')['Precisión'].mean()

"""
Explanation: 4.3 Operaciones sobre conjuntos de filas
Si lo que queremos es agrupar las filas de acuerdo al valor de una o más columnas (u otro criterio), en forma similar a la operación GROUP BY de SQL, podemos utilizar el método groupby. Por ejemplo, si queremos conocer la precisión promedio según el tipo de arma, primero agrupamos por tipo, luego seleccionamos la Series que nos interesa, y finalmente le aplicamos la operación mean
End of explanation
"""


df3.groupby('Tipo').agg([np.mean, np.std])

"""
Explanation: Si queremos obtener la medida de todas las columnas, usamos agg para indicarle que aplique el método np.mean a todas las columnas (el método permite más de una función, así que calcularemos también la desviación estándar). En nuestro ejemplo, estamos agrupando según el valor de una sola columna, pero puede agruparse por más de una.
End of explanation
"""


df3.groupby('Tipo').agg({'Daño':[np.mean, np.std], 'Alcance':[np.mean]})

"""
Explanation: Podemos aplicar diferentes funciones a diferentes columnas...
End of explanation
"""


df3.agg([np.mean, np.std])

df3['Precisión'].agg('mean')

"""
Explanation: Y podemos también aplicarlo a todas las filas de nuestro DataFrame:
End of explanation
"""


df3.loc['PDW-57 ZombieGene']


"""
Explanation: 4.4 Más selección de elementos:  loc e iloc revisados
El atributo loc (observar que no es un método!) permite seleccionar partes (slices) de un DataFrame, utilizando los indexes o las columnas. 
Por ejemplo, como vimos antes, podemos utilizarlo para seleccionar un arma (fila) si tenemos su nombre, y nos devuelve una Series con todos los valores de cada columna de esa fila.
End of explanation
"""


df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]



"""
Explanation: También podemos especificar más de una fila, utilizando una lista (nótese que es una lista dentro de otra)
End of explanation
"""


df3.loc['XPR-50 RedTriangle', 'Alcance']


"""
Explanation: Si queremos obtener una celda, el primer elemento de loc nos selecciona la fila, y el segundo la columna:
End of explanation
"""


df3.loc['XPR-50 RedTriangle', 'Alcance'] *=2
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]


"""
Explanation: Podemos setear el valor de una celda utilizando loc
End of explanation
"""


df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene'], 'Daño':'Alcance']


"""
Explanation: Finalmente, podemos especificar slices, indicando, en cada axis, el primer y último elemento (que estarán incluidos).
End of explanation
"""


df4=pd.read_csv('https://raw.githubusercontent.com/gmonce/datascience/master/data/call_of_duty.csv')

df4

df4.set_index('Arma', inplace=True)
df4

"""
Explanation: Alternativamente, el método iloc permite hacer lo mismo, pero especifiando posiciones enteras, en lugar de labels (de forma similar a como lo hace NumPy)
5. Importar datos desde archivos
Por supuesto, en entornos de trabajo reales, donde hay miles o millones de instancias, no se cargan los dataframes "a mano"", sino que se importan desde archivos (separados por comas, o en algúún tipo de XML, o en formato json). Pandas provee diferentes formas de importar. En nuestro caso, importaremos los datos de las armas desde un archivo csv.
End of explanation
"""


df4['Alc&Prec']=(df4['Alcance']+ df4['Precision']+ df4['Daño'])/3
df4.sort_values('Alc&Prec', ascending=False)


# Ahora promediamos el resto, para los tiradores de cerca
# Elmino las que hacen poco daño o  tienen poca cadencia
df4['Cerca']=(df4['Cadencia']+ df4['Alcance']+ df4['Daño']+ df4['Movilidad'])/4

df4[(df4['Daño']>50) & (df4['Cadencia']>50)].sort_values('Cerca', ascending=False)


"""
Explanation: ¿Cuál es la mejor arma para un francotirador? Ordenamos por el promedio entre alcance y precisión
End of explanation
"""


