Módulo 2

1. Introducción

En este módulo, investigaremos cómo Python, dado un conjunto de datos, puede realizar manipulaciones, proceder a su limpieza y llevar a cabo consultas a través de la librería pandas. Para resolver dudas sobre el uso de este módulo, cuatro buenos recursos son:

2. La estructura de datos Series

La estructura de datos Series, de la librería pandas, es una especie de mezcla entre las listas y los diccionarios de Python que estudiamos en el módulo anterior. Los elementos se almacenan en orden y tenemos a nuestra disposición una serie de etiquetas (labels), que nos permiten un acceso a ellos por nombre.

Empecemos importando la propia librería pandas que, por convención, habitualmente utiliza el alias pd. Desde el notebook de Jupyter podemos acceder a la documentación para la estructura de datos Series si tecleamos pd.Series?.

import pandas as pd
pd.Series?

En primer lugar, generemos una lista de animales y convirtámosla a un objeto de la clase Series:

animales = ["Tigre", "Oso", "Alce"]
pd.Series(animales)
0    Tigre
1      Oso
2     Alce
dtype: object

Análogamente, repitamos el proceso para una lista de números enteros:

numeros = [1, 2, 3]
pd.Series(numeros)
0    1
1    2
2    3
dtype: int64

En ambos casos, observamos una primera columna de índices numéricos, que podríamos utilizar para acceder a los elementos de esta estructura de la manera que estamos habituados en Python. Cabe destacar también el atributo dtype, diferente para ambos objetos y que se adapta automáticamente (aunque lo podemos declarar manualmente) al tipo de los elementos de la lista. Con ello, Python trabaja de manera más eficiente, tanto en memoria, como a la hora de llevar a cabo operaciones.

Por otro lado, la instrucción None nos permite indicar la ausencia de ciertos registros en nuestros conjuntos datos (los conocidos valores perdidos). No obstante, hemos de ser cautos, pues según el tipo del resto de elementos, None se almacena de manera diferente.

animales = ["Tigre", "Oso", None]
pd.Series(animales)
0    Tigre
1      Oso
2     None
dtype: object
numeros = [1, 2, None]
pd.Series(numeros)
0    1.0
1    2.0
2    NaN
dtype: float64

En el primer caso, vemos que se registra como un objeto, mientras que en el segundo lo hace como un número especial en coma flotante: NaN. En cualquier caso, es diferente a None, como podemos comprobar a continuación:

import numpy as np
np.nan == None
False

Curiosamente, también falla el test de comparación con respecto a sí mismo. La lógica tras esta decisión es que dos valores perdidos cualesquiera no tienen porqué coincidir.

np.nan == np.nan
False

Por tanto, para comprobar la existencia de valores perdidos, hemos de recurrir a la función isnan() de la librería NumPy:

np.isnan(np.nan)
True

A continuación, veamos cómo crear un objeto de tipo Series a partir de un diccionario. De proceder de tal modo, la columna de índices estará compuesta por las claves del propio diccionario (y no por números enteros):

deportes = {"Fútbol": "España",
            "Golf": "Italia",
            "Baloncesto": "Francia",
            "Kárate": "Japón"}
d = pd.Series(deportes)
d
Fútbol         España
Golf           Italia
Baloncesto    Francia
Kárate          Japón
dtype: object

Para acceder a los índices de la estructura creada, hemos de utilizar el atributo index:

d.index
Index(['Fútbol', 'Golf', 'Baloncesto', 'Kárate'], dtype='object')

De hecho, no tenemos que recurrir necesariamente al uso de un diccionario para acceder a etiquetas con nombres. En los ejemplos que vimos al principio de este apartado, basta que configuremos adecuadamente el valor del parámetro index para conseguir la misma funcionalidad que arriba:

a = pd.Series(["Tigre", "Oso", "Alce"], index=["India", "Estados Unidos", "Canadá"])
a
India             Tigre
Estados Unidos      Oso
Canadá             Alce
dtype: object

Es más, mediante dicho parámetro, podemos restringir la creación del objeto Series a los valores que nos interesen de un determinado diccionario (y automáticamente proveerá de valores NaN si alguna de las claves no figura en el mencionado diccionario):

deportes = {"Fútbol": "España",
            "Golf": "Italia",
            "Baloncesto": "Francia",
            "Kárate": "Japón"}
d = pd.Series(deportes, index=["Fútbol", "Golf", "Baloncesto"])
d
Fútbol         España
Golf           Italia
Baloncesto    Francia
dtype: object
d = pd.Series(deportes, index=["Fútbol", "Balonmano", "Golf"])
d
Fútbol       España
Balonmano       NaN
Golf         Italia
dtype: object

3. Extracción de elementos en Series

Para empezar, retomemos uno de los últimos ejemplos de la sección anterior:

deportes = {"Fútbol": "España",
            "Golf": "Italia",
            "Baloncesto": "Francia",
            "Kárate": "Japón"}
d = pd.Series(deportes)
d
Fútbol         España
Golf           Italia
Baloncesto    Francia
Kárate          Japón
dtype: object

Existen cuatro maneras diferentes de acceder a los valores almacenados en deportes. Si nos preguntamos a qué país está asociada la etiqueta "Golf" y sabemos que almacenamos este registro en segundo lugar, podemos emplear el atributo iloc:

d.iloc[1]
'Italia'

No obstante, es difícil recordar el orden en el que introdujimos los datos, y más cuando cierto tiempo ha transcurrido desde entonces. Por ello, es interesante que conozcamos la existencia del atributo loc, que nos permite acceder al valor mediante su etiqueta explícita:

d.loc["Golf"]
'Italia'

Nota técnica: notemos que iloc y loc son atributos, por lo que no hemos de utilizar paréntesis () cuando los empleamos.

La manera en que están programados los accesos en pandas busca conseguir la máxima legibilidad posible. Por ejemplo, si directamente utilizamos el operador de índice [] con un valor numérico, pandas empleará el atributo iloc; mientras que si es otro tipo de valor, recurrirá al atributo loc.

d[1]
'Italia'
d["Golf"]
'Italia'

Esta manera de proceder puede ser fuente de complicaciones cuando los índices son, asimismo, valores numéricos. En esta situación, pandas no puede determinar directamente si estamos accediendo a un valor vía referencia numérica o mediante etiquetas.

numeros = {90: "Noventa",
           91: "Noventa y uno",
           92: "Noventa y dos",
           93: "Noventa y tres"}
n = pd.Series(numeros)
n
90           Noventa
91     Noventa y uno
92     Noventa y dos
93    Noventa y tres
dtype: object
n[0]  # No accede al primer elemento, como en las listas. No hay índice etiquetado 0 aquí.
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

<ipython-input-20-56e5e6858fee> in <module>
----> 1 n[0]  # No accede al primer elemento, como en las listas. No hay índice etiquetado 0 aquí.


~\Anaconda3\lib\site-packages\pandas\core\series.py in __getitem__(self, key)
    866         key = com.apply_if_callable(key, self)
    867         try:
--> 868             result = self.index.get_value(self, key)
    869 
    870             if not is_scalar(result):


~\Anaconda3\lib\site-packages\pandas\core\indexes\base.py in get_value(self, series, key)
   4373         try:
   4374             return self._engine.get_value(s, k,
-> 4375                                           tz=getattr(series.dtype, 'tz', None))
   4376         except KeyError as e1:
   4377             if len(self) > 0 and (self.holds_integer() or self.is_boolean()):


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Int64HashTable.get_item()


KeyError: 0
n.iloc[0]

En estos casos, como acabamos de ver arriba, conviene utilizar explícitamente los atributos iloc y loc.

A continuación, veamos cómo trabajar con los valores de un objeto de tipo Series. Por ejemplo, nos puede interesar calcular la suma total de los elementos de una serie numérica de valores en coma flotante, almacenada mediante esta estructura de datos.

s = pd.Series([100.00, 120.00, 101.00, 3.00])
s
0    100.0
1    120.0
2    101.0
3      3.0
dtype: float64

Un posible enfoque consiste en iterar sobre los elementos de s a través de un bucle de tipo for:

total = 0
for item in s:
    total += item
print(total)
324.0

No obstante, la librería numpy dispone de un método que realiza la misma tarea de una manera más eficiente (en consumo de memoria y tiempo):

total = np.sum(s)
print(total)
324.0

¿Cómo podemos comprobar que, efectivamente, conviene utilizar las funciones que estas librerías proporcionan, en lugar de utilizar nuestros propios bucles? Llevemos a cabo un pequeño experimento declarando una serie de 10000 números aleatorios (cada uno de ellos comprendido entre 0 y 999) y procediendo a su suma. El magic command %%timeit nos permitirá acceder al tiempo de computación del proceso, que repetiremos 100 veces para conseguir así un tiempo medio representativo.

s = pd.Series(np.random.randint(0, 1000, 10000))
s.head()  # Imprime los cinco primeros elementos
0    970
1    887
2    127
3     75
4    170
dtype: int32
len(s)
10000
%%timeit -n 100
summary = 0
for item in s:
    summary += item
2.58 ms ± 182 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit -n 100
summary = np.sum(s)
277 µs ± 73.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

La explicación técnica de este resultado reside en que numpy efectúa operaciones algebraicas de manera vectorizada, enfoque de actuación mucho más eficiente que realizarlas elemento a elemento, como cuando utilizamos un bucle como el declarado arriba.

No obstante, la diferencia, aún existente, no llega a ser sorprendente. Repliquemos esta manera de proceder analizando ahora otra operación sencilla: sumar dos unidades a cada uno de los elementos de la serie generada.

Dicha tarea la podemos llevar a cabo directamente mediante el operador incremento correspondiente:

s += 2  # Suma 2 a s, elemento a elemento
s.head()
0    972
1    889
2    129
3     77
4    172
dtype: int32

O, en cualquier caso, a través de un bucle de tipo for:

for label, value in s.iteritems():
    s.set_value(label, value + 2)  # Produce un deprecated warning 
s.head()
FutureWarning: set_value is deprecated and will be removed in a future release. Please use .at[] or .iat[] accessors instead






0    974
1    891
2    131
3     79
4    174
dtype: int32

El anterior bloque de código arroja un mensaje (warning) avisándonos que la función set_value() está en desuso y nos recomienda utilizar los atributos .at[] o iat[]. Modifiquemos pues el código precedente:

for label, value in s.iteritems():
    s.at[label] = value + 2
s.head()
0    976
1    893
2    133
3     81
4    176
dtype: int32

Acto seguido, procedamos a realizar el mencionado experimento:

%%timeit -n 100
s = pd.Series(np.random.randint(0, 1000, 10000))
for label, value in s.iteritems():
    s.at[label] = value + 2
109 ms ± 10.2 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit -n 100
s = pd.Series(np.random.randint(0, 1000, 10000))
s += 2
725 µs ± 196 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

En resumen, si en algún momento nos encontramos iterando sobre un objeto de tipo Series, hemos de deternos y cuestionarnos si estamos llevando a cabo el procedimiento de la manera más adecuada (en términos de eficiencia).

Para finalizar esta sección, veamos algunas maneras de añadir información a un objeto declarado de tipo Series. En primer lugar, y de manera muy parecida a como realizamos el proceso cuando trabajamos con diccionarios, utilizando la estructura s.loc[] = value:

s = pd.Series([1, 2, 3])
s.loc["Animal"] = "Oso"
s
0           1
1           2
2           3
Animal    Oso
dtype: object

Nota: la librería pandas gestiona adecuadamente los índices y valores cuando, como en este caso, son de tipos diferentes (tenemos enteros y cadenas de texto), escogiendo la representación más general para representarlos.

En segundo lugar, la función append() resulta de gran utilidad a la hora de ampliar este tipo de estructura de datos. Además, veamos qué sucede cuando existen diferentes valores cuyo índice, en forma de etiqueta, coincide (situación imposible de conseguir cuando trabajamos, por ejemplo, con bases de datos relacionales):

deportes = pd.Series({"Fútbol": "España",
                      "Golf": "Italia",
                      "Baloncesto": "Francia",
                      "Kárate": "Japón"})
paises_balonmano = pd.Series(["Inglaterra", "Alemania", "Rusia", "Colombia"],
                            index=["Balonmano", "Balonmano", "Balonmano", "Balonmano"])
deportes_ampliado = deportes.append(paises_balonmano)
deportes
Fútbol         España
Golf           Italia
Baloncesto    Francia
Kárate          Japón
dtype: object
paises_balonmano
Balonmano    Inglaterra
Balonmano      Alemania
Balonmano         Rusia
Balonmano      Colombia
dtype: object
deportes_ampliado
Fútbol            España
Golf              Italia
Baloncesto       Francia
Kárate             Japón
Balonmano     Inglaterra
Balonmano       Alemania
Balonmano          Rusia
Balonmano       Colombia
dtype: object
deportes_ampliado.loc["Balonmano"]
Balonmano    Inglaterra
Balonmano      Alemania
Balonmano         Rusia
Balonmano      Colombia
dtype: object

Notas técnicas:

  • La función append(), así como sucede con otras funciones de esta librería, infiere qué tipo es el más adecuado para representar la nueva estructura generada. En el ejemplo anterior, como todo son cadenas de caracteres, no hay problema alguno.
  • Por otro lado, dicha función no modifica la estructura original, sino que devuelve una nueva, comportamiento que puede resultar un tanto extraño en Python, tal y como estamos acostumbrados a modificar objetos. En el ejemplo anterior, tras ejecutar la celda, observamos la variable deportes contiene únicamente los datos originales, aunque sobre ella hayamos utilizado el método append(). Como podemos observar a continuación, cuando trabajamos con listas dicha función produce un resultado diferente.
lista_original = [1, 2, 3]
lista_nueva = lista_original.append(4)

lista_original
[1, 2, 3, 4]

4. La estrutura de datos DataFrame

La estructura de datos DataFrame es, posiblemente, la gran protagonista de la librería pandas y con la que trabajaremos habitualmente a la hora de llevar a cabo análisis de datos. Se trata de una tabla compuesta por múltiples filas y columnas (conceptualmente estaríamos hablando de un array 2-dimensional), donde cada registro posee su propia etiqueta.

Podemos crear un DataFrame a partir de un conjunto de Series o de diccionarios, donde cada elemento represente una fila de la tabla que deseamos generar:

import pandas as pd

compra_1 = pd.Series({'Nombre': 'Alexis',
                      'Objeto comprado': 'Portátil',
                      'Coste': 622.50})
compra_2 = pd.Series({'Nombre': 'Ana',
                      'Objeto comprado': 'Auriculares',
                      'Coste': 7.50})
compra_3 = pd.Series({'Nombre': 'Marta',
                      'Objeto comprado': 'Comida para gatos',
                      'Coste': 15.25})

df = pd.DataFrame([compra_1, compra_2, compra_3], 
                  index=['Tienda 1', 'Tienda 1', 'Tienda 2'])
df.head()
Nombre Objeto comprado Coste
Tienda 1 Alexis Portátil 622.50
Tienda 1 Ana Auriculares 7.50
Tienda 2 Marta Comida para gatos 15.25

Nota: recordemos que no es necesario que las etiquetas asociadas a los registros sean únicas.

De manera similar a como procedíamos en anteriores secciones, podemos extraer información del DataFrame utilizando los atributos iloc y loc. Cabe destacar que, según la cantidad de información extraída, la librería pandas colapsa de manera adecuada el tipo de datos resultante.

Por ejemplo, si extraemos una columna de la tabla, el objeto resultante será de tipo Series:

df.loc["Tienda 2"]
Nombre                         Marta
Objeto comprado    Comida para gatos
Coste                          15.25
Name: Tienda 2, dtype: object
type(df.loc["Tienda 2"])
pandas.core.series.Series

De manera similar, podemos extraer información de la tabla utilizando múltiples índices (uno para la fila y otro para la columna). Por ejemplo, podríamos estar interesados en consultar los costes asociados a los productos comprados en Tienda 1. Para ello, tecleamos

df.loc["Tienda 1", "Coste"]
Tienda 1    622.5
Tienda 1      7.5
Name: Coste, dtype: float64
type(df.loc["Tienda 1", "Coste"])
pandas.core.series.Series

Al tratarse el resultado de un array unidimensional, observamos que su tipo se colapsa al de un objeto de tipo Series. En cambio, si extraemos una “subtabla” de la tabla, el objeto devuelto no varía de tipo y continúa perteneciendo a la clase DataFrame:

df.loc["Tienda 1"]
Nombre Objeto comprado Coste
Tienda 1 Alexis Portátil 622.5
Tienda 1 Ana Auriculares 7.5
type(df.loc["Tienda 1"])
pandas.core.frame.DataFrame

Finalmente, si extraemos un único valor concreto de la tabla, su tipo también se ve alterado:

df.loc["Tienda 2", "Coste"]
15.25
type(df.loc["Tienda 2", "Coste"])
numpy.float64
df.loc["Tienda 2", "Nombre"]
'Marta'
type(df.loc["Tienda 2", "Nombre"])
str

A continuación, imaginemos que estamos interesados en obtener todos los registros asociados a una columna, "Coste" por ejemplo. Una posible estrategia consistiría en trasponer el conjunto de datos y emplear, como antes, el atributo loc, ya que los nombres de las columnas pasan a ser los índices de los registros.

df.T
Tienda 1 Tienda 1 Tienda 2
Nombre Alexis Ana Marta
Objeto comprado Portátil Auriculares Comida para gatos
Coste 622.5 7.5 15.25
df.T.loc["Coste"]
Tienda 1    622.5
Tienda 1      7.5
Tienda 2    15.25
Name: Coste, dtype: object

Sin embargo, es un tanto tedioso proceder de tal forma. La librería pandas permite, directamente, utilizar también los atributos iloc y loc sobre los nombres de las columnas:

df["Coste"]
Tienda 1    622.50
Tienda 1      7.50
Tienda 2     15.25
Name: Coste, dtype: float64

Además, podemos incluso encadenar operadores de extracción de datos como sigue:

df.loc["Tienda 1"]["Coste"]
Tienda 1    622.5
Tienda 1      7.5
Name: Coste, dtype: float64
df.loc["Tienda 2"]["Coste"]
15.25

No obstante, hemos de ser cautos a la hora de proceder de tal manera, pues pandas devuelve una copia del objeto DataFrame, en lugar de una simple vista, con todos los costes asociados de memoria y tiempo de cálculo que ello conlleva.

A la hora de realizar consultas, esta peculiaridad no puede parecer muy importante, pero sí puede ser fuente de errores cuando estamos modificando datos de un conjunto de datos.

Nota: también podemos utilizar el operador : a la hora de extraer información de un conjunto de datos, tal y como estamos habituados a hacerlo cuando trabajamos con listas.

df.loc[:, ["Nombre", "Coste"]]
Nombre Coste
Tienda 1 Alexis 622.50
Tienda 1 Ana 7.50
Tienda 2 Marta 15.25

Acto seguido, veamos cómo descartar datos, acción para la cual la función drop() es ciertamente útil:

df.drop("Tienda 1")
Nombre Objeto comprado Coste
Tienda 2 Marta Comida para gatos 15.25

Nota técnica: la función drop(), como muchas de las implementadas en la librería pandas, no modifica el objeto original, sino que devuelve una copia del mismo sobre la cual se ha llevado a cabo la acción de interés.

df
Nombre Objeto comprado Coste
Tienda 1 Alexis Portátil 622.50
Tienda 1 Ana Auriculares 7.50
Tienda 2 Marta Comida para gatos 15.25

Hagamos una copia del conjunto de datos, utilizando la función copy(), y apliquemos después la función drop():

copia_df = df.copy()
copia_df = copia_df.drop("Tienda 1")
copia_df
Nombre Objeto comprado Coste
Tienda 2 Marta Comida para gatos 15.25

Por otro lado, cabe comentar que la función drop() posee dos interesantes parámetros:

  • inplace: permite que la actualización de datos se realice sobre el objeto original, en lugar de devolver una copia.
  • axis: dimensión que se descarta (0 para filas, 1 para columnas)

Para acceder a más detalles sobre la función, siempre conviene que consultemos su documentación asociada:

copia_df.drop?

Adicionalmente, mediante la combinación del operador índice y la instrucción del tenemos la posibilidad de descartar datos de nuestra tabla. Dicha combinación altera el objeto inicial en lugar de devolver una copia, por lo que hemos de proceder con cautela en su uso.

del copia_df["Nombre"]
copia_df
Objeto comprado Coste
Tienda 2 Comida para gatos 15.25

Finalmente, para añadir columnas únicamente hemos de seguir un patrón familiar a estas alturas:

df["Localización"] = None
df
Nombre Objeto comprado Coste Localización
Tienda 1 Alexis Portátil 622.50 None
Tienda 1 Ana Auriculares 7.50 None
Tienda 2 Marta Comida para gatos 15.25 None

Ejercicio: ¿cómo podríamos aplicar un descuento de un 20% a los artículos de la primera tienda?

df.loc["Tienda 1", "Coste"] *= 0.8
df
Nombre Objeto comprado Coste Localización
Tienda 1 Alexis Portátil 498.00 None
Tienda 1 Ana Auriculares 6.00 None
Tienda 2 Marta Comida para gatos 15.25 None

5. Lectura de archivos como DataFrame

Habitualmente, a la hora de realizar análisis de datos, importamos el conjunto de datos en un DataFrame y luego seleccionamos aquellas que nos resulten de interés para trabajar con ellas.

Como hemos advertido en anteriores secciones, la librería pandas acostumbra a devolver “vistas” de los DataFrames en lugar de copias de los mismos (debido a cuestiones de gestión de memoria y eficiencia en la realización de ciertas operaciones). Por tanto, podemos encontrar que algunas modificaciones que llevemos a cabo pueden tener impacto en el conjunto de datos original y este comportamiento es posible que no nos interese.

Por ejemplo, almacenemos en una variable los costes de los productos pertenecientes a la tabla de la sección anterior:

costes = df["Coste"]
costes
Tienda 1    498.00
Tienda 1      6.00
Tienda 2     15.25
Name: Coste, dtype: float64

Si ahora incrementamos en dos unidades el coste de cada producto, no solo ve alterado su valor la variable costes, sino también el conjunto de datos original df:

costes += 2
costes
Tienda 1    500.00
Tienda 1      8.00
Tienda 2     17.25
Name: Coste, dtype: float64
df
Nombre Objeto comprado Coste Localización
Tienda 1 Alexis Portátil 500.00 None
Tienda 1 Ana Auriculares 8.00 None
Tienda 2 Marta Comida para gatos 17.25 None

A continuación, veamos cómo importar los contenidos de un archivo, de tipo CSV, en una estructura de datos de tipo DataFrame. El fichero olympics.csv (ubicado en el directorio data) registra el número de medallas que cada país ha conseguido en los difirentes tipos de olimpiadas:

df = pd.read_csv("data/olympics.csv")
df.head()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 NaN № Summer 01 ! 02 ! 03 ! Total № Winter 01 ! 02 ! 03 ! Total № Games 01 ! 02 ! 03 ! Combined total
1 Afghanistan (AFG) 13 0 0 2 2 0 0 0 0 0 13 0 0 2 2
2 Algeria (ALG) 12 5 2 8 15 3 0 0 0 0 15 5 2 8 15
3 Argentina (ARG) 23 18 24 28 70 18 0 0 0 0 41 18 24 28 70
4 Armenia (ARM) 5 1 2 9 12 6 0 0 0 0 11 1 2 9 12

A la vista de la tabla anterior, parece que la primera fila únicamente numera las columnas, resiendo en la segunda los nombres de cabecera de dichas columnas. Por tanto, procederemos a saltar la lectura de la línea inicial, utilizando para ello el parámetro skiprows.

Por otro lado, la primera columna contiene los nombres de los distintos países, siendo estos valores perfectos candidatos para conformar los índices de cada uno de los registros. Para conseguir tal característica, simplemente indicamos que la columna de índices es la primera mediante el parámetro index_col=0.

df = pd.read_csv("data/olympics.csv", index_col=0, skiprows=1)
df.head()
№ Summer 01 ! 02 ! 03 ! Total № Winter 01 !.1 02 !.1 03 !.1 Total.1 № Games 01 !.2 02 !.2 03 !.2 Combined total
Afghanistan (AFG) 13 0 0 2 2 0 0 0 0 0 13 0 0 2 2
Algeria (ALG) 12 5 2 8 15 3 0 0 0 0 15 5 2 8 15
Argentina (ARG) 23 18 24 28 70 18 0 0 0 0 41 18 24 28 70
Armenia (ARM) 5 1 2 9 12 6 0 0 0 0 11 1 2 9 12
Australasia (ANZ) [ANZ] 2 3 4 5 12 0 0 0 0 0 2 3 4 5 12
df.columns
Index(['№ Summer', '01 !', '02 !', '03 !', 'Total', '№ Winter', '01 !.1',
       '02 !.1', '03 !.1', 'Total.1', '№ Games', '01 !.2', '02 !.2', '03 !.2',
       'Combined total'],
      dtype='object')

Acto seguido, encontramos dos detalles curiosos:

  • La representación para las medallas de oro, plata y bronce es, cuanto menos, extraña: 01 !, 02 ! y 03 !.
  • Existen columnas con las mismas etiquetas (las asociadas a los tipos de medallas y a los totales), práctica en absoluto recomendable por dar lugar a confusiones de manera muy sencilla. La librería pandas gestiona esta situación incluyendo valores numéricos al final del nombre de las repetidas (.1, .2, .3…) para así poder diferenciarlas.

Podemos bien editar directamente el propio archivo CSV, bien modificar las etiquetas conflictivas, desde Python con la librería pandas, utilizando la función rename():

for col in df.columns:
    if col[:2]=='01':
        df.rename(columns={col:'Gold' + col[4:]}, inplace=True)
    if col[:2]=='02':
        df.rename(columns={col:'Silver' + col[4:]}, inplace=True)
    if col[:2]=='03':
        df.rename(columns={col:'Bronze' + col[4:]}, inplace=True)
    if col[:1]=='№':
        df.rename(columns={col:'#' + col[1:]}, inplace=True) 

df.head()
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Afghanistan (AFG) 13 0 0 2 2 0 0 0 0 0 13 0 0 2 2
Algeria (ALG) 12 5 2 8 15 3 0 0 0 0 15 5 2 8 15
Argentina (ARG) 23 18 24 28 70 18 0 0 0 0 41 18 24 28 70
Armenia (ARM) 5 1 2 9 12 6 0 0 0 0 11 1 2 9 12
Australasia (ANZ) [ANZ] 2 3 4 5 12 0 0 0 0 0 2 3 4 5 12

Notemos el uso del parámetro inplace, que permite modificar el objeto original, puesto que su valor está declarado como True. Por otro lado, hemos de proceder con cautela a la hora de renombrar las columnas para no perder la unicidad durante el proceso, de ahí la justificación de la concatenación + col[4:].

6. Consultas en DataFrame

Antes de abordar cómo realizar consultas en DataFrame, hemos de introducir el concepto de máscara booleana (boolean masking), puesto que esta estrategia es la que permite consultar el conjunto de datos de una manera rápida y eficiente.

La idea es construir un array (unidimensional o multidimensional) de valores lógicos True o False, que luego utilizaremos para extraer la información que nos interese del conjunto de datos.

Por ejemplo, utilizando la tabla de datos que figura en la sección anterior y que recoge el número de medallas obtenidas por cada país en las olimpiadas, podemos estar interesados en consultar qué países han conseguido al menos una medalla de oro en las de verano (recordemos que esta información se almacenaba en la columna "Gold"). Si escribimos

df["Gold"] > 0
Afghanistan (AFG)                               False
Algeria (ALG)                                    True
Argentina (ARG)                                  True
Armenia (ARM)                                    True
Australasia (ANZ) [ANZ]                          True
Australia (AUS) [AUS] [Z]                        True
Austria (AUT)                                    True
Azerbaijan (AZE)                                 True
Bahamas (BAH)                                    True
Bahrain (BRN)                                   False
Barbados (BAR) [BAR]                            False
Belarus (BLR)                                    True
Belgium (BEL)                                    True
Bermuda (BER)                                   False
Bohemia (BOH) [BOH] [Z]                         False
Botswana (BOT)                                  False
Brazil (BRA)                                     True
British West Indies (BWI) [BWI]                 False
Bulgaria (BUL) [H]                               True
Burundi (BDI)                                    True
Cameroon (CMR)                                   True
Canada (CAN)                                     True
Chile (CHI) [I]                                  True
China (CHN) [CHN]                                True
Colombia (COL)                                   True
Costa Rica (CRC)                                 True
Ivory Coast (CIV) [CIV]                         False
Croatia (CRO)                                    True
Cuba (CUB) [Z]                                   True
Cyprus (CYP)                                    False
                                                ...  
Sri Lanka (SRI) [SRI]                           False
Sudan (SUD)                                     False
Suriname (SUR) [E]                               True
Sweden (SWE) [Z]                                 True
Switzerland (SUI)                                True
Syria (SYR)                                      True
Chinese Taipei (TPE) [TPE] [TPE2]                True
Tajikistan (TJK)                                False
Tanzania (TAN) [TAN]                            False
Thailand (THA)                                   True
Togo (TOG)                                      False
Tonga (TGA)                                     False
Trinidad and Tobago (TRI) [TRI]                  True
Tunisia (TUN)                                    True
Turkey (TUR)                                     True
Uganda (UGA)                                     True
Ukraine (UKR)                                    True
United Arab Emirates (UAE)                       True
United States (USA) [P] [Q] [R] [Z]              True
Uruguay (URU)                                    True
Uzbekistan (UZB)                                 True
Venezuela (VEN)                                  True
Vietnam (VIE)                                   False
Virgin Islands (ISV)                            False
Yugoslavia (YUG) [YUG]                           True
Independent Olympic Participants (IOP) [IOP]    False
Zambia (ZAM) [ZAM]                              False
Zimbabwe (ZIM) [ZIM]                             True
Mixed team (ZZX) [ZZX]                           True
Totals                                           True
Name: Gold, Length: 147, dtype: bool

Una vez hemos construido la máscara de valores lógicos (boolean mask), ya solo nos resta aplicarla al conjunto de datos original mediante la función where():

solo_oro = df.where(df["Gold"] > 0)
solo_oro.head()
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Afghanistan (AFG) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
Algeria (ALG) 12.0 5.0 2.0 8.0 15.0 3.0 0.0 0.0 0.0 0.0 15.0 5.0 2.0 8.0 15.0
Argentina (ARG) 23.0 18.0 24.0 28.0 70.0 18.0 0.0 0.0 0.0 0.0 41.0 18.0 24.0 28.0 70.0
Armenia (ARM) 5.0 1.0 2.0 9.0 12.0 6.0 0.0 0.0 0.0 0.0 11.0 1.0 2.0 9.0 12.0
Australasia (ANZ) [ANZ] 2.0 3.0 4.0 5.0 12.0 0.0 0.0 0.0 0.0 0.0 2.0 3.0 4.0 5.0 12.0

Como podemos observar, se conservan todos los registros, pero únicamente existen datos disponibles para aquellos que verifican la condición impuesta, esto es, que han conseguido al menos una medalla de oro en las olimpiadas de verano. En total son

solo_oro["Gold"].count()
100

es decir, 100 países de un total de

df["Gold"].count()
147

147 países que contiene el conjunto de datos original.

Habitualmente, los registros sin información asociada los descartaremos, haciendo uso para ello de la función dropna() que, por defecto, actúa sobre las filas (axis=0):

solo_oro = solo_oro.dropna()
solo_oro.head()
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Algeria (ALG) 12.0 5.0 2.0 8.0 15.0 3.0 0.0 0.0 0.0 0.0 15.0 5.0 2.0 8.0 15.0
Argentina (ARG) 23.0 18.0 24.0 28.0 70.0 18.0 0.0 0.0 0.0 0.0 41.0 18.0 24.0 28.0 70.0
Armenia (ARM) 5.0 1.0 2.0 9.0 12.0 6.0 0.0 0.0 0.0 0.0 11.0 1.0 2.0 9.0 12.0
Australasia (ANZ) [ANZ] 2.0 3.0 4.0 5.0 12.0 0.0 0.0 0.0 0.0 0.0 2.0 3.0 4.0 5.0 12.0
Australia (AUS) [AUS] [Z] 25.0 139.0 152.0 177.0 468.0 18.0 5.0 3.0 4.0 12.0 43.0 144.0 155.0 181.0 480.0

Al ser un tipo de acción habitual a la hora de llevar a cabo análisis de datos, los desarrollares de la librería pandas han incluido un atajo (mediante el operador índice [] al que le suministramos directamente la máscara booleana) para conseguir el mismo efecto de una manera más sencilla y, sobretodo, que destaca por su legibilidad:

solo_oro = df[df["Gold"] > 0]
solo_oro.head()
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Algeria (ALG) 12 5 2 8 15 3 0 0 0 0 15 5 2 8 15
Argentina (ARG) 23 18 24 28 70 18 0 0 0 0 41 18 24 28 70
Armenia (ARM) 5 1 2 9 12 6 0 0 0 0 11 1 2 9 12
Australasia (ANZ) [ANZ] 2 3 4 5 12 0 0 0 0 0 2 3 4 5 12
Australia (AUS) [AUS] [Z] 25 139 152 177 468 18 5 3 4 12 43 144 155 181 480

Adicionalmente, podemos encadenar condiciones lógicas para construir consultas más complejas. Por ejemplo, ¿cuántos países han ganado al menos una medalla de oro en las olimpiadas de verano o en las de invierno?

len(df[(df['Gold'] > 0) | (df['Gold.1'] > 0)])
101

Esto es, 101 países. Recordemos que 100 países habían conseguido al menos una medalla de oro en las olimpiadas de verano, por lo que existe un país que ha ganado al menos una medalla de oro en las olimpiadas de invierno, pero ninguna en las de verano. ¿De qué país se trata?

df[(df['Gold.1'] > 0) & (df['Gold'] == 0)]
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Liechtenstein (LIE) 16 0 0 0 0 18 2 2 5 9 34 2 2 5 9

Nota técnica: debido al orden en el que se efectúan las operaciones en Python cada máscara booleana debe encerrarse entre paréntesis.

Ejercicio: escribe una consulta que devuelva los nombres de las personas que compraron productos cuyo valor es superior a tres dólares.

purchase_1 = pd.Series({'Name': 'Chris',
                        'Item Purchased': 'Dog Food',
                        'Cost': 22.50})
purchase_2 = pd.Series({'Name': 'Kevyn',
                        'Item Purchased': 'Kitty Litter',
                        'Cost': 2.50})
purchase_3 = pd.Series({'Name': 'Vinod',
                        'Item Purchased': 'Bird Seed',
                        'Cost': 5.00})

df2 = pd.DataFrame([purchase_1, purchase_2, purchase_3], index=['Store 1', 'Store 1', 'Store 2'])
purchase_1 = pd.Series({'Name': 'Chris',
                        'Item Purchased': 'Dog Food',
                        'Cost': 22.50})
purchase_2 = pd.Series({'Name': 'Kevyn',
                        'Item Purchased': 'Kitty Litter',
                        'Cost': 2.50})
