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:
In [8]:
import numpy as np
import pandas as pd
# Este notebook fue elaborado con la versión 0.23.4 de Pandas
pd.__version__
Out[8]:
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]
In [9]:
s= pd.Series(np.random.randn(5), index=['a','b','c','d','e'])
s
Out[9]:
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:
In [10]:
d = pd.Series({'b': 1, 'a': 0, 'c': 2})
d
Out[10]:
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):
In [11]:
s[s > s.median()] # Seleccionamos los valores mayores a la mediana del array.
Out[11]:
Alternativamente, podemos ver a las series como un diccionario (de largo fijo) que puede accederse y cambiar valores a través de su índice:
In [12]:
s['a']
Out[12]:
In [13]:
s['e']=12
s
Out[13]:
In [14]:
'e' in s
Out[14]:
In [15]:
s.get(['f'],np.nan) # Si no ponemos el get, devuelve error
Out[15]:
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.
In [16]:
s[1:] # sin el primer elemento
Out[16]:
In [17]:
s[:-1] # sin el último elemento
Out[17]:
In [18]:
s[1:]+s[:-1]
Out[18]:
Las Series tienen un nombre, que está en el atributo name, y que puede especificarse al crearlo, o cambiarse con rename()
In [19]:
s2=s.rename('My_index')
s2
Out[19]:
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).
In [20]:
s.value_counts()
Out[20]:
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:
In [21]:
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
Out[21]:
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):
In [22]:
df=pd.DataFrame(a,columns=['Daño','Precisión','Alcance','Cadencia','Movilidad'])
df
Out[22]:
Podemos consultar los índices y las columnas:
In [23]:
df.index, df.columns
Out[23]:
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.
In [24]:
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
Out[24]:
In [25]:
pd.DataFrame(d, columns=['two','three'])
Out[25]:
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
In [26]:
df.describe()
Out[26]:
In [27]:
df['Precisión']
Out[27]:
In [28]:
df['Dummy']=df['Alcance']*df['Daño']
df['Es_preciso']=df['Precisión'] >= 60
df
Out[28]:
Es posible calcular funciones numéricas sobre algunas columnas:
In [29]:
df[['Precisión', 'Alcance']].mean()
Out[29]:
Para borrar una columna, usamos del
:
In [30]:
del df['Dummy']
df
Out[30]:
Si los valores que se pasan para crear una columna no son suficientes, se completan con NaN
In [31]:
df2['one_trunc'] = df2['one'][:2]
df2
Out[31]:
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)
In [32]:
df['Precisión']
Out[32]:
In [33]:
df[['Precisión', 'Alcance']]
Out[33]:
Vamos a agregarle a nuestro DataFrame los nombres de las armas, y lo ponemos como índice.
In [34]:
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
Out[34]:
In [35]:
df.index # El índice ahora cambió
Out[35]:
Para seleccionar una fila, existen varias formas diferentes. Si conocemos su index, utilizamos loc
:
In [36]:
df.loc['BY15 SnowFlakes']
Out[36]:
Si conocemos el índice de su posición, utilizamos iloc
In [37]:
df.iloc[0]
Out[37]:
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):
In [38]:
df[1:3]
Out[38]:
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).
In [39]:
df>50
Out[39]:
In [40]:
df[df>50]
Out[40]:
In [ ]:
del df['Es_preciso']
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.
In [42]:
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
Out[42]:
In [ ]:
del df3['Dummy']
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.
In [44]:
df3.iloc[0]
Out[44]:
In [45]:
df3 - df3.iloc[0]
Out[45]:
Los DF se pueden multiplicar por escalares, y se pueden aplicar operadores booleanos, exactamente igual que a los ndarrays.
In [46]:
df3['Movilidad']*1.5
Out[46]:
In [47]:
df3[['Daño','Precisión']].head(5)
Out[47]:
In [48]:
df3[df3['Precisión']>50]
Out[48]:
Lo que estamos haciendo es construir una Series de valores booleanos, para que me devuelva todas las filas que tienen True:
In [49]:
df3['Precisión']>50
Out[49]:
... 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:
In [50]:
df3[ (df3['Precisión']>=80) | (df3['Daño']>=80) ]
Out[50]:
Si queremos seleccionar solamente alguna de las columnas Y algunas de las filas, podemos especificarlas en el DataFrame resultado de la operación anterior.
In [51]:
df3[df3['Precisión']>50][['Alcance', 'Cadencia']]
Out[51]:
In [52]:
df3
Out[52]:
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.
In [53]:
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
Out[53]:
Listemos solamente los Fusiles de Precisión
In [54]:
df3[df3['Tipo']=='Fusil de Precisión']
Out[54]:
Es posible ordenar los resultados de una consulta (que es siempre un DataFrame):
In [55]:
df3[df3['Tipo']=='Fusil de Precisión'].sort_values(['Daño', 'Precisión'])
Out[55]:
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
In [56]:
df3.groupby('Tipo')['Precisión'].mean()
Out[56]:
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.
In [57]:
df3.groupby('Tipo').agg([np.mean, np.std])
Out[57]:
Podemos aplicar diferentes funciones a diferentes columnas...
In [58]:
df3.groupby('Tipo').agg({'Daño':[np.mean, np.std], 'Alcance':[np.mean]})
Out[58]:
Y podemos también aplicarlo a todas las filas de nuestro DataFrame:
In [59]:
df3.agg([np.mean, np.std])
Out[59]:
In [60]:
df3['Precisión'].agg('mean')
Out[60]:
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.
In [61]:
df3.loc['PDW-57 ZombieGene']
Out[61]:
También podemos especificar más de una fila, utilizando una lista (nótese que es una lista dentro de otra)
In [62]:
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]
Out[62]:
Si queremos obtener una celda, el primer elemento de loc nos selecciona la fila, y el segundo la columna:
In [63]:
df3.loc['XPR-50 RedTriangle', 'Alcance']
Out[63]:
Podemos setear el valor de una celda utilizando loc
In [64]:
df3.loc['XPR-50 RedTriangle', 'Alcance'] *=2
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]
Out[64]:
Finalmente, podemos especificar slices, indicando, en cada axis, el primer y último elemento (que estarán incluidos).
In [65]:
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene'], 'Daño':'Alcance']
Out[65]:
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)
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.
Primero, lo traemos a nuestro entorno (esto no es necesario si el archivo está local...), a través de wget
. La opción -N
permite que se traiga solamente si fue modificado.
In [95]:
!wget -N 'https://raw.githubusercontent.com/gmonce/datascience/master/data/call_of_duty.csv'
In [ ]:
df4=pd.read_csv('call_of_duty.csv')
In [97]:
df4
Out[97]:
In [98]:
df4.set_index('Arma', inplace=True)
df4
Out[98]:
¿Cuál es la mejor arma para un francotirador? Ordenamos por el promedio entre alcance y precisión
In [109]:
df4['Alc&Prec']=(df4['Alcance']+ df4['Precision']+ df4['Daño'])/3
df4.sort_values('Alc&Prec', ascending=False)
Out[109]: