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).

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]:
'0.25.3'

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]


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


Out[9]:
a    0.682555
b    0.410991
c   -0.940965
d    2.032562
e   -0.017147
dtype: float64

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]:
b    1
a    0
c    2
dtype: int64

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]:
a    0.682555
d    2.032562
dtype: float64

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]:
0.6825546509190483

In [13]:
s['e']=12

s


Out[13]:
a     0.682555
b     0.410991
c    -0.940965
d     2.032562
e    12.000000
dtype: float64

In [14]:
'e' in s


Out[14]:
True

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


Out[15]:
nan

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]:
b     0.410991
c    -0.940965
d     2.032562
e    12.000000
dtype: float64

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


Out[17]:
a    0.682555
b    0.410991
c   -0.940965
d    2.032562
dtype: float64

In [18]:
s[1:]+s[:-1]


Out[18]:
a         NaN
b    0.821983
c   -1.881930
d    4.065125
e         NaN
dtype: float64

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]:
a     0.682555
b     0.410991
c    -0.940965
d     2.032562
e    12.000000
Name: My_index, dtype: float64

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]:
-0.940965     1
 0.682555     1
 2.032562     1
 0.410991     1
 12.000000    1
dtype: int64

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:


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]:
0 1 2 3 4
0 65 60 60 45 60
1 75 35 50 75 40
2 85 80 30 20 75
3 75 45 30 70 80
4 80 55 90 40 45
5 90 60 95 15 45
6 60 55 45 55 40

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]:
Daño Precisión Alcance Cadencia Movilidad
0 65 60 60 45 60
1 75 35 50 75 40
2 85 80 30 20 75
3 75 45 30 70 80
4 80 55 90 40 45
5 90 60 95 15 45
6 60 55 45 55 40

Podemos consultar los índices y las columnas:


In [23]:
df.index, df.columns


Out[23]:
(RangeIndex(start=0, stop=7, step=1),
 Index(['Daño', 'Precisión', 'Alcance', 'Cadencia', 'Movilidad'], dtype='object'))

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]:
one two
a 1.0 1.0
b 2.0 2.0
c 3.0 3.0
d NaN 4.0

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


Out[25]:
two three
a 1.0 NaN
b 2.0 NaN
c 3.0 NaN
d 4.0 NaN

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]:
Daño Precisión Alcance Cadencia Movilidad
count 7.000000 7.000000 7.000000 7.000000 7.000000
mean 75.714286 55.714286 57.142857 45.714286 55.000000
std 10.578505 13.972763 26.435006 22.990681 16.832508
min 60.000000 35.000000 30.000000 15.000000 40.000000
25% 70.000000 50.000000 37.500000 30.000000 42.500000
50% 75.000000 55.000000 50.000000 45.000000 45.000000
75% 82.500000 60.000000 75.000000 62.500000 67.500000
max 90.000000 80.000000 95.000000 75.000000 80.000000

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.


In [27]:
df['Precisión']


Out[27]:
0    60
1    35
2    80
3    45
4    55
5    60
6    55
Name: Precisión, dtype: int64

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


Out[28]:
Daño Precisión Alcance Cadencia Movilidad Dummy Es_preciso
0 65 60 60 45 60 3900 True
1 75 35 50 75 40 3750 False
2 85 80 30 20 75 2550 True
3 75 45 30 70 80 2250 False
4 80 55 90 40 45 7200 False
5 90 60 95 15 45 8550 True
6 60 55 45 55 40 2700 False

Es posible calcular funciones numéricas sobre algunas columnas:


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


Out[29]:
Precisión    55.714286
Alcance      57.142857
dtype: float64

Para borrar una columna, usamos del:


In [30]:
del df['Dummy']
df


Out[30]:
Daño Precisión Alcance Cadencia Movilidad Es_preciso
0 65 60 60 45 60 True
1 75 35 50 75 40 False
2 85 80 30 20 75 True
3 75 45 30 70 80 False
4 80 55 90 40 45 False
5 90 60 95 15 45 True
6 60 55 45 55 40 False

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]:
one two one_trunc
a 1.0 1.0 1.0
b 2.0 2.0 2.0
c 3.0 3.0 NaN
d NaN 4.0 NaN

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]:
0    60
1    35
2    80
3    45
4    55
5    60
6    55
Name: Precisión, dtype: int64

In [33]:
df[['Precisión', 'Alcance']]


Out[33]:
Precisión Alcance
0 60 60
1 35 50
2 80 30
3 45 30
4 55 90
5 60 95
6 55 45

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]:
Daño Precisión Alcance Cadencia Movilidad Es_preciso
Arma
M16 Evil Clown 65 60 60 45 60 True
S36 Evil Clown 75 35 50 75 40 False
BY15 SnowFlakes 85 80 30 20 75 True
MSMC Ancient Runes 75 45 30 70 80 False
XPR-50 April's Fool 80 55 90 40 45 False
DLQ33 DeepShark 90 60 95 15 45 True
M4LMG RibbonExplosion 60 55 45 55 40 False

In [35]:
df.index # El índice ahora cambió


Out[35]:
Index(['M16 Evil Clown', 'S36 Evil Clown', 'BY15 SnowFlakes',
       'MSMC Ancient Runes', 'XPR-50 April's Fool', 'DLQ33 DeepShark',
       'M4LMG RibbonExplosion'],
      dtype='object', name='Arma')

Para seleccionar una fila, existen varias formas diferentes. Si conocemos su index, utilizamos loc:


In [36]:
df.loc['BY15 SnowFlakes']


Out[36]:
Daño            85
Precisión       80
Alcance         30
Cadencia        20
Movilidad       75
Es_preciso    True
Name: BY15 SnowFlakes, dtype: object

Si conocemos el índice de su posición, utilizamos iloc


In [37]:
df.iloc[0]


Out[37]:
Daño            65
Precisión       60
Alcance         60
Cadencia        45
Movilidad       60
Es_preciso    True
Name: M16 Evil Clown, dtype: object

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]:
Daño Precisión Alcance Cadencia Movilidad Es_preciso
Arma
S36 Evil Clown 75 35 50 75 40 False
BY15 SnowFlakes 85 80 30 20 75 True

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]:
Daño Precisión Alcance Cadencia Movilidad Es_preciso
Arma
M16 Evil Clown True True True False True False
S36 Evil Clown True False False True False False
BY15 SnowFlakes True True False False True False
MSMC Ancient Runes True False False True True False
XPR-50 April's Fool True True True False False False
DLQ33 DeepShark True True True False False False
M4LMG RibbonExplosion True True False True False False

In [40]:
df[df>50]


Out[40]:
Daño Precisión Alcance Cadencia Movilidad Es_preciso
Arma
M16 Evil Clown 65 60.0 60.0 NaN 60.0 NaN
S36 Evil Clown 75 NaN NaN 75.0 NaN NaN
BY15 SnowFlakes 85 80.0 NaN NaN 75.0 NaN
MSMC Ancient Runes 75 NaN NaN 70.0 80.0 NaN
XPR-50 April's Fool 80 55.0 90.0 NaN NaN NaN
DLQ33 DeepShark 90 60.0 95.0 NaN NaN NaN
M4LMG RibbonExplosion 60 55.0 NaN 55.0 NaN NaN

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]:
Daño Precisión Alcance Cadencia Movilidad Dummy
M16 Evil Clown 65 60 60 45 60 NaN
S36 Evil Clown 75 35 50 75 40 NaN
BY15 SnowFlakes 85 80 30 20 75 NaN
MSMC Ancient Runes 75 45 30 70 80 NaN
XPR-50 April's Fool 80 55 90 40 45 NaN
DLQ33 DeepShark 90 60 95 15 45 NaN
M4LMG RibbonExplosion 60 55 45 55 40 NaN
Arctic.50 Bats 85 52 95 30 50 -1.0
XPR-50 RedTriangle 80 55 90 40 45 -1.0
M16 NeonTiger 65 60 60 45 60 -1.0
Arctic.50 RedTriangle 85 52 95 30 50 -1.0
BK57 JackFrost 48 65 90 63 60 -1.0
M4MLG RedTriangle 60 55 45 55 40 -1.0
AKS-74U NeonTiger 78 55 32 60 75 -1.0
PDW-57 ZombieGene 90 40 25 60 75 -1.0

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]:
Daño         65
Precisión    60
Alcance      60
Cadencia     45
Movilidad    60
Name: M16 Evil Clown, dtype: int64

In [45]:
df3 - df3.iloc[0]


Out[45]:
Daño Precisión Alcance Cadencia Movilidad
M16 Evil Clown 0 0 0 0 0
S36 Evil Clown 10 -25 -10 30 -20
BY15 SnowFlakes 20 20 -30 -25 15
MSMC Ancient Runes 10 -15 -30 25 20
XPR-50 April's Fool 15 -5 30 -5 -15
DLQ33 DeepShark 25 0 35 -30 -15
M4LMG RibbonExplosion -5 -5 -15 10 -20
Arctic.50 Bats 20 -8 35 -15 -10
XPR-50 RedTriangle 15 -5 30 -5 -15
M16 NeonTiger 0 0 0 0 0
Arctic.50 RedTriangle 20 -8 35 -15 -10
BK57 JackFrost -17 5 30 18 0
M4MLG RedTriangle -5 -5 -15 10 -20
AKS-74U NeonTiger 13 -5 -28 15 15
PDW-57 ZombieGene 25 -20 -35 15 15

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]:
M16 Evil Clown            90.0
S36 Evil Clown            60.0
BY15 SnowFlakes          112.5
MSMC Ancient Runes       120.0
XPR-50 April's Fool       67.5
DLQ33 DeepShark           67.5
M4LMG RibbonExplosion     60.0
Arctic.50 Bats            75.0
XPR-50 RedTriangle        67.5
M16 NeonTiger             90.0
Arctic.50 RedTriangle     75.0
BK57 JackFrost            90.0
M4MLG RedTriangle         60.0
AKS-74U NeonTiger        112.5
PDW-57 ZombieGene        112.5
Name: Movilidad, dtype: float64

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).


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


Out[47]:
Daño Precisión
M16 Evil Clown 65 60
S36 Evil Clown 75 35
BY15 SnowFlakes 85 80
MSMC Ancient Runes 75 45
XPR-50 April's Fool 80 55

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:


In [48]:
df3[df3['Precisión']>50]


Out[48]:
Daño Precisión Alcance Cadencia Movilidad
M16 Evil Clown 65 60 60 45 60
BY15 SnowFlakes 85 80 30 20 75
XPR-50 April's Fool 80 55 90 40 45
DLQ33 DeepShark 90 60 95 15 45
M4LMG RibbonExplosion 60 55 45 55 40
Arctic.50 Bats 85 52 95 30 50
XPR-50 RedTriangle 80 55 90 40 45
M16 NeonTiger 65 60 60 45 60
Arctic.50 RedTriangle 85 52 95 30 50
BK57 JackFrost 48 65 90 63 60
M4MLG RedTriangle 60 55 45 55 40
AKS-74U NeonTiger 78 55 32 60 75

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]:
M16 Evil Clown            True
S36 Evil Clown           False
BY15 SnowFlakes           True
MSMC Ancient Runes       False
XPR-50 April's Fool       True
DLQ33 DeepShark           True
M4LMG RibbonExplosion     True
Arctic.50 Bats            True
XPR-50 RedTriangle        True
M16 NeonTiger             True
Arctic.50 RedTriangle     True
BK57 JackFrost            True
M4MLG RedTriangle         True
AKS-74U NeonTiger         True
PDW-57 ZombieGene        False
Name: Precisión, dtype: bool

... 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]:
Daño Precisión Alcance Cadencia Movilidad
BY15 SnowFlakes 85 80 30 20 75
XPR-50 April's Fool 80 55 90 40 45
DLQ33 DeepShark 90 60 95 15 45
Arctic.50 Bats 85 52 95 30 50
XPR-50 RedTriangle 80 55 90 40 45
Arctic.50 RedTriangle 85 52 95 30 50
PDW-57 ZombieGene 90 40 25 60 75

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]:
Alcance Cadencia
M16 Evil Clown 60 45
BY15 SnowFlakes 30 20
XPR-50 April's Fool 90 40
DLQ33 DeepShark 95 15
M4LMG RibbonExplosion 45 55
Arctic.50 Bats 95 30
XPR-50 RedTriangle 90 40
M16 NeonTiger 60 45
Arctic.50 RedTriangle 95 30
BK57 JackFrost 90 63
M4MLG RedTriangle 45 55
AKS-74U NeonTiger 32 60

In [52]:
df3