purchase_3 = pd.Series({'Name': 'Vinod',
                        'Item Purchased': 'Bird Seed',
                        'Cost': 5.00})

df2 = pd.DataFrame([purchase_1, purchase_2, purchase_3], 
                  index=['Store 1', 'Store 1', 'Store 2'])

df2[df2["Cost"] > 3]["Name"]
Store 1    Chris
Store 2    Vinod
Name: Name, dtype: object

7. Índices en DataFrame

Además de las maneras que hemos visto para generar índices en un objeto de tipo Series o DataFrame, podemos utilizar la función set_index(), que toma una lista de columnas y las convierte en índices para el objeto.

Hemos de actuar con cautela, porque la función no almacena el índice actual antes de sobreescribirlo por el nuevo. No obstante, siempre podemos crear una columna adicional en el conjunto de datos, que almacene el índice actual, antes de utilizar la mencionada función.

df.head()
# Summer Gold Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total
Afghanistan (AFG) 13 0 0 2 2 0 0 0 0 0 13 0 0 2 2
Algeria (ALG) 12 5 2 8 15 3 0 0 0 0 15 5 2 8 15
Argentina (ARG) 23 18 24 28 70 18 0 0 0 0 41 18 24 28 70
Armenia (ARM) 5 1 2 9 12 6 0 0 0 0 11 1 2 9 12
Australasia (ANZ) [ANZ] 2 3 4 5 12 0 0 0 0 0 2 3 4 5 12

Imaginemos que deseamos indexar (almacenando previamente en una columna el índice actual por países) la anterior tabla por el número de medallas de oro conseguidas en las olimpiadas de verano:

df["country"] = df.index  # guardamos el índice actual en una columna del conjunto de datos
df = df.set_index("Gold")
df.head()
# Summer Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total country
Gold
0 13 0 2 2 0 0 0 0 0 13 0 0 2 2 Afghanistan (AFG)
5 12 2 8 15 3 0 0 0 0 15 5 2 8 15 Algeria (ALG)
18 23 24 28 70 18 0 0 0 0 41 18 24 28 70 Argentina (ARG)
1 5 2 9 12 6 0 0 0 0 11 1 2 9 12 Armenia (ARM)
3 2 4 5 12 0 0 0 0 0 2 3 4 5 12 Australasia (ANZ) [ANZ]

Por otro lado, podemos deshacernos directamente del índice asignado sin más que emplear la función reset_index(), que almacena el actual en una columna del conjunto de datos (no en el orden que antes que estaba declarado) y genera un índice numérico nuevo:

df = df.reset_index()
df.head()
Gold # Summer Silver Bronze Total # Winter Gold.1 Silver.1 Bronze.1 Total.1 # Games Gold.2 Silver.2 Bronze.2 Combined total country
0 0 13 0 2 2 0 0 0 0 0 13 0 0 2 2 Afghanistan (AFG)
1 5 12 2 8 15 3 0 0 0 0 15 5 2 8 15 Algeria (ALG)
2 18 23 24 28 70 18 0 0 0 0 41 18 24 28 70 Argentina (ARG)
3 1 5 2 9 12 6 0 0 0 0 11 1 2 9 12 Armenia (ARM)
4 3 2 4 5 12 0 0 0 0 0 2 3 4 5 12 Australasia (ANZ) [ANZ]

A continuación, veamos una característica ciertamente útil de la librería pandas: permite la existencia de múltiples índices (pasando una lista con varios elementos a la función set_index()).

Para ver en acción esta funcionalidad, analicemos datos de censo, que suelen estar divididos por estado y ciudad, posibilitando así ver en acción múltiples índices:

df = pd.read_csv("data/census.csv")
df.head()
SUMLEV REGION DIVISION STATE COUNTY STNAME CTYNAME CENSUS2010POP ESTIMATESBASE2010 POPESTIMATE2010 ... RDOMESTICMIG2011 RDOMESTICMIG2012 RDOMESTICMIG2013 RDOMESTICMIG2014 RDOMESTICMIG2015 RNETMIG2011 RNETMIG2012 RNETMIG2013 RNETMIG2014 RNETMIG2015
0 40 3 6 1 0 Alabama Alabama 4779736 4780127 4785161 ... 0.002295 -0.193196 0.381066 0.582002 -0.467369 1.030015 0.826644 1.383282 1.724718 0.712594
1 50 3 6 1 1 Alabama Autauga County 54571 54571 54660 ... 7.242091 -2.915927 -3.012349 2.265971 -2.530799 7.606016 -2.626146 -2.722002 2.592270 -2.187333
2 50 3 6 1 3 Alabama Baldwin County 182265 182265 183193 ... 14.832960 17.647293 21.845705 19.243287 17.197872 15.844176 18.559627 22.727626 20.317142 18.293499
3 50 3 6 1 5 Alabama Barbour County 27457 27457 27341 ... -4.728132 -2.500690 -7.056824 -3.904217 -10.543299 -4.874741 -2.758113 -7.167664 -3.978583 -10.543299
4 50 3 6 1 7 Alabama Bibb County 22915 22919 22861 ... -5.527043 -5.068871 -6.201001 -0.177537 0.177258 -5.088389 -4.363636 -5.403729 0.754533 1.107861

5 rows × 100 columns

df["SUMLEV"].unique()
array([40, 50], dtype=int64)

Mediante la función unique() tenemos acceso a un listado con los diferentes valores recogidos en una columna concreta del conjunto de datos. Por ejemplo, para SUMLEV encontramos únicamente dos valores distintos, 40 y 50, según las estadísticas de resumen se hayan proporcionado a nivel de estado o de condado.

Quedemos con estas últimas aplicando una máscara booleana adecuada:

df = df[df["SUMLEV"] == 50]
df.head()
SUMLEV REGION DIVISION STATE COUNTY STNAME CTYNAME CENSUS2010POP ESTIMATESBASE2010 POPESTIMATE2010 ... RDOMESTICMIG2011 RDOMESTICMIG2012 RDOMESTICMIG2013 RDOMESTICMIG2014 RDOMESTICMIG2015 RNETMIG2011 RNETMIG2012 RNETMIG2013 RNETMIG2014 RNETMIG2015
1 50 3 6 1 1 Alabama Autauga County 54571 54571 54660 ... 7.242091 -2.915927 -3.012349 2.265971 -2.530799 7.606016 -2.626146 -2.722002 2.592270 -2.187333
2 50 3 6 1 3 Alabama Baldwin County 182265 182265 183193 ... 14.832960 17.647293 21.845705 19.243287 17.197872 15.844176 18.559627 22.727626 20.317142 18.293499
3 50 3 6 1 5 Alabama Barbour County 27457 27457 27341 ... -4.728132 -2.500690 -7.056824 -3.904217 -10.543299 -4.874741 -2.758113 -7.167664 -3.978583 -10.543299
4 50 3 6 1 7 Alabama Bibb County 22915 22919 22861 ... -5.527043 -5.068871 -6.201001 -0.177537 0.177258 -5.088389 -4.363636 -5.403729 0.754533 1.107861
5 50 3 6 1 9 Alabama Blount County 57322 57322 57373 ... 1.807375 -1.177622 -1.748766 -2.062535 -1.369970 1.859511 -0.848580 -1.402476 -1.577232 -0.884411

