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:
- El portal Stack Overflow.
- Los libros Python for Data Analysis y Learning the Pandas Library.
- El agregador de blogs Planet Python.
- El podcast Data Skeptic.
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étodoappend()
. 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 !
y03 !
. - 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.