Out[52]:
Daño Precisión Alcance Cadencia Movilidad
M16 Evil Clown 65 60 60 45 60
S36 Evil Clown 75 35 50 75 40
BY15 SnowFlakes 85 80 30 20 75
MSMC Ancient Runes 75 45 30 70 80
XPR-50 April's Fool 80 55 90 40 45
DLQ33 DeepShark 90 60 95 15 45
M4LMG RibbonExplosion 60 55 45 55 40
Arctic.50 Bats 85 52 95 30 50
XPR-50 RedTriangle 80 55 90 40 45
M16 NeonTiger 65 60 60 45 60
Arctic.50 RedTriangle 85 52 95 30 50
BK57 JackFrost 48 65 90 63 60
M4MLG RedTriangle 60 55 45 55 40
AKS-74U NeonTiger 78 55 32 60 75
PDW-57 ZombieGene 90 40 25 60 75

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]:
Daño Precisión Alcance Cadencia Movilidad Tipo
M16 Evil Clown 65 60 60 45 60 Fusil de Asalto
S36 Evil Clown 75 35 50 75 40 Ametralladora
BY15 SnowFlakes 85 80 30 20 75 Escopeta
MSMC Ancient Runes 75 45 30 70 80 Ametralladora Ligera
XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión
DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión
M4LMG RibbonExplosion 60 55 45 55 40 Ametralladora
Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
M16 NeonTiger 65 60 60 45 60 Fusil de Asalto
Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión
BK57 JackFrost 48 65 90 63 60 Fusil de Asalto
M4MLG RedTriangle 60 55 45 55 40 Ametralladora Ligera
AKS-74U NeonTiger 78 55 32 60 75 Subfusil
PDW-57 ZombieGene 90 40 25 60 75 Subfusil

Listemos solamente los Fusiles de Precisión


In [54]:
df3[df3['Tipo']=='Fusil de Precisión']


Out[54]:
Daño Precisión Alcance Cadencia Movilidad Tipo
XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión
DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión
Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión

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]:
Daño Precisión Alcance Cadencia Movilidad Tipo
XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión
Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión
DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión

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


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


Out[56]:
Tipo
Ametralladora           45.000000
Ametralladora Ligera    50.000000
Escopeta                80.000000
Fusil de Asalto         61.666667
Fusil de Precisión      54.800000
Subfusil                47.500000
Name: Precisión, dtype: float64

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]:
Daño Precisión Alcance Cadencia Movilidad
mean std mean std mean std mean std mean std
Tipo
Ametralladora 67.500000 10.606602 45.000000 14.142136 47.5 3.535534 65.0 14.142136 40 0.000000
Ametralladora Ligera 67.500000 10.606602 50.000000 7.071068 37.5 10.606602 62.5 10.606602 60 28.284271
Escopeta 85.000000 NaN 80.000000 NaN 30.0 NaN 20.0 NaN 75 NaN
Fusil de Asalto 59.333333 9.814955 61.666667 2.886751 70.0 17.320508 51.0 10.392305 60 0.000000
Fusil de Precisión 84.000000 4.183300 54.800000 3.271085 93.0 2.738613 31.0 10.246951 47 2.738613
Subfusil 84.000000 8.485281 47.500000 10.606602 28.5 4.949747 60.0 0.000000 75 0.000000

Podemos aplicar diferentes funciones a diferentes columnas...


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


Out[58]:
Daño Alcance
mean std mean
Tipo
Ametralladora 67.500000 10.606602 47.5
Ametralladora Ligera 67.500000 10.606602 37.5
Escopeta 85.000000 NaN 30.0
Fusil de Asalto 59.333333 9.814955 70.0
Fusil de Precisión 84.000000 4.183300 93.0
Subfusil 84.000000 8.485281 28.5

Y podemos también aplicarlo a todas las filas de nuestro DataFrame:


In [59]:
df3.agg([np.mean, np.std])


Out[59]:
Daño Precisión Alcance Cadencia Movilidad
mean 74.733333 54.933333 62.133333 46.866667 56.000000
std 12.498381 10.498072 27.601415 17.872032 14.417252

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


Out[60]:
54.93333333333333

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.


In [61]:
df3.loc['PDW-57 ZombieGene']


Out[61]:
Daño               90
Precisión          40
Alcance            25
Cadencia           60
Movilidad          75
Tipo         Subfusil
Name: PDW-57 ZombieGene, dtype: object

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]:
Daño Precisión Alcance Cadencia Movilidad Tipo
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
PDW-57 ZombieGene 90 40 25 60 75 Subfusil

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]:
90

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]:
Daño Precisión Alcance Cadencia Movilidad Tipo
XPR-50 RedTriangle 80 55 180 40 45 Fusil de Precisión
PDW-57 ZombieGene 90 40 25 60 75 Subfusil

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]:
Daño Precisión Alcance
XPR-50 RedTriangle 80 55 180
PDW-57 ZombieGene 90 40 25

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.

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'


--2020-02-25 01:59:32--  https://raw.githubusercontent.com/gmonce/datascience/master/data/call_of_duty.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.0.133, 151.101.64.133, 151.101.128.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.0.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 835 [text/plain]
Saving to: ‘call_of_duty.csv’

call_of_duty.csv    100%[===================>]     835  --.-KB/s    in 0s      

Last-modified header missing -- time-stamps turned off.
2020-02-25 01:59:32 (199 MB/s) - ‘call_of_duty.csv’ saved [835/835]


In [ ]:
df4=pd.read_csv('call_of_duty.csv')

In [97]:
df4


Out[97]:
Arma Daño Precision Alcance Cadencia Movilidad Tipo
0 M4 45 60 70 45 60 Fusil de Asalto
1 M16 Evil Clown 65 60 60 45 60 Fusil de Asalto
2 S3 Evil Clown 75 35 50 75 40 Ametralladora
3 BY15 SnowFlakes 85 80 30 20 75 Escopeta
4 MSMC Ancient Runes 75 45 30 70 80 Ametralladora Ligera
5 XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión
6 DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión
7 M4LMG RibbonExplosion 60 55 45 55 40 Ametralladora
8 Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión
9 XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
10 M16 NeonTiger 65 60 60 45 60 Fusil de Asalto
11 Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión
12 BK57 JackFrost 48 65 90 63 60 Fusil de Asalto
13 M4MLG RedTriangle 60 55 45 55 40 Ametralladora Ligera
14 AKS-74U NeonTiger 78 55 32 60 75 Subfusil
15 PDW-57 ZombieGene 90 40 25 60 75 Subfusil

In [98]:
df4.set_index('Arma', inplace=True)
df4


Out[98]:
Daño Precision Alcance Cadencia Movilidad Tipo
Arma
M4 45 60 70 45 60 Fusil de Asalto
M16 Evil Clown 65 60 60 45 60 Fusil de Asalto
S3 Evil Clown 75 35 50 75 40 Ametralladora
BY15 SnowFlakes 85 80 30 20 75 Escopeta
MSMC Ancient Runes 75 45 30 70 80 Ametralladora Ligera
XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión
DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión
M4LMG RibbonExplosion 60 55 45 55 40 Ametralladora
Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión
M16 NeonTiger 65 60 60 45 60 Fusil de Asalto
Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión
BK57 JackFrost 48 65 90 63 60 Fusil de Asalto
M4MLG RedTriangle 60 55 45 55 40 Ametralladora Ligera
AKS-74U NeonTiger 78 55 32 60 75 Subfusil
PDW-57 ZombieGene 90 40 25 60 75 Subfusil

¿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]:
Daño Precision Alcance Cadencia Movilidad Tipo Alc&Prec
Arma
DLQ33 DeepShark 90 60 95 15 45 Fusil de Precisión 81.666667
Arctic.50 Bats 85 52 95 30 50 Fusil de Precisión 77.333333
Arctic.50 RedTriangle 85 52 95 30 50 Fusil de Precisión 77.333333
XPR-50 April's Fool 80 55 90 40 45 Fusil de Precisión 75.000000
XPR-50 RedTriangle 80 55 90 40 45 Fusil de Precisión 75.000000
BK57 JackFrost 48 65 90 63 60 Fusil de Asalto 67.666667
BY15 SnowFlakes 85 80 30 20 75 Escopeta 65.000000
M16 Evil Clown 65 60 60 45 60 Fusil de Asalto 61.666667
M16 NeonTiger 65 60 60 45 60 Fusil de Asalto 61.666667
M4 45 60 70 45 60 Fusil de Asalto 58.333333
AKS-74U NeonTiger 78 55 32 60 75 Subfusil 55.000000
S3 Evil Clown 75 35 50 75 40 Ametralladora 53.333333
M4LMG RibbonExplosion 60 55 45 55 40 Ametralladora 53.333333
M4MLG RedTriangle 60 55 45 55 40 Ametralladora Ligera 53.333333
PDW-57 ZombieGene 90 40 25 60 75 Subfusil 51.666667
MSMC Ancient Runes 75 45 30 70 80 Ametralladora Ligera 50.000000