5 rows × 100 columns

Acto seguido, para trabajar con una tabla algo más manejable, restrinjamos sus columnas a nacimientos y estimaciones para la población, además de conservar también el nombre del estado y la correspondiente ciudad:

columns_to_keep = ['STNAME',
                   'CTYNAME',
                   'BIRTHS2010',
                   'BIRTHS2011',
                   'BIRTHS2012',
                   'BIRTHS2013',
                   'BIRTHS2014',
                   'BIRTHS2015',
                   'POPESTIMATE2010',
                   'POPESTIMATE2011',
                   'POPESTIMATE2012',
                   'POPESTIMATE2013',
                   'POPESTIMATE2014',
                   'POPESTIMATE2015']
df = df[columns_to_keep]
df.head()
STNAME CTYNAME BIRTHS2010 BIRTHS2011 BIRTHS2012 BIRTHS2013 BIRTHS2014 BIRTHS2015 POPESTIMATE2010 POPESTIMATE2011 POPESTIMATE2012 POPESTIMATE2013 POPESTIMATE2014 POPESTIMATE2015
1 Alabama Autauga County 151 636 615 574 623 600 54660 55253 55175 55038 55290 55347
2 Alabama Baldwin County 517 2187 2092 2160 2186 2240 183193 186659 190396 195126 199713 203709
3 Alabama Barbour County 70 335 300 283 260 269 27341 27226 27159 26973 26815 26489
4 Alabama Bibb County 44 266 245 259 247 253 22861 22733 22642 22512 22549 22583
5 Alabama Blount County 183 744 710 646 618 603 57373 57711 57776 57734 57658 57673

A continuación, observamos que la estructura del conjunto de datos admite, de manera ideal, dos índices: uno correspondiente al nombre del estado y otro al de la ciudad. Utilicemos adecuadamente la función set_index() para conseguir tal característica:

df = df.set_index(["STNAME", "CTYNAME"])
df.head()
BIRTHS2010 BIRTHS2011 BIRTHS2012 BIRTHS2013 BIRTHS2014 BIRTHS2015 POPESTIMATE2010 POPESTIMATE2011 POPESTIMATE2012 POPESTIMATE2013 POPESTIMATE2014 POPESTIMATE2015
STNAME CTYNAME
Alabama Autauga County 151 636 615 574 623 600 54660 55253 55175 55038 55290 55347
Baldwin County 517 2187 2092 2160 2186 2240 183193 186659 190396 195126 199713 203709
Barbour County 70 335 300 283 260 269 27341 27226 27159 26973 26815 26489
Bibb County 44 266 245 259 247 253 22861 22733 22642 22512 22549 22583
Blount County 183 744 710 646 618 603 57373 57711 57776 57734 57658 57673

Ahora, para consultar, por ejemplo, los datos de Washtenaw County, hemos de pasar al atributo loc también el valor del estado donde se encuentra dicha ciudad, Michigan, y respetando el orden declarado arriba en el interior de la función set_index()(estado, ciudad).

df.loc["Michigan", "Washtenaw County"]
BIRTHS2010            977
BIRTHS2011           3826
BIRTHS2012           3780
BIRTHS2013           3662
BIRTHS2014           3683
BIRTHS2015           3709
POPESTIMATE2010    345563
POPESTIMATE2011    349048
POPESTIMATE2012    351213
POPESTIMATE2013    354289
POPESTIMATE2014    357029
POPESTIMATE2015    358880
Name: (Michigan, Washtenaw County), dtype: int64

De esta manera, generar tablas comparativas entre diferentes ciudades de interés es sumamente sencillo:

df.loc[[("Michigan", "Washtenaw County"),
        ("Michigan", "Wayne County")]]
BIRTHS2010 BIRTHS2011 BIRTHS2012 BIRTHS2013 BIRTHS2014 BIRTHS2015 POPESTIMATE2010 POPESTIMATE2011 POPESTIMATE2012 POPESTIMATE2013 POPESTIMATE2014 POPESTIMATE2015
STNAME CTYNAME
Michigan Washtenaw County 977 3826 3780 3662 3683 3709 345563 349048 351213 354289 357029 358880
Wayne County 5918 23819 23270 23377 23607 23586 1815199 1801273 1792514 1775713 1766008 1759335

Finalmente, el concepto de índices múltiples no se restringe únicamente a las filas, sino que también es posible obtener esta característica para las columnas. Basta trasponer el conjunto de datos (mediante el atributo T) y utilizar adecuadamente la función set_index().

Ejercicio: indexa los registros de compras del siguiente DataFrama jerárquicamente, primero por tienda y luego por persona. Designa dichos índices como "Location" y "Nombre". Después, añade un nuevo registro a la tabla con el siguiente valor:

Name: 'Kevyn', Item Purchased: 'Kitty Food', Cost: 3.00 Location: 'Store 2'

purchase_1 = pd.Series({'Name': 'Chris',
                        'Item Purchased': 'Dog Food',
                        'Cost': 22.50})
purchase_2 = pd.Series({'Name': 'Kevyn',
                        'Item Purchased': 'Kitty Litter',
                        'Cost': 2.50})
purchase_3 = pd.Series({'Name': 'Vinod',
                        'Item Purchased': 'Bird Seed',
                        'Cost': 5.00})

df = pd.DataFrame([purchase_1, purchase_2, purchase_3], index=['Store 1', 'Store 1', 'Store 2'])
purchase_1 = pd.Series({'Name': 'Chris',
                        'Item Purchased': 'Dog Food',
                        'Cost': 22.50})
purchase_2 = pd.Series({'Name': 'Kevyn',
                        'Item Purchased': 'Kitty Litter',
                        'Cost': 2.50})
purchase_3 = pd.Series({'Name': 'Vinod',
                        'Item Purchased': 'Bird Seed',
                        'Cost': 5.00})

df = pd.DataFrame([purchase_1, purchase_2, purchase_3], 
                  index=['Store 1', 'Store 1', 'Store 2'])


df = df.set_index([df.index, 'Name'])
df.index.names = ['Location', 'Name']
df = df.append(pd.Series(data={'Cost': 3.00, 
                               'Item Purchased': 'Kitty Food'}, 
                         name=('Store 2', 'Kevyn')))
df
Item Purchased Cost
Location Name
Store 1 Chris Dog Food 22.5
Kevyn Kitty Litter 2.5
Store 2 Vinod Bird Seed 5.0
Kevyn Kitty Food 3.0

8. Valores perdidos

Dado que es bastante frecuente encontrar valores perdidos en conjuntos de datos, veamos cómo podemos gestionarlos con la librería pandas.

Para empezar, importemos un conjunto de datos que registra la actividad de visualización de vídeos de algunos estudiantes en una plataforma de aprendizaje en línea.

import pandas as pd

