#!/usr/bin/env python # coding: utf-8 # # 11. Pandas # Pandas es un paquete de Python que proporciona estructuras de datos __rápidas, flexibles y expresivas__ diseñadas para que trabajar con datos __"relacionales" o "etiquetados"__ sea fácil e intuitivo, es una de las librerias más usadas debido a su potencia y además es de código abierto . Su función es ser una herramienta de alto nivel para realizar__ analisis de datos__ en el mundo real. # # Pandas es muy adecuado para muchos tipos diferentes de datos: # # - Datos tabulares con columnas de tipo heterogéneo, como en una tabla de SQL o una hoja de cálculo de Excel # - Datos de series de tiempo ordenados y no ordenados (no necesariamente de frecuencia fija). # - Datos de matriz arbitraria (homogéneamente tipados o heterogéneos) con etiquetas de fila y columna # - Cualquier otra forma de conjuntos de datos observacionales / estadísticos. Los datos realmente no necesitan ser etiquetados en absoluto para ser colocados en una estructura de datos pandas. # # Pandas ofrece las siguientes estructuras de datos: # # * __Series__: Son arrays unidimensionales con indexación (arrays con índice o etiquetados), similar a los diccionarios. Pueden generarse a partir de diccionarios o de listas. # # * __DataFrame__: Son estructuras de datos similares a las tablas de bases de datos relacionales como SQL. # # * __Panel, Panel4D y PanelND__: Estas estructuras de datos permiten trabajar con más de dos dimensiones. Dado que es algo complejo y poco utilizado trabajar con arrays de más de dos dimensiones no trataremos los paneles en estos tutoriales de introdución a Pandas. # # Aquí están algunas de las cosas que los pandas hacen bien: # # - Manejo fácil de __datos faltantes__ (representados como __NaN__) en punto flotante así como datos de punto no flotante. # - Cambios de tamaño: las columnas se pueden __insertar y eliminar__ de DataFrame y objetos de dimensiones superiores. # - __Alineación automática y explícita de datos__: los objetos pueden alinearse explícitamente con un conjunto de etiquetas, o el usuario puede simplemente ignorar las etiquetas y dejar que Series, DataFrame, etc. alineen automáticamente los datos en cálculos. # - Potente y flexible al __agrupar por funcionalidad__ para realizar operaciones __split-apply-combine__ en conjuntos de datos, tanto para la agregación como para la transformación de datos. # - __Facilita la conversión__ de datos desiguales y diferenciados en otras estructuras de datos Python y NumPy en objetos DataFrame. # - Recorte inteligente basado en __slicing, fancy indexing, y subsetting__ de grandes conjuntos de datos. # - Robustas herramientas de IO para cargar datos de __archivos planos__ (CSV y delimitado), archivos de Excel, bases de datos y guardar / cargar datos desde el formato __HDF5 ultrarrápido__. # - __Funciones específicas de series de tiempo__: generación de intervalos de fechas y conversión de frecuencia, estadísticas de ventanas en movimiento, regresiones lineales de ventanas en movimiento, cambio de fecha y retraso, etc. # # Para los científicos de datos, el trabajo con datos suele dividirse en múltiples etapas: muestrear y limpiar los datos, analizarlos o modelarlos, y luego organizar los resultados del análisis en una forma adecuada para representación gráfica o tabular. Pandas es la herramienta ideal para todas estas tareas. # # Otras notas: # # - Pandas es __rápido__. Muchos de los bits algorítmicos de bajo nivel se han modificado extensamente en el código de Cython. Sin embargo, como con cualquier otra cosa, la generalización suele sacrificar el rendimiento. Así que si usted se centra en una característica para su aplicación que puede ser capaz de crear una herramienta especializada más rápida. # - Pandas es una dependencia de __statsmodels__, por lo que es una parte importante del ecosistema de computación estadística en Python. # - Pandas ha sido ampliamente utilizado en la producción en __aplicaciones financieras__. # Ahora empecemos importando pandas: # In[1]: # Forma convencional de importar pandas: import pandas as pd # In[2]: # Importando matplotlib para graficar get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt plt.rcParams['figure.figsize'] = (15, 5) # ## Estructuras de Datos # ------------------------------ # # ### Series # # La estructura de datos de Series en Pandas es una matriz etiquetada unidimensional. # # - Los datos de la matriz pueden ser de cualquier tipo (números enteros, cadenas, números de punto flotante, objetos Python, etc.). # # - Los datos dentro de la matriz son homogéneos. # # - Los datos pueden ser listas, arrays, o un diccionario. # # In[3]: # Constructor de serie con datos como una lista de enteros s1 = pd.Series([33, 19, 15, 89, 11, -5, 9]) s1 # In[4]: # Tipo de serie es la serie pandas type(s1) # In[5]: # Recupera los valores de la serie s1.values # In[6]: # Tipo de valores de datos es NumPy ndarray type(s1.values) # ![Alt text](../images/series.jpg "Optional title") # In[7]: # Define los datos e indices como listas data1 = [33, 19, 15, 89, 11, -5, 9] index1 = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] # Crea la serie s2 = pd.Series(data1, index=index1) s2 # ![Alt text](../images/series2.jpg "Optional title") # In[8]: # También podemos dar etiquetas significativas a los datos de la serie y el índice s2.name='Daily Temperatures' s2.index.name='Weekday' s2 # La representación más general de una serie es como un almacén de key-values ordenado. # # - El orden es representado por el offset. # - El valor-clave es una asignación de índice o etiqueta a los valores de matriz de datos. # - Indice como "offset" o "posición" vs índice como "etiqueta" o "clave". # # ![Alt text](../images/series3.jpg "Optional title") # Al ser las series de tipo__NumPy-ndarray__ podemos efectuar las mismas operaciones que hicimos en Numpy: # In[9]: s2 * 2 # In[10]: # Podemos usar el slicing usando la posicion s2[0:3] # In[11]: # Tambien podemos usar slicin usando sus etiquetas(labes) s2['Mon':'Wed'] # #### Uniendo Series # In[12]: import numpy as np s1 = pd.Series(np.random.randint(1, high=5, size=100, dtype='l')) s2 = pd.Series(np.random.randint(1, high=4, size=100, dtype='l')) s3 = pd.Series(np.random.randint(10000, high=30001, size=100, dtype='l')) housemkt = pd.concat([s1, s2, s3], axis=1) housemkt.rename(columns = {0: 'bedrs', 1: 'bathrs', 2: 'price_sqr_meter'}, inplace=True) housemkt.head() # ### Dataframes # # La estructura de datos de DataFrame en Pandas es una matriz etiquetada bidimensional. # # - Los datos de la matriz pueden ser de cualquier tipo (números enteros, cadenas, números de punto flotante, objetos Python, etc.). # - Los datos dentro de cada columna son homogéneos # - De forma predeterminada, Pandas crea un índice numérico para las filas en la secuencia 0 ... n # # ![Alt text](../images/dataframe.jpg "Optional title") # In[13]: import datetime # Creamos una lista de fechas desde 12-01 to 12-10 dt = datetime.datetime(2016,12,1) end = datetime.datetime(2016,12,10) step = datetime.timedelta(days=1) dates = [] # Rellenar la lista while dt < end: dates.append(dt.strftime('%m-%d')) dt += step # In[14]: dates # In[15]: d = {'Date': dates, 'Arequipa' : [15,19,15,11,9,8,13,14,16], 'Puno': [-2,0,2,5,7,-5,-3,4,7], 'Lima':[20,18,23,19,25,27,23,29,30]} temps = pd.DataFrame(d) temps # ## Leyendo data de un archivo csv # # Puede leer datos de un archivo __CSV__ (comma-separated values) utilizando la función read_csv. # # Vamos a buscar algunos datos de avistamientos de ovnis. # In[16]: # Leyendo el dataset de reportes de avistamientos en un dataframe ufo = pd.read_csv('../data/ufo.csv') # In[17]: # Examinamos las 5 primeras filas ufo.head() # Documentacion de [read_csv](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html). # También podemos leer data de una web, en este caso leeremos un archivo __TSV__ (Tabular-separated-values): con __read_table__: # In[18]: # Leyendo el dataset de ordenes de Chipotle de una URL y guardar los resultados en un dataframe orders = pd.read_table('http://bit.ly/chiporders') #mostramos las ultimas filas orders.tail() # Para seleccionar una Columna o __"Serie"__ usamos la notacion []: # In[19]: ufo['City'].head() # Tambien podemos usar la notación punto (__.__): # In[20]: ufo.City.head() # La notacion de brackets [] o corchetes siempre funciona mientras que la notación del punto tiene limitaciones: # # - La notación de puntos no funciona si hay espacios en el nombre de la serie # - La notación de puntos no funciona si la Serie tiene el mismo nombre que un método o atributo de DataFrame (como 'head' o 'shape') # - No se puede utilizar la notación de puntos para definir el nombre de una nueva serie (véase más adelante) # In[21]: # Leyendo un dataset de las top-rated IMDb movies en un dataframe movies = pd.read_csv('../data/imdb.csv') movies.head() # In[22]: # Método describe: calcula un resumen de estadísticas movies.describe() # In[23]: movies.shape # Porqué algunos comandos de pandas terminan en parentesis y otros no? # Los __métodos__ terminan con paréntesis, mientras que los __atributos__ no # In[24]: # Ejemplo de atributo: obtener el data type de cada columna movies.dtypes # In[25]: # Utilice un parámetro opcional para describir el método para resumir sólo las columnas 'object' movies.describe(include=['object']) # Ahora vamos a obtener el numero de peliculas por clasificación de contenido ( R, PG-13, PG.. etc): # In[26]: movie_ratings = movies['content_rating'].value_counts() # In[27]: movie_ratings.plot(kind='bar') # In[28]: # Examinamos las 5 ultimas filas ufo = pd.read_csv('../data/ufo.csv') ufo.head() # Los nombres de cada columna deben de tratar de no tener espacios, para ello podemos cambiar el nombre de las columnas de distintas formas como: # In[29]: # Reemplazar todos los nombres de columnas sobrescribiendo el atributo 'columnas' ufo_cols = ['city', 'colors_reported', 'shape_reported', 'state', 'time'] ufo.columns = ufo_cols ufo.head() # In[30]: # Renombrar dos de las columnas mediante el método 'rename' ufo.rename(columns={'colors_reported':'Colors_Reported_test', 'shape_reported':'Shape_Reported_test'}, inplace=True) ufo.head() # Para remover una columna usamos el método __drop__ : # In[31]: # Eliminar una columna (axis=1 se refiere a columnas) ufo.drop('Colors_Reported_test', axis=1, inplace=True) ufo.head() # In[32]: # Eliminar varias columnas a la vez ufo.drop(['state', 'time'], axis=1, inplace=True) ufo.head() # In[33]: # Eliminar varias filas a la vez (axis=0 se refiere a filas) ufo.drop([0, 1], axis=0, inplace=True) ufo.head() # ### Filtering y Sorting # In[34]: # Examinamos las 5 primeras filas movies = pd.read_csv('../data/imdb.csv') movies.head() # In[35]: # Si queremos ordenar usamos el metodo sort_values() movies.title.sort_values(ascending=False).head() # En este caso ordenamos de forma descendente # In[36]: # Identificamos: los indices se mantienen con cada fila cuando filtramos el dataframe movies[movies.content_rating=='PG-13'].head() # In[37]: # Ordenar todo el DataFrame por la serie 'title' (devuelve un DataFrame) movies.sort_values('title').head() # __Objetivo:__ filtrar las filas de DataFrame para mostrar sólo películas con una "duración" de al menos 200 minutos. # In[38]: is_long = movies.duration >= 200 movies[is_long] # O de forma equivalente, escríbalo en una línea (no es necesario crear el objeto 'is_long') movies[movies.duration >= 200] # In[39]: # Selecciona la serie 'genre' del DataFrame filtrado movies[movies.duration >= 200].genre # O de forma equivalente, use el método 'loc' movies.loc[movies.duration >= 200, 'genre'] # __Meta:__ Filtrar aún más el DataFrame de películas largas (duration> = 200) para mostrar sólo películas que también tienen un 'genre' de 'Drama' # In[40]: movies[(movies.duration >=200) & (movies.genre == 'Drama')] # __Objetivo:__ Filtrar el DataFrame original para mostrar películas con un 'genre' de 'Crime' o 'Drama' o 'Action' # In[41]: # Utiliza el '|' Operador para especificar que una fila puede coincidir con cualquiera de los tres criterios movies[(movies.genre == 'Crime') | (movies.genre == 'Drama') | (movies.genre == 'Action')].head(10) # O de forma equivalente, use el método 'isin' movies[movies.genre.isin(['Crime', 'Drama', 'Action'])].head(10) # In[42]: # Lee el dataset de Chipotle orders en un DataFrame orders = pd.read_table('http://bit.ly/chiporders') orders.head() # In[43]: # Convierte un string a un numero para poder hacer operaciones matemáticas orders.item_price.str.replace('$', '').astype(float).head() # In[44]: # Método string 'contains' checkea si es que hay un substring que contenga 'Chicken' y retorna una serie booleana orders.item_name.str.contains('Chicken').head() # In[45]: # Convierte una serie booleana a un entero (False = 0 , True = 1) orders.item_name.str.contains('Chicken').astype(int).head() # ### Grouping # In[46]: # Lee el dataset de consumo de alcohol en un dataframe drinks = pd.read_csv('http://bit.ly/drinksbycountry') drinks.head() # In[47]: # Calcula la media de cervezas servidas solo en paises del continente africano drinks[drinks.continent=='Africa'].beer_servings.mean() # In[48]: # Calcula la media de cervezas servidas por cada continente drinks.groupby('continent').beer_servings.mean() # In[49]: # Especificando una columna a la que se debe aplicar la función de agregación no se requiere drinks.groupby('continent').mean() # In[50]: # Diagrama de barras de lado a lado del DataFrame de arriba drinks.groupby('continent').beer_servings.mean().plot(kind='bar') # In[51]: # bar plot of the 'value_counts' for the 'genre' Series movies.genre.value_counts().plot(kind='bar') # ### Manejando Valores perdidos o Missing Values # # ¿Qué significa "NaN"? # # - "NaN" no es una cadena, sino que es un valor especial: __numpy.nan__. # - Representa "Not a number" e indica un valor faltante. # - __read_csv__ detecta los valores perdidos (de forma predeterminada) al leer el archivo y los reemplaza con este valor especial. # In[52]: # Leyendo el dataset de reportes de avistamientos en un dataframe ufo = pd.read_csv('../data/ufo.csv') ufo.tail() # In[53]: # Si color reported es null retornara True ufo['Colors Reported'].isnull().tail() # In[54]: # Caso contrario retornara False con notnull() ufo['Colors Reported'].notnull().tail() # In[55]: # Nos devuelve el dataframe con las columnas vacias de City ufo[ufo.City.isnull()].head() # In[56]: # Devuelve el numero de filas y columnas ufo.shape # In[57]: # Si faltan 'algun (any)' valor en una fila entonces elimina esa fila esa fila ufo.dropna(how='any').shape # In[58]: # Si faltan todos(all) los valores en una fila, entonces elimina esa fila (no se eliminan en este caso) ufo.dropna(how='all').shape # In[59]: # Si falta algun valor en una fila (teniendo en cuenta sólo 'City' y 'Shape Reported'), entonces se elimina esa fila ufo.dropna(subset=['City', 'Shape Reported'], how='any').shape # In[60]: # Si "all" los valores estan faltantes en una filla (considerando solo 'City' y 'Shape Reported') entonces elimina esa fila ufo.dropna(subset=['City', 'Shape Reported'], how='all').shape # In[61]: # 'value_counts' no incluye missing values por defecto ufo['Shape Reported'].value_counts().head() # In[62]: # Incluye explícitamente los missing values ufo['Shape Reported'].value_counts(dropna=False).head() # In[63]: # Rellenar los valores faltantes con un valor especificado ufo['Shape Reported'].fillna(value='VARIOUS', inplace=True) # In[64]: # Confirmar que los valores faltantes fueron rellenados ufo['Shape Reported'].value_counts().head() # ### Slicing # # El método __loc__ se utiliza para seleccionar filas y columnas por etiqueta. Puede pasar # : # - Una etiqueta única # - Una lista de etiquetas # - Una porción de etiquetas # - Una serie booleana # - Dos puntos (que indica "todas las etiquetas") # In[65]: # Leyendo la data de crimen cometidos en los EEUU crime_data = pd.read_csv('../data/crime.csv') crime_data.head() # In[66]: # Fila 0, todas las columnas crime_data.loc[0, :] # In[67]: # filas 0 , 1 y 2, all columns crime_data.loc[[0, 1, 2], :] # In[68]: # filas 0 a la 8, all columns crime_data.loc[0:8 , :] # In[69]: # filas 0 a la 3, all columns crime_data.loc[0:3] # In[70]: # filas de la 0 a la 4 (incluyendo) las columnas 'State' y 'Count' crime_data.loc[0:4, ['State', 'Count']] # El método __iloc__ se utiliza para seleccionar filas y columnas por posición entera. Se puede pasar: # # - Una sola posición entera # - Una lista de posiciones enteras # - Una porción de posiciones enteras # - Dos puntos (que indica "todas las posiciones enteras") # In[71]: # filas en la posicion 0 al 2 (excluyente) todas las columnas crime_data.iloc[0:2, :] # El método __ix__ se utiliza para seleccionar filas y columnas por etiqueta o posición de número entero, y sólo debe utilizarse cuando se necesita mezclar selección basada en etiquetas y enteros en la misma llamada. # # Reglas para el uso de números con ix: # # - Si el índice es cadenas, los números se tratan como posiciones enteras, y por lo tanto los cortes son exclusivos a la derecha. # - Si el índice es números enteros, los números se tratan como etiquetas y, por lo tanto, los cortes son inclusivos. # In[72]: # Leer el dataset de consumo de alcohol en un DataFrame y establecer 'país' como el índice drinks = pd.read_csv('http://bit.ly/drinksbycountry', index_col='country') drinks.head(10) # In[73]: # Fila con la etiqueta 'Albania' columna en la posicion 0 drinks.ix['Albania', 0] # In[74]: # fila en la posicion 1, columna con etiqueta 'beer_servings' drinks.ix[1, 'beer_servings'] # In[75]: # Filas 'Albania' hasta 'Andorra' (inclusiva), columnas en la posicion 0 hasta 2 (exclusiva) drinks.ix['Albania':'Andorra', 0:2] # __ Entonces... ¿Cuáles son las diferencias entre loc, iloc, e ix? veamos... __ # # Usemos la vieja confiable para contestar esta pregunta, es bueno buscar preguntas en internet ya que es imposible memorizar tantos metodos... ;) # http://stackoverflow.com/questions/31593201/pandas-iloc-vs-ix-vs-loc-explanation # ### Apply # # Podemos aplicar funciones a data series: # In[76]: def alcoholics(x): if x > 10: return 'alcoholics!' else: return 'Sober people' # In[77]: drinks['Kind of people'] = drinks['total_litres_of_pure_alcohol'].apply(alcoholics) drinks.head(10) # ### Creación de Dummy Variables # # En general: # - Si tiene __"K" posibles valores__ para una característica categórica, sólo necesita __"K-1" dummy variables __ para capturar toda la información sobre esa característica. # - Una convención es __eliminar la primera variable ficticia__, que define ese nivel como la "baseline". # In[78]: # Leyendo la data de crimen cometidos en los EEUU titanic = pd.read_csv('../data/titanic.csv') titanic.head() # In[79]: # create the 'Sex_male' dummy variable using the 'map' method titanic['Sex_male'] = titanic.Sex.map({'female':0, 'male':1}) titanic.head() # In[80]: pd.get_dummies(titanic.Sex).head() # In[81]: # Eliminamos la primera first dummy variable ('female') usando el metodo iloc pd.get_dummies(titanic.Sex, prefix='Sex').iloc[:, 1:].head() # In[82]: # Usando 'get_dummies' Con una característica que tiene 3 valores posibles pd.get_dummies(titanic.Embarked, prefix='Embarked').head(5) # In[83]: # Eliminamos la primera dummy variable ('C') pd.get_dummies(titanic.Embarked, prefix='Embarked').iloc[:, 1:].head(5) # Cómo traducir estos valores de nuevo al valor original __'Embarked'__: # * 0, 0 significa C # * 1, 0 significa Q # * 0, 1 significa S # In[84]: # Pasa el DataFrame a 'get_dummies' y especifica qué columnas a dummy (descarta las columnas originales) titanic = pd.read_csv('../data/titanic.csv') pd.get_dummies(titanic, columns=['Sex', 'Embarked']).head() # In[85]: # Utiliza el parámetro 'drop_first' (nuevo en pandas 0.18) para eliminar la primera variable dummy para cada característica pd.get_dummies(titanic, columns=['Sex', 'Embarked'], drop_first=True).head() # ### Ejemplo # # ¿Son los niños más altos que las niñas en promedio a los X años? X puede ser 2 años, 9 años y 18 años de edad # In[86]: niños = pd.read_csv('../data/children_heights.txt',sep='\t') niños.tail(10) # In[87]: # Resumen de estadísticas niños.describe() # In[88]: def plot_hist(df, columns): df[columns].hist(color='r', alpha=0.3, normed=False) plot_hist(niños,['Boys_2', 'Girls_2']) # In[89]: plot_hist(niños,['Boys_9', 'Girls_9']) # In[90]: plot_hist(niños, ['Boys_18', 'Girls_18']) # In[91]: from scipy.stats import ttest_ind def plot_probs(df, boy_col, girl_col): fig = plt.figure() ax = fig.add_subplot(111) # No considero los valores Nan boy_values = df[boy_col].values boy_values = boy_values[~np.isnan(boy_values)] weights = np.ones_like(boy_values)/len(boy_values) _ = ax.hist(boy_values, alpha=0.3, label=['Niños'], weights=weights) girl_values = df[girl_col].values girl_values = girl_values[~np.isnan(girl_values)] weights = np.ones_like(girl_values)/len(girl_values) _ = ax.hist(girl_values, color='red', alpha=.3, label='Niñas', weights=weights) ax.set_xlabel('Altura(cm)', fontsize=14) ax.set_ylabel('Probabilidad', fontsize=14) ax.vlines(np.mean(boy_values), 0.0, ax.get_ylim()[1], colors='b', linestyle='--', linewidth=4) ax.vlines(np.mean(girl_values), 0.0, ax.get_ylim()[1], colors='r', linestyle='--', linewidth=4) ax.legend() print("Resultado ", ttest_ind(boy_values, girl_values)) # In[92]: plot_probs(niños, 'Boys_2', 'Girls_2') # In[93]: plot_probs(niños, 'Boys_9', 'Girls_9') # In[94]: plot_probs(niños, 'Boys_18', 'Girls_18') # Obsérvese que aunque hay diferencias en las alturas de niñas y niños a los 2 años 9, no se encuentran diferencias significativas en promedio. Sin embargo, a los 18 años de edad, en promedio los niños son más altos que las niñas y esta diferencia es estadísticamente significativa a nivel de 0,05 # ### Time Series # In[95]: # Leyendo el dataset de reportes de avistamientos en un dataframe ufo = pd.read_csv('../data/ufo.csv') ufo.head() # In[96]: # Hora se puede acceder usando el corte de cadena, pero este enfoque se rompe con demasiada facilidad ufo.Time.str.slice(-5, -3).astype(int).head() # In[97]: # Convierte 'Time' a un datetime format ufo['Time'] = pd.to_datetime(ufo.Time) ufo['Time'].head() # In[98]: # Convertir una sola cadena al formato datetime (sale un objeto timestamp) ts = pd.to_datetime('1/1/1999') # In[99]: # Comparar una serie de fecha y hora con una marca de tiempo ufo.loc[ufo.Time >= ts, :].head() # In[100]: # Cuenta el numero de avistamientos de ovnis por año ufo['Year'] = ufo.Time.dt.year ufo['Year'].head() # In[101]: # Grafica el número de informes de OVNI por año ufo.Year.value_counts().sort_index().plot() # In[102]: # Esta celda da el estilo al notebook from IPython.core.display import HTML css_file = '../styles/StyleCursoPython.css' HTML(open(css_file, "r").read())