Este notebook es una traducción y adaptación de esta notebook creada por Christopher Fonnesbeck y de estas notebooks creadas por Jake Vanderplas. El material aquí presentado es un pequeño subconjunto de las capacidades disponibles en Pandas. Para mayor información referirse a:
In [1]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-darkgrid')
import seaborn as sns
import numpy as np
import pandas as pd # asi se suele importar Pandas
Pandas es un paquete que Python que provee estructuras de datos rápidas, flexibles y expresivas diseñadas para trabajar con datos rotulados. Dichas estructuras de datos se pueden pensar como arrays de NumPy donde las filas y columnas están rotuladas. O de forma similar como una planilla de cálculo bajo Python.
Así como NumPy es una muy buena herramienta para trabajar con números, vectores, álgebra lineal, etc. Pandas es adecuado para trabajar con:
¿Por qué es importante tener una herramienta como Pandas?
El tiempo consumido (por un humano) en aplicar métodos estadísticos y/o de machine learning es en muchos casos mucho menor que el tiempo requerido para obtener y procesar los datos. Pandas intenta facilitar el procesado de los datos y reducir el tiempo que este consume para que podamos aumentar el tiempo que pasamos pensando en los problemas que queremos resolver. Procesar datos es una tarea que suele involucrar los siguientes pasos
Pandas introduce fundamentalmente 3 nuevas estructuras de datos:
Series
DataFrame
Index
Empecemos por la primera de estas.
In [2]:
conteo = pd.Series([632, 1638, 569, 115])
conteo
Out[2]:
Una Series
tiene siempre dos columnas. La primer columna contiene los índices y la segunda los datos. En el ejemplo anterior, pasamos una lista de datos y omitimos el índice por lo que Pandas creo un índice automáticamente usando una secuencia de enteros, empezando por 0 (como es usual en Python).
La idea que una Series
es un array con un índice explícito, no es solo una metáfora. De hecho a partir de una Serie es posible obtener el array de NumPy "contenido" en ella.
In [3]:
conteo.values
Out[3]:
Y también es posible obtener el índice.
In [4]:
conteo.index
Out[4]:
Es importante notar que los arreglos de NumPy también tienen índices, solo que estos son implícitios y siempre son enteros comenzando desde el 0. En cambio los Index
en Pandas son explícitos y no están limitados a enteros. Podemos asignar rótulos que tengan sentido según nuestros datos. Si nuestros datos representan la cantidad de bacterias según sus especies, podríamos tener algo como:
In [5]:
bacteria = pd.Series([632, 1638, 569, 115],
index=['Firmicutes', 'Proteobacteria',
'Actinobacteria', 'Bacteroidetes'])
bacteria
Out[5]:
Ahora el Index
contiene strings
en lugar de enteros. Es posible que estos pares rótulo-dato nos recuerden a un diccionario. Si esta analogía es válida deberíamos poder usar los rótulos para referirnos directamente a los valores contenidos en la serie.
In [6]:
bacteria['Actinobacteria']
Out[6]:
Incluso podemos crear Series a partir de diccionarios
In [7]:
bacteria_dict = {'Firmicutes': 632,
'Proteobacteria': 1638,
'Actinobacteria': 569,
'Bacteroidetes': 115}
pd.Series(bacteria_dict)
Out[7]:
O podemos hacerlo de forma algo más breve usando atributos. Un atributo es el nombre que se le da a un dato o propiedad en programación orientada a objetos. En el siguiente ejemplo bacteria
es un objeto y Actinobacteria
el atributo.
In [8]:
bacteria.Actinobacteria
Out[8]:
El hecho que tengamos índices explícitos, como los nombres de bacterias, no elimina la posibilidad de acceder a los datos usando índices implicitos, como es común hacer con listas y arreglos.
In [9]:
bacteria[2]
Out[9]:
Si tuvieras una Series
con índices que fuesen enteros ¿Qué pasaría al indexarla?
una_series[1]
¿Obtendríamos el segundo elemento de la serie o el elemento cuyo índice explícito es 1? ¿Y si el ńumero 1 no estuviera contenido en el índice de la serie?
Más adelante veremos que solución ofrece Pandas para evitar confusiones. Pero mientras quizá podemos pensar una solución y discutirla.
Al igual que con los arreglos de NumPy podemos usar booleanos para indexar una serie. De esta forma podemos contestar la pregunta; cuáles bacterias dieron conteos superiores a 1000 de forma bastante intuitiva (¿Es realmente intuitiva?).
In [10]:
bacteria[bacteria > 1000]
Out[10]:
O podríamos necesitar encontrar el subconjunto de bacterias cuyos nombres terminan en "bacteria":
In [11]:
bacteria[[nombre.endswith('bacteria') for nombre in bacteria.index]]
Out[11]:
In [12]:
bacteria[:'Actinobacteria']
Out[12]:
In [13]:
bacteria[:3]
Out[13]:
Al indexar con un índice implícito el último índice NO se incluye. Esto es lo que esperamos de listas, tuplas, arreglos etc. En cambio, al indexar con un índice explícito el último índice se incluye!
También es posible indexar usando una lista.
In [14]:
bacteria[['Actinobacteria', 'Proteobacteria']]
Out[14]:
In [15]:
datos = pd.Series(['x', 'y', 'z'], index=range(10, 13))
datos
Out[15]:
Pandas
usará el índice explítico al indexar
In [16]:
datos[10]
Out[16]:
Pero el implícito al tomar rebanadas!
In [17]:
datos[0:2]
Out[17]:
Pandas provee de dos métodos para indexar. El primero de ellos es loc
que permite hacer las operaciones de indexado/rebanado usando SIEMPRE el índice explícito.
In [18]:
datos.loc[10]
Out[18]:
In [19]:
datos.loc[10:11]
Out[19]:
El otro método es iloc
el cual permite usar el índice implícito.
In [20]:
datos.iloc[0]
Out[20]:
In [21]:
datos.iloc[0:2]
Out[21]:
Siguiendo el zen de Python que dice “explícito es mejor que implícito", la recomendación general es usar loc
e iloc
. De esta forma la intención del código se hace explícita lo que contribuye a una lectura más fluida y a reducir la posibilidad de errores.
Una de las características más valiosas de NumPy es la posiblidad de vectorizar código, evitando escribir loops, al realizar operaciones como sumas, multiplicaciones, logaritmos, etc. Pandas hereda de NumPy esta capacidad y la adapta de dos formas:
In [22]:
np.log(bacteria)
Out[22]:
Esto facilita el realizar operaciones que implican combinar datos de distintas fuentes, algo que puede no ser tan simple al usar NumPy.
Para ejemplificar este comportamiento vamos a crear una nueva Series a partir de un diccionario, pero especificando los índices
In [23]:
bacteria2 = pd.Series(bacteria_dict,
index=['Cyanobacteria',
'Firmicutes',
'Actinobacteria'])
bacteria2
Out[23]:
Observemos dos detalles de este ejemplo. El orden en el que aparecen los elementos es el mismo que el orden especificado por el argumento index
, comparemos esto con el caso anterior donde creamos una serie a partir de un diccionario, pero sin especificar el índice.
El otro detalle es que hemos pasado un rótulo para un valor que no existe en el diccionario (y hemos omitido dos valores que si existen). Como resultado Pandas no devolvió un error, si no que interpretó que tenemos datos faltantes (missing data). El dato faltante se indica utilizando un tipo especial de float NaN
(del inglés Not A Number).
Los índices son una conveniencia para manipular datos haciendo referencia a nombres que nos puede resultar más familiares o convenientes (comparado con recordar la posición de los datos). Además, los índices son usados para alinear datos al operar con más de una serie, por ej podríamos querer obtener el total de bacterias en dos conjuntos de datos.
In [24]:
bacteria + bacteria2
Out[24]:
El resultado es una serie donde el índice corresponde a la unión de los índices originales. Pandas suma solo los valores para los cuales los índices de ambas Series coinciden! Y además propaga los valores faltantes (NaN
).
¿Qué sucede si intentamos sumar dos arreglos de NumPy de distinta longitud?
Es posible controlar que pasa con los datos faltantes usando el método .add()
(en vez del operador +
). Usando este método podemos indicar que cambie NaN
por otro valor.
In [25]:
bacteria.add(bacteria2, fill_value=0)
Out[25]:
Una variante sería hacer la operación y luego cambiar los NaN
por cualquier otro valor.
In [26]:
(bacteria + bacteria2).fillna(0)
Out[26]:
Al analizar datos es común que tengamos que trabajar con datos multivariados. Para esos casos es útil tener algo como una Series donde a cada índice le correspondan más de una columna de valores. Ese objeto se llama DataFrame
.
Un DataFrame
es una estructura de datos tabular que se puede pensar como una colección de Series
que comparten un mismo índice. También es posible pensar un DataFrame como una generalización de un arreglo de NumPy o la generalización de un diccionario.
In [27]:
datos = pd.DataFrame({'conteo':[632, 1638, 569, 115, 433, 1130, 754, 555],
'phylum':['Firmicutes', 'Proteobacteria', 'Actinobacteria', 'Bacteroidetes'] * 2,
'paciente':np.repeat([1, 2], 4)})
datos
Out[27]:
Lo primero que notamos es que Jupyter
le pone onda al DataFrame, y lo muestra como una tabla con algunas mejoras estéticas.
También podemos ver que contrario a un arreglo de NumPy en un DataFrame es posible tener datos de distinto tipo (enteros y strings en este caso). Además se ve que las columnas están ordenadas alfabéticamente, podemos cambiar el orden indexando el DataFrame en el orden preferido.
In [28]:
datos[['paciente', 'phylum', 'conteo']]
Out[28]:
Los DataFrame
tienen dos Index
:
In [29]:
datos.columns
Out[29]:
Es posible acceder a los valores de las columnas de forma similar a como lo haríamos en una serie o en un diccionario.
In [30]:
datos['conteo']
Out[30]:
También podemos hacerlo por atributo.
In [31]:
datos.conteo
Out[31]:
Esta sintáxis no funciona para todos los casos. Algunos casos donde fallará es si la columna continene espacios o si el nombre de la columna entra en conflicto con algún método existente para DataFrames, por ejemplo no sería raro que llamaramos a una columna con alguno de estos nombres all
, cov
, index
, mean
.
Una posible fuente de confusión es que la sintaxis que acabamos de ver devuelve filas en una Series
, pero columnas en un DataFrame
. Si queremos acceder a las filas de un DataFrame podemos hacerlo usando el atributo loc
:
In [32]:
datos.loc[3]
Out[32]:
¿Que pasa si intentamos acceder a una fila usando la sintaxis datos[3]
?
La Series
que se obtienen al indexar un DataFrame
es una vista (view) del DataFrame
y NO una copia. Por lo que hay que tener cuidado al manipularla, por ello Pandas nos devuelve una advertencia.
In [33]:
cont = datos['conteo']
cont
Out[33]:
In [34]:
cont[5] = 0
cont
Out[34]:
In [35]:
datos
Out[35]:
Si queremos modificar una Series
que proviene de un DataFrame
puede ser buena idea hacer una copia primero.
In [36]:
cont = datos['conteo'].copy()
cont[5] = 1000
datos
Out[36]:
Es posible agregar columnas a un DataFrame
mediante una asignación.
In [37]:
datos['año'] = 2013
datos
Out[37]:
Podemos agregar una Series
como una nueva columna en un DataFrame
, el resultado dependerá de los índices de ambos objetos.
In [38]:
tratamiento = pd.Series([0] * 4 + [1] * 4)
tratamiento
Out[38]:
In [39]:
datos['tratamiento'] = tratamiento
datos
Out[39]:
¿Qué sucede si intentamos agregar una nueva columna a partir de una lista cuya longitud no coincida con la del DataFrame
? ¿Y si en vez de una lista es una Series
?
In [40]:
datos['mes'] = ['enero'] * len(datos)
datos
Out[40]:
Podemos usar del
para eliminar columnas, de la misma forma que podemos hacerlo con diccionarios.
In [41]:
del datos['mes']
datos
Out[41]:
Es posible extraer los datos de un DataFrame
en forma de arreglo de NumPy.
In [42]:
datos.values
Out[42]:
Fijense que el dtype
del arreglo es object
. Esto se debe a la mezcla de enteros, strings y flotantes. El dtype
es elegido por Pandas automaticamente de forma tal de acomodar todos los tipos de valores presentes en el DataFrame
.
¿Qué hubiéramos obtenido si la columna phylum
no fuese parte del dataFrame?
Además es posible realizar operaciones típica de arrays sobre DataFrame
, por ejemplo transponerlo.
In [43]:
datos.T
Out[43]:
In [44]:
datos.index
Out[44]:
Los Index
son inmutables, no puede ser modificados una vez creados, lo cual pueden comprobar al descomentar y ejecutar la siguiente celda.
In [45]:
#datos.index[0] = 15
La inmutabilidad del Index
tiene como función permitir que los Index
se compartan entre objetos sin riesgo de que se modifiquen en algún momento. Esto es lo que permite agregar una Series
a un DataFrame
aunque sus longitudes no coincidan, Pandas busca que sean los índices los que coincidan y si hiciera falta completa los valores que sean necesarios.
En principio es posible usar Python para leer cualquier archivo que uno desee, pero para el trabajo rutinario de anñalisis de datos es preferible tener a mano funciones enlatadas que permitan leer los formatos más comunes. NumPy provee de algunas funciones para leer archivos, como genfromtxt
y loadtxt
. Pandas ofrece funciones más vestátiles y robustas. Empecemos leyendo un archivo en formato csv
(comma separated values).
In [46]:
!head datos/microbioma.csv # este es un comando de Linux que nos permite ver las primeras lineas de un archivo
Pandas ofrece una función llamada read_csv
ideal para leer este tipo de datos:
In [47]:
mb = pd.read_csv('datos/microbioma.csv')
In [48]:
type(mb)
Out[48]:
De forma similar al comando head
de Linux, le podemos pedir a Pandas que nos nuestre solo las primeras lineas.
In [49]:
mb.head()
Out[49]:
Por defecto read_csv
usará la primer linea del archivo como encabezado (header). Este comportamiento lo podemos modificar usando el argumento header
.
In [50]:
pd.read_csv('datos/microbioma.csv', header=None).head()
Out[50]:
Por defecto read_csv
usa ,
como separadores, pero es posible modificar este comportamiento usando el argumento sep
. Un caso muy común es el de archivos que tienen una cantidad variable de espacios en blanco. En esos casos podemos usar una expresión regular:
sep='\s+'
'\s+'
quiere decir "1 o más espacios en blanco". Otro caso común son archivos separados por tabulaciones (tabs), en ese caso podemos usar \t
.
Si queremos omitir datos (por ejemplo datos mal tomados), podemos indicaselo a Pandas usando el argumento skiprows
:
In [51]:
pd.read_csv('datos/microbioma.csv', skiprows=[3,4,6]).head()
Out[51]:
También podemos indicarque que solo queremos importar unas pocas columnas, lo que puede ser muy útil cuando estamos haciendo pruebas y explorando los datos y queremos evitar importar un larga lista de datos.
In [52]:
pd.read_csv('datos/microbioma.csv', nrows=4)
Out[52]:
Pandas ofrece la capacidad de leer varios otros formatos incluyendo archivos xls
, xlsx
, JSON
, XML
, HDF5
, etc. Para más información leer la documentación de Pandas o Python for Data Analysis.
Se llama agregación de datos a las operaciones que reducen un conjunto de números a uno (o unos pocos) números. Entre estas operaciones se encuentran la media, mediana, desviación estándar, mínimo, máximo, conteo, suma, producto, etc.
Para averiguar el total de bacterias encontradas en tejido
en nuestro conjunto de datos podemos hacer:
In [53]:
mb['Tejido'].sum()
Out[53]:
Pandas ofrece además un método que describe un DataFrame utilizando varias de estas operaciones de agregado:
In [54]:
mb.describe()
Out[54]:
Es común al analizar datos, que nos interese agregar datos respecto de alguna variable en particular por ejemplo podríamos querer obtener los valores promedios por Grupo
. Para ello podemos usar el método groupby
.
In [55]:
mb.groupby('Grupo').mean().round(0)
Out[55]:
Conceptualmente podemos pensar que groupby
realiza los siguientes pasos:
DataFrame
en subgrupos según las variables que le pasemos.
In [56]:
mb.groupby('Paciente').mean()
Out[56]:
No estamos limitados a una sola variable
In [57]:
mb.groupby(['Grupo', 'Paciente']).mean()
Out[57]:
Al aplicar el método groupby
se obtiene un objeto sobre el cual se puede operar de forma flexible
In [58]:
mb.groupby('Grupo')
Out[58]:
Por ejemplo podemos indexarlo usando los nombres de las columnas, podríamos querer hacer esto para saber cuantos pacientes tenemos en cada grupo
In [59]:
mb.groupby('Grupo')['Paciente'].count()
Out[59]:
In [60]:
mb.groupby('Grupo')['Heces'].describe().round(2)
Out[60]:
El método aggregate
permite agregar datos de forma muy flexible. Este método acepta una lista de funciones (o incluso algunos strings asocidados a ciertas funciones), siempre y cuando estas funciones agreguen datos.
In [61]:
mb.groupby('Grupo')['Heces'].aggregate([min, np.median, 'max'])
Out[61]:
Incluso es posible pasar un diccionario indicando que operaciones (values
) se aplican sobre cuales columnas (keys
).
In [62]:
mb.groupby('Grupo').aggregate({'Heces': [min, max], 'Paciente': 'count'})
Out[62]:
In [63]:
da = mb.groupby('Grupo').aggregate({'Heces': [min, max], 'Paciente': 'count'})
Otro método que se puede aplicar sobre un objeto groupby
es filter
. Este método permite filtrar el resultado mediante una función arbitraria. Supongamos que fuese de interés saber cuales pacientes tuvieron un conteo mayor a 120 bacterias para todas las categorías taxonómicas medidas.
In [64]:
def filtro(x):
return np.all(x['Tejido'] > 120)
mb.groupby('Paciente').filter(filtro)
Out[64]:
Para mayor claridad de los resultados podríamos querer ordenar el DataFrame resultante en función de la columna 'Paciente'
In [65]:
mb.groupby('Paciente').filter(filtro).sort_values('Paciente')
Out[65]:
La función que pasamos a filter
debe ser una función que devuelva un booleano. Los grupos para los cuales esta función evaluen como True serán los que observaremos como resultado.
Otro método disponible es transform
. A diferencia de aggregate
y de filter
la función que pasamos debe devolver un resultado con el mismo shape
de los datos que le pasamos. Una transformación que es común al analizar datos es estandarizarlos (esto facilita el desempeño de muchos métodos y algoritmos).
In [66]:
def estandarizar(x):
return (x - x.mean()) / x.std()
mb.groupby('Paciente')[['Tejido', 'Heces']].transform(estandarizar)
Out[66]:
Una característica de Python que no vimos en la primer notebook es la posibilidad de escribir funciones anónimas, es decir funciones que no tienen nombre. Esto se conoce como funciones lambda. Siguiendo el ejemplo anterior la sintaxis para una función anónima que estandariza datos es:
lambda x: (x - x.mean()) / x.std()
Por lo que podríamos escribir.
In [67]:
mb.groupby('Paciente')[['Tejido', 'Heces']].transform(lambda x: (x - x.mean()) / x.std())
Out[67]:
Por último tenemos al método apply
, el cual nos permite aplicar una función arbitraria a los subgrupos. La función debe tomar un DataFrame
y devolver un DataFrame
, Series
o un escalar.
Si necesitaramos normalizar el conteo en las Heces
usando el conteo en el Tejido
.
In [68]:
def norm(x):
x['Heces'] /= x['Tejido'].sum()
return x
In [69]:
mb.groupby('Grupo').apply(norm)
Out[69]:
Vamos a cerrar esta sección con un ejemplo tomado de Python Data Science Handbook. Este ejemplo combina varias de las operaciones vistas hasta ahora. El objetivo es obtener un listado por década de la cantidad de exoplanetas descubiertos con diversos métodos.
Primero cargamos los datos
In [70]:
planets = sns.load_dataset('planets')
planets.head()
Out[70]:
Luego, apartir de la columna year
creamos una Series llamada decades
. Es importante notar que no estamos modificando el DataFrame planets
In [71]:
decade = (10 * (planets['year'] // 10)).astype(str) + 's'
decade.name = 'decade'
decade.head(8)
Out[71]:
Y por último aplicamos una combinación de operaciones en una sola linea!
In [72]:
planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
Out[72]:
Antes de continuar conviene detenerse a estudiar que es lo que hace cada elemento de la expresión anterior
Es común al analizar datos encontrarnos con datos faltantes. Las razones son variadas desde errores de transcripción, errores en la toma de muestra, observaciones incompletas, etc. En algunos casos estos datos faltantes quedan registrados simplemente como huecos en el conjunto de datos o usando algunos valores sentinelas como NaN
, None
o valores que esten claramente fuera del rango de los datos como podrían ser -9999
para valores de datos positivos o 999
para valores que, estamos seguros, son inferiores a 100.
In [73]:
!head datos/microbioma_faltantes.csv
In [74]:
mbm = pd.read_csv('datos/microbioma_faltantes.csv')
mbm.head(14)
Out[74]:
En el ejemplo anterior Pandas reconoció correctamente a NA
y a un campo vacío como datos faltantes, pero pasó por alto a ?
y a -99999
. Es facil pasar por alto estos errores, por lo que siempre es buena idea hacer gráficos de los datos y resúmenes como el siguiente:
In [75]:
mbm.describe()
Out[75]:
Se puede ver que el conteo (count) para Paciente
y Heces
no coinciden, que el valor más pequeño para Heces
es un número negativo cuando debería ser mayor o igual a cero. Y vemos que no tenemos descripción para Tejido
!
¿Se te ocurre por que falta la columna para Tejido
?
Podemos indicarle a Pandas que valores debe considerar como datos faltantes. Para eso usamos el argumento na_values
.
In [76]:
mbm = pd.read_csv('datos/microbioma_faltantes.csv', na_values=['?', -99999])
In [77]:
mbm.describe()
Out[77]:
Si fuese necesario especificar valores distintos para distintas columnas es posible pasar un diccionario a na_values
, indicando los nombres de las columnas y los valores a usar como indicadores.
Este es un buen momento para que pruebes como hacer esto antes de seguir con la nueva sección.
In [78]:
mbm.isnull()[:3] # y su opuesto .notnull()
Out[78]:
O podríamos querer eliminar los valores nulos. Esto es posible usando dropna()
. En el caso de una Series
es posible eliminar solo los valores nulos. En el caso de DataFrames
esto no es posible, a menos que eliminemos toda la fila (o columna) donde aparece ese valor nulo. Por defecto dropna()
eliminará todas las filas que contengan al menos un valor nulo.
In [79]:
mbm.dropna().head(11)
Out[79]:
Es posible que no estemos interesados en remover los valores nulos si no que en cambio nos interese rellenar los valores nulos con algún número que tenga sentido para nuestro análisis.
In [80]:
mbm.fillna(42).head(11)
Out[80]:
O simplemente completando con otros valores del propio DataFrame
.
In [81]:
mbm.fillna(method='ffill').head(11)
Out[81]:
In [82]:
dada = pd.read_csv('datos/microbioma.csv')
dada.head()
Out[82]:
In [83]:
ser1 = pd.Series(['A', 'B', 'C'], index=[1, 2, 3])
ser2 = pd.Series(['D', 'E', 'F'], index=[4, 5, 6])
pd.concat([ser1, ser2])
Out[83]:
In [84]:
df0 = pd.DataFrame({'A': [0, 2], 'B': [1, 3]})
df0
Out[84]:
In [85]:
df1 = pd.DataFrame({'A': [4, 6], 'B': [5, 7]})
df1
Out[85]:
Al concatenar los índices se preservan incluso si esto da origen a duplicados
In [86]:
pd.concat([df0, df1])
Out[86]:
Es posible concatenar ignorando los índices originales. En este caso Pandas generará un nuevo índice.
In [87]:
pd.concat([df0, df1], ignore_index=True)
Out[87]:
Como vimos al concatenar arrays es posible pasar el argumento axis
In [88]:
pd.concat([df0, df1], axis=1)
Out[88]:
En caso que sea necesario verificar que los índices no se superponen es posible pasar el argumento verify_integrity=True
.
In [89]:
df2 = pd.DataFrame({'A': [2, 6], 'B': [0, 1], 'C': [9, -1]})
df2
Out[89]:
En caso de que los nombre de las columnas no coincidan al contenar Pandas completará los espacios faltantes con NaN
.
In [90]:
pd.concat([df0, df2])
Out[90]:
concat
tiene un argumento llamado join
que por defecto toma el valor outer
, en este caso la concatenación funciona como una unión de los nombres de las columnas. Si cambiamos el valor de join
por inner
, entonces la concatenación se comporta como una intersección.
In [91]:
pd.concat([df0, df2], join='inner')
Out[91]:
Una alternativa es especificar cuales son las columnas que queremos concatenar, para ello debemos pasarle al argumento join_axes
una lista de Index
.
In [92]:
pd.concat([df0, df2], join_axes=[pd.Index(['B', 'C'])])
Out[92]:
In [93]:
df0.append(df1)
Out[93]:
Una diferencia importante entre este append()
y el que vimos para listas de Python es que Pandas no modifica el objeto original (como sucedía con las listas) si no que cada vez que hacemos un append()
estamos creando un nuevo objeto con un nuevo índice. Esto hace que este método no sea muy eficiente. Dicho de otras forma mientras las listas están pensadas para hacer uso repetido del método append()
los DataFrames no. Por lo tanto en caso de ser necesario hacer varios append()
sobre DataFrames es mejor crear una lista de DataFrames y concatenar esa lista una sola vez.
Pandas ofrece operaciones del tipo merge
y join
que son familiares para quienes hayan trabajado con bases de datos. merge
tiene implementado un subconjunto de lo que se conoce como algebra relacional que son un conjunto de reglas formales para manipular datos relacionales. Este conjunto de reglas funciona como los bloques básicos (o primitivas) con los que se construyen operaciones más complejas sobre los datos. Pandas implementa varios de estos bloques básicos.
Para mayor información por favor referirse a la documentación de Pandas y/o libros como Python Data Science Handbook y Python for Data Analysis
In [94]:
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]],
names=['año', 'visita'])
columns = pd.MultiIndex.from_product([['Ana', 'Brian', 'Carlos'], ['RC', 'Temp']],
names=['sujeto', 'tipo'])
datos = np.zeros((4, 6))
datos[:, 1::2] += np.random.normal(37, 0.5, (4, 3))
datos[:, ::2] += np.random.normal(65, 1, (4, 3))
datos_médicos = pd.DataFrame(np.round(datos, 1), index=index, columns=columns)
datos_médicos
Out[94]:
In [95]:
datos_médicos['Brian']
Out[95]:
Recordemos que son las columnas quienes tiene prioridad en un DataFrame
, si quisieramos el ritmo cardíaco de Ana:
In [96]:
datos_médicos['Ana', 'RC']
Out[96]:
También es posible usar los idexadores iloc
o loc
.
In [97]:
datos_médicos.iloc[:2, :2]
Out[97]:
A cada índice individual en loc
e iloc
se le puede pasar una tupla con índices.
In [98]:
datos_médicos.loc[:, ('Ana', 'RC')]
Out[98]:
Es necesario ser cuidadosos al intentar tomar slices por ejemplo intentar algo como
datos_médicos.loc[(:, 1), (:, 'RC')]
Daría un error de syntaxis. El resultado deseado se puede obtener empleando el objeto IndexSlice object
, que Pandas provee para este tipo de situación.
In [99]:
idx = pd.IndexSlice
datos_médicos.loc[idx[:, 1], idx[:, 'RC']]
Out[99]:
In [100]:
datos_médicos
Out[100]:
Así como en arrays y DataFrames es posible agregar datos (médias, sumas, etc) a lo largo de axes
es posible en DataFrame multi-indexados agregar datos a lo largo de niveles. Dado que tenemos registros de dos visitas por año podríamos querer saber el promedio anual, es decir promediar a lo largo del nivel año
.
In [101]:
media_anual = datos_médicos.mean(level='año')
media_anual
Out[101]:
Es posible hacer uso combinado del argumento level
y axis
. En el siguiente ejemplo promediamos a lo largo de las columnas, axis=1
y a lo largo del nivel 'tipo'
.
In [102]:
media_anual.mean(axis=1, level='tipo').round(1)
Out[102]:
In [103]:
mbm[['Tejido', 'Heces']].plot(kind='kde', xlim=(0));
Internamente los gráficos son generados usando MatplotLib por lo que tenemos libertad para modificarlos, usando la misma sintáxis de Matplotlib
.
In [104]:
mbm[['Tejido', 'Heces']].plot(kind='kde')
plt.xlim(0);
Hasta ahora hemos visto algunas de las funciones más básicas de Pandas, equipados con estos conocimientos haremos un análisis de un conjunto de datos muy usando en Ciencia de Datos, estos datos tienen información sobre el famoso y trágico viaje del transatlántico Titanic.
Usando este conjunto de datos intentaremos contestar una serie de preguntas relacionadas con las chances de supervivencia:
Podemos tomarnos un momento para pensar sobre estas preguntas y tratar de aventurar algunas respuestas, luego veremos si los datos se condicen con nuestras sospechas.
Empecemos cargando los datos
In [105]:
titanic = pd.read_csv('datos/Titanic.csv')
print(titanic.shape)
titanic.head()
Out[105]:
Se puede ver que tenemos información sobre 1310 personas (filas) que estuvieron a bordo y para cada persona tenemos 14 variables (columnas). En machinelearngua se le suele llamar features a las variables.
¿Qué tipos de variables se corresponden con cada columna?
Recordemos que podemos acceder a los nombres de las variables mediante el atributo columns
.
In [106]:
titanic.columns
Out[106]:
Para algunas de estas variables veamos que porcentaje de datos faltantes tenemos:
In [107]:
for i in ['fare', 'pclass', 'age', 'sex']:
print('{:8s}{:4.1f} % datos faltantes'.format(i, titanic[i].isnull().mean() * 100))
Podemos ver que los registros están casi completos excepto para la edad (age
), variable para la cual el 20% de los datos son faltantes. Luego volveremos sobre esto, por ahora tratemos de constestar cual fue el porcentaje de sobrevivientes. Dado que los sobrevivientes están indicados con 1.0
y los fallecidos con 0.0
, podemos obtener el porcentaje de sobrevivientes haciendo:
In [108]:
round(titanic['survived'].mean() * 100, 0)
Out[108]:
Pero esta no es la única forma de hacerlo, una forma alternativa de calcular el número de sobrevivientes y fallecidos es:
In [109]:
(titanic['survived'].value_counts(normalize=True) * 100).round(0)
Out[109]:
¿Habrá alguna diferencia para los sobrevivientes en función del costo del boleto que pagaron?
In [110]:
boleto_fallecidos = titanic['fare'][titanic['survived'] == 0]
boleto_sobrevivientes = titanic['fare'][titanic['survived'] == 1]
In [111]:
'£{:.0f}, £{:.0f}'.format(boleto_fallecidos.mean(), boleto_sobrevivientes.mean())
Out[111]:
Podemos ver que en promedio el grupo de fallecidos pagó menos que el grupo de sobrevivientes.
Veamos estos mismos datos, pero de forma gráfica.
In [112]:
boleto_fallecidos.plot(kind='kde')
boleto_sobrevivientes.plot(kind='kde')
plt.xlim(0); # valores por debajo de 0 no tienen sentido
In [113]:
sns.violinplot(x="survived", y="fare", data=titanic[titanic['fare'] < 200]);
Veamos que además de la variable continua fare
tenemos la variable discreta pclass
que toma 3 valores de acuerdo a si las personas viajaban en primera (1), segunda (2) o tercera (3) clase. Veamos que nos dicen los datos cuando exploramos la tasa de supervivencia en función de la clase.
In [114]:
sns.barplot(x="pclass", y="survived", data=titanic);
El plot anterior incluye barras de error, por defecto, es posible removerlas pasando como argumento ci=None
.
¿Consideran correcto usar barras de error en este caso particular? Discutan y fundamenten con sus pares
Ya hemos contestado las dos primeras preguntas que nos hicimos. Ahora llegó el turno de explorar la tercer pregunta. Usaremos la siguiente lista como guía para explorar estas preguntas y para obtener información de este conjunto de datos, pero somos libres de ir más allá de este listado y sumar otras preguntas o análisis.
sns.barplot
tiene un argumento llamado hue
que podemos usar para combinar información de clase, género y supervivencia. crosstab
que puede sernos útil para analizar estas diferencias de genero. Esta función tiene varios argumentos probemos usar normalize
.Seaborn incluye varios datasets uno de ellos se conoce como iris y contiene medidas de 4 variables para 3 especies de flores. Estos datos se pueden cargar en un DataFrame haciendo:
iris = sns.load_dataset('iris')
Exploren este conjunto de datos usando las herramientas aprendidas hasta el momento.