df = pd.read_csv("data/log.csv")
df
time user video playback position paused volume
0 1469974424 cheryl intro.html 5 False 10.0
1 1469974454 cheryl intro.html 6 NaN NaN
2 1469974544 cheryl intro.html 9 NaN NaN
3 1469974574 cheryl intro.html 10 NaN NaN
4 1469977514 bob intro.html 1 NaN NaN
5 1469977544 bob intro.html 1 NaN NaN
6 1469977574 bob intro.html 1 NaN NaN
7 1469977604 bob intro.html 1 NaN NaN
8 1469974604 cheryl intro.html 11 NaN NaN
9 1469974694 cheryl intro.html 14 NaN NaN
10 1469974724 cheryl intro.html 15 NaN NaN
11 1469974454 sue advanced.html 24 NaN NaN
12 1469974524 sue advanced.html 25 NaN NaN
13 1469974424 sue advanced.html 23 False 10.0
14 1469974554 sue advanced.html 26 NaN NaN
15 1469974624 sue advanced.html 27 NaN NaN
16 1469974654 sue advanced.html 28 NaN 5.0
17 1469974724 sue advanced.html 29 NaN NaN
18 1469974484 cheryl intro.html 7 NaN NaN
19 1469974514 cheryl intro.html 8 NaN NaN
20 1469974754 sue advanced.html 30 NaN NaN
21 1469974824 sue advanced.html 31 NaN NaN
22 1469974854 sue advanced.html 32 NaN NaN
23 1469974924 sue advanced.html 33 NaN NaN
24 1469977424 bob intro.html 1 True 10.0
25 1469977454 bob intro.html 1 NaN NaN
26 1469977484 bob intro.html 1 NaN NaN
27 1469977634 bob intro.html 1 NaN NaN
28 1469977664 bob intro.html 1 NaN NaN
29 1469974634 cheryl intro.html 12 NaN NaN
30 1469974664 cheryl intro.html 13 NaN NaN
31 1469977694 bob intro.html 1 NaN NaN
32 1469977724 bob intro.html 1 NaN NaN

La columna time registra, respecto a Epoch, cuándo se accedió a un determinado vídeo por un usuario concreto (columnas video y user). Además, tenemos información sobre qué momento del vídeo está visualizando el estudiante (playback position), el volumen (volume) y si está pausado dicho recurso (paused).

Podemos observar que existe gran cantidad de valores perdidos en este conjunto de datos. Ello se explica porque el sistema, si no hay cambio significativo en el estado de la información de cierta columna, simplemente inserta NaN como registro.

Una útil función, para trabajar con valores perdidos, que incorpora la librería pandas es fillna(), que en su parámetro method nos permite, por ejemplo, asignar a un valor perdido el valor existente en el registro anterior (method=ffil) o en el posterior (method=bfill).

df.fillna?

No obstante, para aplicar los anteriores métodos, hemos de ordenar el conjunto de datos previamente. Para ello, tecleamos:

df = df.set_index("time")
df = df.sort_index()
df
user video playback position paused volume
time
1469974424 cheryl intro.html 5 False 10.0
1469974424 sue advanced.html 23 False 10.0
1469974454 cheryl intro.html 6 NaN NaN
1469974454 sue advanced.html 24 NaN NaN
1469974484 cheryl intro.html 7 NaN NaN
1469974514 cheryl intro.html 8 NaN NaN
1469974524 sue advanced.html 25 NaN NaN
1469974544 cheryl intro.html 9 NaN NaN
1469974554 sue advanced.html 26 NaN NaN
1469974574 cheryl intro.html 10 NaN NaN
1469974604 cheryl intro.html 11 NaN NaN
1469974624 sue advanced.html 27 NaN NaN
1469974634 cheryl intro.html 12 NaN NaN
1469974654 sue advanced.html 28 NaN 5.0
1469974664 cheryl intro.html 13 NaN NaN
1469974694 cheryl intro.html 14 NaN NaN
1469974724 cheryl intro.html 15 NaN NaN
1469974724 sue advanced.html 29 NaN NaN
1469974754 sue advanced.html 30 NaN NaN
1469974824 sue advanced.html 31 NaN NaN
1469974854 sue advanced.html 32 NaN NaN
1469974924 sue advanced.html 33 NaN NaN
1469977424 bob intro.html 1 True 10.0
1469977454 bob intro.html 1 NaN NaN
1469977484 bob intro.html 1 NaN NaN
1469977514 bob intro.html 1 NaN NaN
1469977544 bob intro.html 1 NaN NaN
1469977574 bob intro.html 1 NaN NaN
1469977604 bob intro.html 1 NaN NaN
1469977634 bob intro.html 1 NaN NaN
1469977664 bob intro.html 1 NaN NaN
1469977694 bob intro.html 1 NaN NaN
1469977724 bob intro.html 1 NaN NaN

Ahora bien, examinando el resultado, apreciamos que, en ocasiones, dos usuarios utilizan el sistema al mismo tiempo (situación habitual en grandes plataformas de aprendizaje en línea). Por tanto, en este caso particular, utilizar múltiples índices es una buena idea.

df = df.reset_index()
df = df.set_index(["time", "user"])
df
video playback position paused volume
time user
1469974424 cheryl intro.html 5 False 10.0
sue advanced.html 23 False 10.0
1469974454 cheryl intro.html 6 NaN NaN
sue advanced.html 24 NaN NaN
1469974484 cheryl intro.html 7 NaN NaN
1469974514 cheryl intro.html 8 NaN NaN
1469974524 sue advanced.html 25 NaN NaN
1469974544 cheryl intro.html 9 NaN NaN
1469974554 sue advanced.html 26 NaN NaN
1469974574 cheryl intro.html 10 NaN NaN
1469974604 cheryl intro.html 11 NaN NaN
1469974624 sue advanced.html 27 NaN NaN
1469974634 cheryl intro.html 12 NaN NaN
1469974654 sue advanced.html 28 NaN 5.0
1469974664 cheryl intro.html 13 NaN NaN
1469974694 cheryl intro.html 14 NaN NaN
1469974724 cheryl intro.html 15 NaN NaN
sue advanced.html 29 NaN NaN
1469974754 sue advanced.html 30 NaN NaN
1469974824 sue advanced.html 31 NaN NaN
1469974854 sue advanced.html 32 NaN NaN
1469974924 sue advanced.html 33 NaN NaN
1469977424 bob intro.html 1 True 10.0
1469977454 bob intro.html 1 NaN NaN
1469977484 bob intro.html 1 NaN NaN
1469977514 bob intro.html 1 NaN NaN
1469977544 bob intro.html 1 NaN NaN
1469977574 bob intro.html 1 NaN NaN
1469977604 bob intro.html 1 NaN NaN
1469977634 bob intro.html 1 NaN NaN
1469977664 bob intro.html 1 NaN NaN
1469977694 bob intro.html 1 NaN NaN
1469977724 bob intro.html 1 NaN NaN

Acto seguido, utilizamos la función fillna(), pasándole como argumento el valor "ffill" al parámetro method.

df = df.fillna(method="ffill")
df.head()
video playback position paused volume
time user
1469974424 cheryl intro.html 5 False 10.0
sue advanced.html 23 False 10.0
1469974454 cheryl intro.html 6 False 10.0
sue advanced.html 24 False 10.0
1469974484 cheryl intro.html 7 False 10.0

Nota: muchas funciones, por defecto, ignoran los valores perdidos a la hora de realizar cálculos. Hemos de proceder pues con cautela si este comportamiento no es el que nos interesa.

Anterior