#!/usr/bin/env python
# coding: utf-8

# # Introducción a Pandas
# 
# Aprenderemos a utilizar pandas para el análisis de datos:
# 
# * Introducción a Pandas
# * Series
# * DataFrames
# * Valores perdidos
# * GroupBy
# * Fusionado, Unión y Concatenación
# * Operaciones habituales
# * Entrada y salida de datos

# ___

# In[1]:


import numpy as np
import pandas as pd


# # Series

# ### Creando Series
# 
# Conversión de una lista, Array Numpy o diccionario a Series:

# In[2]:


labels = ['a','b','c']
my_list = [10,20,30]
arr = np.array([10,20,30])
d = {'a':10,'b':20,'c':30}

print (labels)
print (my_list)
print (arr)
print (d)


# ### Usando listas

# In[3]:


list_to_series = pd.Series(data=my_list)


# In[4]:


# Asignamos etiquetas a la serie
pd.Series(data=my_list,index=labels)


# In[5]:


pd.Series(my_list,labels)


# ### Usando Arrays

# In[6]:


array_to_series = pd.Series(arr,labels)


# ### Usando Diccionarios

# In[7]:


dict_to_series = pd.Series(d)


# ### Usando índices
# 
# La clave para usar Series, es entender sus índices. Pandas usa los índices en formato numéricos o texto. 

# In[8]:


ser1 = pd.Series([1,2,3,4],index = ['USA', 'Germany','USSR', 'Japan'])
ser2 = pd.Series([1,2,5,4],index = ['USA', 'Germany','Italy', 'Japan'])                                   
print (ser1)
print ()
print (ser2)


# In[9]:


ser1['USA']


# In[10]:


# Las operaciones se realizan en función del índice
ser1 + ser2


# # DataFrames
# 
# Los DataFrames están directamente inspirados del lenguaje de programación R.  Podemos ver un DataFrame como un conjunto de objetos Series unidos.

# In[11]:


import pandas as pd
import numpy as np
from numpy.random import randn
np.random.seed(123)


# In[12]:


df = pd.DataFrame(randn(5,4),index='A B C D E'.split(),columns='W X Y Z'.split())
df


# ### Selección e indexación
# 

# In[13]:


df['W']


# In[14]:


type(df)


# In[15]:


# Selección de varias columnas por su nombre
df[['W','Z']]


# In[16]:


# Pandas también permite sintaxis tipo SQL, no obstante no se recomienda su uso.
df.W


# ¿Qué tipo de dato hay en la columna W?

# In[17]:


type(df['W'])


# Como vemos la columna W es simplemente una Serie

# **Creando una nueva columna:**

# In[18]:


df['nueva'] = df['W'] + df['Y']
df


# **Eliminar columnas**

# In[19]:


df.drop('nueva',axis=1)


# In[20]:


# A menos que lo especifiquemos con inplace, no se elimina nada
df


# In[21]:


df.drop('nueva',axis=1, inplace=True)
df


# **Eliminar filas**

# In[22]:


df.drop('E',axis=0)


# In[23]:


df.shape 


# **Selección de filas**

# In[24]:


df.loc['A']


# In[25]:


df.iloc[0]


# **Selección de un subset de datos**

# In[26]:


df.loc['B','Y']


# In[27]:


df.loc[['A','B'],['W','Y']]


# ### Selección condicional
# 
# Una importante característica de Pandas es la selección condicional de manera muy similar a Numpy:

# In[28]:


df>0


# In[29]:


df [df>0]


# In[30]:


print (df['W']>0)
df[df['W']>0]


# In[31]:


print (df['W']>0)
df[df['W']>0]['Y']


# In[32]:


df[df['W']>0][['Y','X']]


# Podemos concatenar condiciones con | y &.  Deberemos encerrar entre paréntesis cada una de las condiciones:

# In[33]:


df


# In[34]:


df[(df['W']>0) & (df['Y'] > 1)]


# In[35]:


df[(df['W']>0) | (df['Y'] > 1)]


# ### Más sobre índices

# In[36]:


df


# In[37]:


# Reseteamos el índice a una secuencia de 0 a n
df.reset_index()


# In[38]:


nuevoindice = 'CA NY WY OR CO'.split()


