En un mundo ideal, todos los datos que necesitas estarían disponibles en el formato correcto y con contenido completo.
En el mundo real, probablemente necesitarás obtener datos de muchas fuentes diferentes e incompletas. Por eso es importante aprender a limpiar tus datos antes de analizarlos o pasarlos a un algoritmo de ML.
La limpieza de datos puede no ser la parte más glamorosa de tu trabajo, pero es una parte crucial del desarrollo de productos basados en datos. No solo es importante para todo el pipeline, sino que también es una de las tareas que más tiempo consume en un proyecto (algunos estiman que consume alrededor del 80% del tiempo en un proyecto de aprendizaje automático).
En este artículo, aprenderemos algunas técnicas básicas de limpieza de datos que te permitirán manejar las situaciones más comunes.
¡Genial, comencemos!
Manejo de valores faltantes
El manejo de valores faltantes es una técnica básica (y extremadamente útil). Los datos a los que tendrás acceso probablemente carezcan de atributos de algunas entradas, y es mejor dedicar algo de tiempo a manejar explícitamente cada caso.
Imagina que tienes un archivo csv con el siguiente contenido:
Name,Type,Color,Evolves,HP
Abra,Psychic,,True,
Pikachu,Electric,,True,35
Ekans,,Purple,,35
Dratini,,Blue,,41
Ditto,Normal,Pink,False,48
Como puedes ver, faltan entradas para varias filas. Si lo cargas usando read_csv
, el dataframe resultante se verá así:
import pandas as pd
frame = pd.read_csv('./sample_data/pokes_missing.csv')
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | NaN | True | NaN |
1 | Pikachu | Electric | NaN | True | 35.0 |
2 | Ekans | NaN | Purple | NaN | 35.0 |
3 | Dratini | NaN | Blue | NaN | 41.0 |
4 | Ditto | Normal | Pink | False | 48.0 |
Pandas llena cada entrada sin valor con el valor centinela predeterminado NaN. En muchas situaciones, simplemente quieres eliminar las filas con valores faltantes y dejar solo las que tienen exclusivamente campos no-NaN. Para esto, puedes usar la función dropna
:
# dropna no afecta el dataframe original, devuelve uno nuevo.
frame.dropna()
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
4 | Ditto | Normal | Pink | False | 48.0 |
#Si quieres realizar el dropna en el lugar, especifica el atributo inplace
frame.dropna(inplace=True)
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
4 | Ditto | Normal | Pink | False | 48.0 |
# Recarguemos los datos:
frame = pd.read_csv('./sample_data/pokes_missing.csv')
Dropna puede usarse de manera diferente: Puedes decirle a la función que elimine solo las filas que están compuestas exclusivamente de valores NaN. De esta manera puedes deshacerte de entradas que no proporcionan información alguna. Para hacer esto, proporciona el parámetro opcional * how='all'
al llamar dropna.
Eliminar todas las filas con valores NaN puede no ser la mejor opción. Muy a menudo, la mejor opción es llenar las entradas faltantes con valores que proporcionas o con valores calculados del propio dataframe. Pandas proporciona una función llamada fillna
que te permite manejar valores faltantes:
# Llenemos cada entrada faltante con el valor 0
frame.fillna(10)
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | 10 | True | 10.0 |
1 | Pikachu | Electric | 10 | True | 35.0 |
2 | Ekans | 10 | Purple | 10 | 35.0 |
3 | Dratini | 10 | Blue | 10 | 41.0 |
4 | Ditto | Normal | Pink | False | 48.0 |
¿Notas algo malo con el dataframe? Bueno, para empezar, el valor 10 podría funcionar razonablemente bien para HP, pero no tiene sentido para Type, Color o Evolves. fillna
te permite especificar cómo llenar valores NaN para cada columna si pasas un diccionario en lugar de un solo valor:
frame.fillna({'Type': 'Unknown',
'Color': 'Yellow',
'Evolves': True,
'HP': 30})
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 30.0 |
1 | Pikachu | Electric | Yellow | True | 35.0 |
2 | Ekans | Unknown | Purple | True | 35.0 |
3 | Dratini | Unknown | Blue | True | 41.0 |
4 | Ditto | Normal | Pink | False | 48.0 |
¡Ahora el dataframe tiene un poco más de sentido! Como con dropna, el cambio realizado por fillna no ocurre en el dataframe original, solo crea un nuevo dataframe con nuevos valores. Si quieres cambiar el dataframe original proporciona el argumento inplace:
frame.fillna({'Type': 'Unknown',
'Color': 'Yellow',
'Evolves': True,
'HP': 30},
inplace=True)
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 30.0 |
1 | Pikachu | Electric | Yellow | True | 35.0 |
2 | Ekans | Unknown | Purple | True | 35.0 |
3 | Dratini | Unknown | Blue | True | 41.0 |
4 | Ditto | Normal | Pink | False | 48.0 |
Manejo de duplicados
A veces tus datos contendrán filas duplicadas o valores con los mismos campos. Echa un vistazo a este dataframe:
frame = pd.read_csv('./sample_data/pokes_duplicates.csv')
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 25 |
1 | Pikachu | Electric | Yellow | True | 35 |
2 | Ekans | Poison | Purple | True | 35 |
3 | Dratini | Dragon | Blue | True | 41 |
4 | Ditto | Normal | Pink | False | 48 |
5 | Abra | Psychic | Yellow | True | 25 |
6 | Dratini | Dragon | Blue | True | 41 |
El frame tiene dos Dratinis y dos Abras, así que eliminémoslos usando drop_duplicates:
frame.drop_duplicates()
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 25 |
1 | Pikachu | Electric | Yellow | True | 35 |
2 | Ekans | Poison | Purple | True | 35 |
3 | Dratini | Dragon | Blue | True | 41 |
4 | Ditto | Normal | Pink | False | 48 |
A veces quieres eliminar duplicados basándote en el valor de un atributo. En este caso, puedes proporcionar un argumento extra (o lista de argumentos si quieres usar múltiples atributos) para especificar la columna a tomar en consideración. Por ejemplo, la siguiente llamada asegura que tengamos solo filas con colores únicos:
# Abra y Pikachu son ambos amarillos, así que es hora de elegir Pikachu
frame.drop_duplicates('Color')
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 25 |
2 | Ekans | Poison | Purple | True | 35 |
3 | Dratini | Dragon | Blue | True | 41 |
4 | Ditto | Normal | Pink | False | 48 |
Mapeos y otras transformaciones
En esta sección, aprenderemos sobre algunas otras técnicas para modificar tus datos. La primera técnica es usar la función map para alterar una columna en un dataframe:
frame = frame = pd.read_csv('./sample_data/pokes.csv')
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | Psychic | Yellow | True | 25 |
1 | Pikachu | Electric | Yellow | True | 35 |
2 | Ekans | Poison | Purple | True | 35 |
3 | Dratini | Dragon | Blue | True | 41 |
4 | Ditto | Normal | Pink | False | 48 |
# Transformemos el tipo en una cadena completamente en mayúsculas
frame['Type'] = frame['Type'].map(lambda x: x.upper())
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | PSYCHIC | Yellow | True | 25 |
1 | Pikachu | ELECTRIC | Yellow | True | 35 |
2 | Ekans | POISON | Purple | True | 35 |
3 | Dratini | DRAGON | Blue | True | 41 |
4 | Ditto | NORMAL | Pink | False | 48 |
Lo siguiente que podemos hacer es usar la función replace para cambiar valores. En el siguiente ejemplo reemplazaremos los valores booleanos de la columna Evolves por las cadenas ‘Yes’ y ‘No’:
frame['Evolves'] = frame['Evolves'].replace([True, False], ['Yes','No'])
frame
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | PSYCHIC | Yellow | Yes | 25 |
1 | Pikachu | ELECTRIC | Yellow | Yes | 35 |
2 | Ekans | POISON | Purple | Yes | 35 |
3 | Dratini | DRAGON | Blue | Yes | 41 |
4 | Ditto | NORMAL | Pink | No | 48 |
También podemos usar selección condicional para obtener solo los datos que necesitamos. Supongamos que estamos interesados solo en los elementos con HP menor a 40:
frame[frame['HP'] < 40]
Name | Type | Color | Evolves | HP | |
---|---|---|---|---|---|
0 | Abra | PSYCHIC | Yellow | Yes | 25 |
1 | Pikachu | ELECTRIC | Yellow | Yes | 35 |
2 | Ekans | POISON | Purple | Yes | 35 |
Lo último con lo que trataremos es transformar datos categóricos en columnas codificadas one-hot. Si quieres una explicación más detallada de la codificación one-hot puedes leer este artículo:
# get_dummies transforma datos categóricos en un dataframe codificado one-hot
# concatenaremos el dataframe original y las columnas de tipo codificadas one-hot
pd.concat([frame, pd.get_dummies(frame['Type'])], axis=1)
Name | Type | Color | Evolves | HP | DRAGON | ELECTRIC | NORMAL | POISON | PSYCHIC | |
---|---|---|---|---|---|---|---|---|---|---|
0 | Abra | PSYCHIC | Yellow | Yes | 25 | 0 | 0 | 0 | 0 | 1 |
1 | Pikachu | ELECTRIC | Yellow | Yes | 35 | 0 | 1 | 0 | 0 | 0 |
2 | Ekans | POISON | Purple | Yes | 35 | 0 | 0 | 0 | 1 | 0 |
3 | Dratini | DRAGON | Blue | Yes | 41 | 1 | 0 | 0 | 0 | 0 |
4 | Ditto | NORMAL | Pink | No | 48 | 0 | 0 | 1 | 0 | 0 |
Limpia y limpia
Obtener datos perfectos al comienzo del proyecto es extremadamente improbable. Probablemente, necesitarás juntar un conjunto de datos de muchas fuentes, y en el proceso, podría haber algunos datos malformados, erróneos o faltantes.
Eso está bien siempre y cuando sepas cómo limpiar tus datos y ponerlos en forma adecuada para tus análisis/algoritmos. A menudo se dice que probablemente pasarás el 80% de tu tiempo limpiando y manipulando datos, así que es una buena idea aprender uno o dos trucos.
En el próximo artículo, aprenderemos cómo juntar datos de diferentes fuentes en una sola colección.
¡Gracias por leer!
Qué hacer a continuación
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
- Puedes encontrar el código fuente de esta serie en este repositorio.
- Este artículo está basado en el libro: Python for Data Analysis.
- Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor)