import numpy as np
import pandas as pd
df = pd.DataFrame([5, 27.3, np.nan, -16], columns=["numbers"])
df| numbers | |
|---|---|
| 0 | 5.0 |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16.0 |
En este capítulo veremos las herramientas y trucos para lidiar con valores faltantes. Empezaremos hablando de algunas herramientas generales para trabajar con valores faltantes registrados como NA. Luego exploraremos la idea de los valores faltantes implícitos, valores que simplemente están ausentes de tus datos, y mostraremos algunas herramientas que puedes usar para hacerlos explícitos. Terminaremos con una discusión relacionada sobre los grupos vacíos, causados por categorías que no aparecen en los datos.
Este capítulo usará el paquete de análisis de datos pandas.
Para empezar, exploremos algunas herramientas útiles para crear o eliminar valores faltantes explícitos, es decir, celdas donde ves un NA o nan.
Algo a tener en cuenta sobre los valores faltantes en pandas es que ¡no todos son iguales!
Por ejemplo, los números reales en pandas (como el dtype float64) usan un ‘nan’ (es decir, Not a Number, «no es un número»):
import numpy as np
import pandas as pd
df = pd.DataFrame([5, 27.3, np.nan, -16], columns=["numbers"])
df| numbers | |
|---|---|
| 0 | 5.0 |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16.0 |
¡Pero esta no es la única forma! También podemos usar el valor None integrado en Python (que, aquí, se convierte en NaN porque los valores válidos son todos números de punto flotante) y el pd.NA de pandas:
numbers = pd.DataFrame([pd.NA, 27.3, np.nan, -16, None], columns=["numbers"])
numbers| numbers | |
|---|---|
| 0 | <NA> |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16 |
| 4 | None |
Sin embargo, con el tipo de dato object (el predeterminado para strings), los tipos pueden coexistir:
fruits = pd.DataFrame(
["orange", np.nan, "apple", None, "banana", pd.NA], columns=["fruit"]
)
fruits| fruit | |
|---|---|
| 0 | orange |
| 1 | NaN |
| 2 | apple |
| 3 | None |
| 4 | banana |
| 5 | <NA> |
Ambos tipos de valor faltante se pueden encontrar con la función .isna() de pandas. Esta devuelve una nueva columna de valores boolean que son True si el valor es cualquier tipo de valor faltante.
fruits.isna()| fruit | |
|---|---|
| 0 | False |
| 1 | True |
| 2 | False |
| 3 | True |
| 4 | False |
| 5 | True |
Por comodidad, también existe una función notna().
Hay varias opciones para tratar los valores faltantes. La función fillna() permite hacerlo. Veámosla con algunos datos de prueba:
nan_df = pd.DataFrame(
[
[np.nan, 2, None, 0],
[3, 4, np.nan, 1],
[5, np.nan, np.nan, pd.NA],
[np.nan, 3, np.nan, 4],
],
columns=list("ABCD"),
)
nan_df| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
| 3 | NaN | 3.0 | NaN | 4 |
Primero, podemos simplemente rellenar cualquier valor faltante con un único valor fijo:
nan_df.fillna(0)/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/4054961691.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(0)
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 0.0 | 0 |
| 1 | 3.0 | 4.0 | 0.0 | 1 |
| 2 | 5.0 | 0.0 | 0.0 | 0 |
| 3 | 0.0 | 3.0 | 0.0 | 4 |
Esto se puede hacer columna por columna; aquí reemplazamos todos los elementos NaN de las columnas ‘A’, ‘B’, ‘C’ y ‘D’ por 0, 1, 2 y 3 respectivamente.
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3})/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/2397886090.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3})
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 2.0 | 0 |
| 1 | 3.0 | 4.0 | 2.0 | 1 |
| 2 | 5.0 | 1.0 | 2.0 | 3 |
| 3 | 0.0 | 3.0 | 2.0 | 4 |
También podemos propagar los valores no nulos hacia adelante o hacia atrás (en relación con el índice)
nan_df.fillna(method="ffill")/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/1353804149.py:1: FutureWarning: DataFrame.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
nan_df.fillna(method="ffill")
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/1353804149.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(method="ffill")
| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 4.0 | NaN | 1 |
| 3 | 5.0 | 3.0 | NaN | 4 |
nan_df.fillna(method="bfill")/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/2505504399.py:1: FutureWarning: DataFrame.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
nan_df.fillna(method="bfill")
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/2505504399.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(method="bfill")
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 3.0 | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 3.0 | NaN | 4 |
| 3 | NaN | 3.0 | NaN | 4 |
Las opciones de relleno hacia adelante y hacia atrás son especialmente útiles para series temporales, ¡pero ten cuidado al usarlas si estás haciendo un ejercicio de pronóstico!
Otra característica de todas estas funciones es que puedes limitar el número de NaN que se reemplazan con el argumento de palabra clave limit=.
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3}, limit=1)/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/1730877720.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3}, limit=1)
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 2.0 | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 1.0 | NaN | 3 |
| 3 | NaN | 3.0 | NaN | 4 |
Por supuesto, otra opción podría ser simplemente filtrar por completo los valores faltantes. Hay un par de formas de hacerlo según si quieres eliminar filas enteras (axis=0) o columnas (axis=1; aunque en este caso, como hay al menos un NaN en cada columna, ¡no quedará ningún dato!)
nan_df["A"].dropna(axis=0) # on a single column1 3.0
2 5.0
Name: A, dtype: float64
nan_df.dropna(axis=1)| 0 |
|---|
| 1 |
| 2 |
| 3 |
dropna() también admite algunos argumentos de palabra clave; por ejemplo, how="all" solo elimina una columna o fila si todos sus valores son NA.
nan_df.dropna(how="all")| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
| 3 | NaN | 3.0 | NaN | 4 |
Existe una palabra clave thresh (de threshold, «umbral»), que te permite conservar solo las filas o columnas que contienen como máximo cierto número de observaciones faltantes.
Otra forma de filtrar los nan es usar los mismos métodos de filtrado que usarías normalmente, mediante columnas boolean, en combinación con la función .notna(). En el ejemplo siguiente, vemos todas las columnas de las filas en las que A no es NA.
nan_df[nan_df["A"].notna()]| A | B | C | D | |
|---|---|---|---|---|
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
A veces te encontrarás con el problema opuesto, en el que algún valor concreto en realidad representa un valor faltante. Esto suele ocurrir en datos generados por software antiguo que no tiene una forma adecuada de representar valores faltantes, por lo que debe usar algún valor especial como 99 o -999.
Si es posible, resuélvelo al leer los datos, por ejemplo, usando el argumento de palabra clave na_values= al llamar a pd.read_csv(). Si descubres el problema más tarde, o tu fuente de datos no ofrece una forma de manejarlo al leer el archivo, puedes usar diversas opciones para reemplazar los datos dados:
stata_df = pd.DataFrame([[3, 4, 5], [-7, 4, -99], [-99, 6, 5]], columns=list("ABC"))
stata_df| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | -7 | 4 | -99 |
| 2 | -99 | 6 | 5 |
La opción más sencilla probablemente sea .replace():
stata_df.replace({-99: pd.NA})| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | -7 | 4 | <NA> |
| 2 | <NA> | 6 | 5 |
Como .replace() acepta un diccionario, es posible reemplazar varios valores a la vez:
stata_df.replace({-99: pd.NA, -7: pd.NA})| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | <NA> | 4 | <NA> |
| 2 | <NA> | 6 | 5 |
Ten en cuenta que esto se aplica a todas las columnas del dataframe. Para aplicarlo a una sola, simplemente selecciona esa columna específica.
Hasta ahora hemos hablado de valores que faltan explícitamente, es decir, puedes ver un NA o algo similar en tus datos. Pero los valores también pueden faltar implícitamente, si una fila entera de datos simplemente está ausente. Ilustremos la diferencia con un dataset sencillo que registra el precio de una acción cada trimestre:
stocks = pd.DataFrame(
{
"year": [2020, 2020, 2020, 2020, 2021, 2021, 2021],
"qtr": [1, 2, 3, 4, 2, 3, 4],
"price": [1.88, 0.59, 0.35, np.nan, 0.92, 0.17, 2.66],
}
)
stocks| year | qtr | price | |
|---|---|---|---|
| 0 | 2020 | 1 | 1.88 |
| 1 | 2020 | 2 | 0.59 |
| 2 | 2020 | 3 | 0.35 |
| 3 | 2020 | 4 | NaN |
| 4 | 2021 | 2 | 0.92 |
| 5 | 2021 | 3 | 0.17 |
| 6 | 2021 | 4 | 2.66 |
Este dataset tiene dos observaciones faltantes:
El price del cuarto trimestre de 2020 falta explícitamente, porque su valor es NA.
El price del primer trimestre de 2021 falta implícitamente, porque simplemente no aparece en el dataset.
Una forma de pensar en la diferencia es con este koan de estilo zen:
Un valor faltante explícito es la presencia de una ausencia.
Un valor faltante implícito es la ausencia de una presencia.
A veces quieres hacer explícitos los faltantes implícitos para tener algo físico con lo que trabajar. En otros casos, la estructura de los datos te impone faltantes explícitos y quieres deshacerte de ellos. Las siguientes secciones tratan algunas herramientas para pasar de faltantes implícitos a explícitos y viceversa.
Ya has visto una herramienta que puede hacer explícitos los faltantes implícitos y viceversa: el pivot. Hacer los datos más anchos puede volver explícitos los valores faltantes implícitos, porque cada combinación de filas y nuevas columnas debe tener algún valor. Por ejemplo, si pivotamos stocks para poner quarter en las columnas (y hacemos de year el índice), ambos valores faltantes se vuelven explícitos:
stocks.pivot(columns="qtr", values="price", index="year")| qtr | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| year | ||||
| 2020 | 1.88 | 0.59 | 0.35 | NaN |
| 2021 | NaN | 0.92 | 0.17 | 2.66 |
Por defecto, hacer los datos más largos conserva los valores faltantes explícitos.
Un último tipo de ausencia es el grupo vacío, un grupo que no contiene ninguna observación, lo cual puede surgir al trabajar con datos categóricos.
Por ejemplo, imagina que tenemos un dataset que contiene información de salud sobre algunas personas:
health = pd.DataFrame(
{
"name": ["Ikaia", "Oletta", "Leriah", "Dashay", "Tresaun"],
"smoker": ["no", "no", "previously", "no", "yes"],
"age": [34, 88, 75, 47, 56],
}
)
health["smoker"] = health["smoker"].astype("category")Ahora eliminamos la última fila de datos:
health_cut = health.iloc[:-1, :]
health_cut| name | smoker | age | |
|---|---|---|---|
| 0 | Ikaia | no | 34 |
| 1 | Oletta | no | 88 |
| 2 | Leriah | previously | 75 |
| 3 | Dashay | no | 47 |
El valor ‘yes’ de smoker ahora no aparece (aparentemente) en ningún lugar de nuestro dataframe. Pero si ejecutas value_counts() para contar cuántos hay de cada categoría, verás que el dataframe ‘recuerda’ que existe una categoría ‘yes’ que actualmente no está presente:
health_cut["smoker"].value_counts()smoker
no 3
previously 1
yes 0
Name: count, dtype: int64
Verás que ocurre lo mismo con una operación groupby():
health_cut.groupby("smoker")["age"].mean()/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75825/3998383890.py:1: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
health_cut.groupby("smoker")["age"].mean()
smoker
no 56.333333
previously 75.000000
yes NaN
Name: age, dtype: float64
Aquí puedes ver que, como calculamos la media de un número que no existe, obtuvimos un NaN en lugar de un valor real para la fila yes (pero sí hay una fila ‘yes’).