# In[39]:


df['Estados'] = nuevoindice
df


# Utilizamos la columna Estados como índice en el dataset

# In[40]:


df.set_index('Estados')


# Tenemos que tener en cuenta que si no usamos el argumento inplace, no se aplican los cambios

# In[41]:


df


# In[42]:


df.set_index('Estados', inplace=True)
df


# ### Índices múltiples y jerarquía en los índices

# In[43]:


# Creamos diferentes 'índices'
outside = ['G1','G1','G1','G2','G2','G2']
inside = [1,2,3,1,2,3]
hier_index = list(zip(outside,inside))
hier_index = pd.MultiIndex.from_tuples(hier_index)


# In[44]:


print (outside)
print (inside)
print (hier_index)


# In[45]:


df = pd.DataFrame(np.random.randn(6,2),index=hier_index,columns=['A','B'])
df


# ¿Cómo extraemos los datos en base a este índice doble?

# In[46]:


# Haciendo uso de .loc
df.loc['G1']


# In[47]:


df.loc['G1'].loc[1]


# Podemos entender G1 y G2 como una columna extra que se usa para el filtrado.
# Además a los índices podemos asignarles nombres

# In[48]:


df.index.names


# In[49]:


df.index.names = ['Grupo','Número']
df


# Supongamos que queremos obtener aquellos datos cuyo grupo es G1 y su número es 1

# In[50]:


df.xs(['G1',1])


# # Valores perdidos

# In[51]:


import numpy as np
import pandas as pd


# In[52]:


df = pd.DataFrame({'A':[1,2,np.nan],
                  'B':[5,np.nan,np.nan],
                  'C':[1,2,3]})
df


# In[53]:


df.dropna()


# In[54]:


df.dropna(axis=1)


# In[55]:


df.dropna(thresh=2)


# In[56]:


df.fillna(value='Valor Rellenado')


# Una caso más elaborado (y habitual), sería el de imputar la media de su columna a los NA

# In[57]:


df['A'].fillna(value=df['A'].mean())


# # Groupby
# El método groupby permite agrupar filas en base a un criterio y ejecutar operaciones de agregación sobre las mismas.

# In[58]:


import pandas as pd
# Generación del dataframe
data = {'Compañía':['GOOG','GOOG','MSFT','MSFT','FB','FB'],
       'Trabajador':['Ana','Carlos','Rosa','Vanesa','Carlos','Sara'],
       'Ventas':[200,120,340,124,243,350]}
df = pd.DataFrame(data)


# In[59]:


df


# In[60]:


# Agrupamos los datos en base a la columna Compañía
df.groupby('Compañía')


# In[61]:


# Guardamos el resultado en una variable
grupo = df.groupby('Compañía')


# In[62]:


# Ahora podemos aplicar funciones sobre la agrupación.
grupo.sum()


# In[63]:


# Ejecutado todo de una vez
df.groupby('Compañía').mean()


# Más ejemplos de agregaciones

# In[64]:


df.groupby('Compañía').count()


# In[65]:


df.groupby('Compañía').std()


# Podemos hacer un 'describe' para ver las características de nuestra agrupación de datos

# In[66]:


grupo.describe()


# In[67]:


# Si no nos gusta como se muestra la información podemos usar el método transpose
grupo.describe().transpose()


# In[68]:


# Descripción de la compañía FB
grupo.describe().loc['FB']


# # Fusionado, Unión y Concatenación

# In[69]:


import pandas as pd


# In[70]:


# Generación de los sets de datos a utilizar
df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                        'B': ['B0', 'B1', 'B2', 'B3'],
                        'C': ['C0', 'C1', 'C2', 'C3'],
                        'D': ['D0', 'D1', 'D2', 'D3']},
                        index=[0, 1, 2, 3])

df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
                        'B': ['B4', 'B5', 'B6', 'B7'],
                        'C': ['C4', 'C5', 'C6', 'C7'],
                        'D': ['D4', 'D5', 'D6', 'D7']},
                         index=[4, 5, 6, 7]) 

df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
                        'B': ['B8', 'B9', 'B10', 'B11'],
                        'C': ['C8', 'C9', 'C10', 'C11'],
                        'D': ['D8', 'D9', 'D10', 'D11']},
                        index=[8, 9, 10, 11])


# In[71]:


print (df1)
print (df2)
print (df3)


# ## Concatenacion
# 
# La concatenación, básicamente une diferentes DataFrames. Hay que tener en cuenta que las dimensiones (respecto del eje usado en la concatenación) de los diferentes DataFrames, deben ser iguales.

# In[72]:


pd.concat([df1,df2,df3])


# In[73]:


pd.concat([df1,df2,df3],axis=1)


# ## Fusionado
# 
# Permite la unión de diferentes DataFrames usando una lógica similar a la SQL a la hora de fusionar tablas.

# In[74]:


izquierda = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                     'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3']})
   
derecha = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                          'C': ['C0', 'C1', 'C2', 'C3'],
                          'D': ['D0', 'D1', 'D2', 'D3']})    


# In[75]:


izquierda


# In[76]:


derecha


# In[77]:


# 4 diferentes tipos de fusionado: inner, left, rigth, outer
pd.merge(izquierda,derecha,how='inner',on='key')


# Unos casos algo más complicados

# In[78]:


izquierda = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],
                     'key2': ['K0', 'K1', 'K0', 'K1'],
                        'A': ['A0', 'A1', 'A2', 'A3'],
                        'B': ['B0', 'B1', 'B2', 'B3']})
    
derecha = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],
                               'key2': ['K0', 'K0', 'K0', 'K0'],
                                  'C': ['C0', 'C1', 'C2', 'C3'],
                                  'D': ['D0', 'D1', 'D2', 'D3']})


# In[79]:


izquierda


# In[80]:


derecha


# In[81]:


# Podemos fusionar las tablas en base a más de una key (columna)
pd.merge(izquierda, derecha, on=['key1', 'key2'])


# In[82]:


pd.merge(izquierda, derecha, how='outer', on=['key1', 'key2'])


# In[83]:


pd.merge(izquierda, derecha, how='right', on=['key1', 'key2'])


# In[84]:


pd.merge(izquierda, derecha, how='left', on=['key1', 'key2'])


# ## Unión
# De 2 DataFrames, con índices iguales o no, en uno sólo,

# In[85]:


izquierda = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                      index=['K0', 'K1', 'K2']) 

derecha = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                      index=['K0', 'K2', 'K3'])
izquierda


# In[86]:


derecha


# In[87]:


# Observamos que el registro 2, al no existir en derecha, no se completa a nivel de columnas C y D
izquierda.join(derecha)


# In[88]:


izquierda.join(derecha, how='outer')


# # Operaciones habituales

# In[89]:


import pandas as pd
df = pd.DataFrame({'col1':[1,2,3,4],'col2':[444,555,666,444],'col3':['abc','def','ghi','xyz']})
df.head()


# ### Información sobre los valores únicos de una columna

# In[90]:


df['col2'].unique()


# In[91]:


# Total de elementos únicos 
df['col2'].nunique()


# In[92]:


df['col2'].value_counts()


# ### Selección de datos

# In[93]:


#Selección de un DataFrame filtrando en base a valores de columnas
nuevodf = df[(df['col1']>2) & (df['col2']==444)]
nuevodf


# ### Funciones Apply

# In[94]:


def doble(x):
    return x**2


# In[95]:


# Aplicamos la función cuadrado A TODOS los elementos de col1.  Es una operación columnar, por tanto no hace falta iterar registro a registro
df['col1'].apply(doble)


# In[96]:


# Mismo resultado de diferente manera
df['col1'].apply(lambda x: x**2)


# In[97]:


# Obtener el tamaño de las diferentes filas
df['col3'].apply(len)


# In[98]:


# Eliminar columnas (ojo, hasta no usar inplace no se eliminan del set original)
df.drop('col1',axis=1)


# In[99]:


df.columns


# In[100]:


df.index


# In[101]:


df['col1'].sum()


# **Eliminar permanentemente una columna**

# In[102]:


del df['col1']
df


# **Ordenar los DataFrames:**

# In[103]:


df.sort_values(by='col2') #inplace=False por defecto

# Nota, observad como el índice no varía.  Cada registro sigue manteniendo el índice original.

