1.- Análisis y limpieza de un conjunto de datos¶

Objetivo: Familiarizarse con el análisis y limpieza de datos a través de un conjunto de datos simulados.

Se requiere llevar a cabo los siguientes pasos:

  1. Cargar los datos en un DataFrame de Pandas y visualizar las primeras filas.
  2. Identificar y manejar valores faltantes.
  3. Detectar y corregir valores fuera de rango.
  4. Apoyarte en la visualización de datos para encontrar valores atípicos.
  5. Crear una nueva columna dependencia_movil, calculada mediante la fórmula: horas_movil_dia × (11 – satisfacción).
  6. Generar un AED básico comentando los estadísticos más importantes.
  7. Exportar el DataFrame procesado a un archivo CSV, el cual será descargado.
In [1]:
import numpy as np
import pandas as pd
In [2]:
df = pd.read_csv('content/encuesta.csv')
df.head(10)
Out[2]:
edad genero horas_movil_dia satisfaccion red_social_principal
0 24 Femenino 3.5 8 Instagram
1 30 Masculino 2.0 7 Twitter
2 18 Femenino 5.5 9 TikTok
3 45 Masculino 1.5 6 LinkedIn
4 29 Otro 4.0 5 Instagram
5 55 Femenino 2.0 8 Facebook
6 22 Masculino 6.0 7 Instagram
7 33 Femenino 3.0 6 Instagram
8 41 Masculino 1.0 9 LinkedIn
9 19 Otro 7.0 8 Instagram
  1. Identificar y manejar valores faltantes
In [3]:
df.isnull().sum()
Out[3]:
edad                    1
genero                  1
horas_movil_dia         2
satisfaccion            3
red_social_principal    1
dtype: int64

Identificamos valores faltantes pero si son numéricos los trataremos más tarde

Edad, horas_movil_dia y satisfacción son numéricas

Genero y red_social_principal son categoricas y no faltan demasiados valores

Vamos a intentar procesar estas últimas

In [4]:
df['genero'].value_counts(dropna=False)
Out[4]:
genero
Femenino     45
Masculino    38
Otro         16
NaN           1
Name: count, dtype: int64

Podríamos decidir usar la moda en este campo, pero al existir ya la categoria "Otro" también podemos elegirla. Podría pasar que alguien acostumbrado a no poner masculino o femenino lo dejase vacío. Usaremos este razonamiento

In [5]:
df['genero'] = df['genero'].fillna('Otro')
df['genero'].value_counts(dropna=False)
Out[5]:
genero
Femenino     45
Masculino    38
Otro         17
Name: count, dtype: int64
In [6]:
df['red_social_principal'].value_counts(dropna=False)
Out[6]:
red_social_principal
Instagram    27
TikTok       21
Facebook     19
Twitter      16
LinkedIn     16
NaN           1
Name: count, dtype: int64

En este caso no podemos hacer una imputación lógica, pero al tener un ganador claro se justifica el uso de la moda.

(Al ser solo un registro, tampoco vamos a cambiar demasiado el caracter de la serie)

In [7]:
moda_red_social = df['red_social_principal'].mode()[0]
df['red_social_principal'] = df['red_social_principal'].fillna(moda_red_social)
print(df[['genero', 'red_social_principal']].isnull().sum())
genero                  0
red_social_principal    0
dtype: int64

Como comprobación adicional ¿Puede que haya '0' en lugar de nulos? Lo comprobamos

In [8]:
(df == 0).sum()
Out[8]:
edad                    0
genero                  0
horas_movil_dia         0
satisfaccion            0
red_social_principal    0
dtype: int64
  1. Detectar y corregir valores fuera de rango

Antes de calcular medias o modas para rellenar huecos vacíos, debemos asegurarnos de que nuestros datos son únicos o nuestro rango estará viciado.

In [9]:
df.duplicated().sum()
Out[9]:
np.int64(8)

Tenemos 8 duplicados, pero como no tenemos un identificador único, puede que sean casualidades

In [10]:
# Como no sabemos si podemos borrar o no, pintamos todas las filas y buscamos patrones
df[df.duplicated(keep=False)]
Out[10]:
edad genero horas_movil_dia satisfaccion red_social_principal
0 24 Femenino 3.5 8 Instagram
1 30 Masculino 2.0 7 Twitter
2 18 Femenino 5.5 9 TikTok
3 45 Masculino 1.5 6 LinkedIn
4 29 Otro 4.0 5 Instagram
5 55 Femenino 2.0 8 Facebook
7 33 Femenino 3.0 6 Instagram
8 41 Masculino 1.0 9 LinkedIn
20 24 Femenino 3.5 8 Instagram
21 30 Masculino 2.0 7 Twitter
22 18 Femenino 5.5 9 TikTok
23 45 Masculino 1.5 6 LinkedIn
24 29 Otro 4.0 5 Instagram
25 55 Femenino 2.0 8 Facebook
27 33 Femenino 3.0 6 Instagram
28 41 Masculino 1.0 9 LinkedIn
In [11]:
# Podemos ver que la repetición va en bloque ( los 8 primeros son los mismos que los 8 últimos) borramos los últimos
df = df.drop_duplicates(keep='first')
df.duplicated().sum()
Out[11]:
np.int64(0)

Con las variables categóricas ya vimos que no hay valores fuera de rango (errores tipográficos, fallos en el orden de columnas, etc.). Pero mejor volver a comprobarlo listando valores

In [12]:
print(df['genero'].value_counts())
print(df['red_social_principal'].value_counts())
genero
Femenino     41
Masculino    35
Otro         16
Name: count, dtype: int64
red_social_principal
Instagram    25
TikTok       20
Facebook     18
Twitter      15
LinkedIn     14
Name: count, dtype: int64

Con las numéricas tenemos que comprobarlo todavía y el primer paso es ver si efectivamente todos los valores son numéricos.

Este es buen momento para identificar también los campos vacíos, pero aún no los modificamos

In [13]:
columnas_numericas = ['edad', 'satisfaccion', 'horas_movil_dia']
# Usamos un df auxiliar porque el coerce nos va a borrar los valores no numéricos (pone NaN)
df_numerico = df[columnas_numericas].apply(pd.to_numeric, errors='coerce')
# Ahora comparamos los NaN del df auxiliar para ver que había en el df original, así podemos modificar lo que nos interese
df[df_numerico.isna().any(axis=1)]
Out[13]:
edad genero horas_movil_dia satisfaccion red_social_principal
31 28 Masculino NaN 8 Twitter
33 NaN Masculino 4.0 5 TikTok
35 32 Otro 3.0 NaN LinkedIn
62 Veinte Femenino 5.0 8 TikTok
63 40 Masculino 2.5 Baja LinkedIn
85 33 Otro 3.5 NaN Instagram
91 61 Femenino NaN 8 Facebook
94 30 Masculino 4.0 NaN Twitter

Todos los valores incorrectos eran ya NaN excepto el 'veinte' de edad y el 'Baja' de satisfacción,

Para 'veinte', le asignamos su valor numérico (20)

Para 'Baja', decidimos asignarle un valor suficientemente bajo (3)

In [14]:
df.loc[df['edad'] == 'Veinte', 'edad'] = 20
df.loc[df['satisfaccion'] == 'Baja', 'satisfaccion'] = 3
# Nos aseguramos de que nuestras columnas corregidas sean reconocidas como numéricas
df[columnas_numericas] = df[columnas_numericas].apply(pd.to_numeric)
df[columnas_numericas].dtypes
# Seguimos sin tocar valores faltantes de momento, pero ya los tenemos localizados
Out[14]:
edad               float64
satisfaccion       float64
horas_movil_dia    float64
dtype: object

Hemos identificado los valores faltantes y hemos modificado los "incorrectos".

¿Por qué aún no terminamos el apartado?

Porque si "rellenásemos" nulos con un valor calculado podría pasar que introdujeramos la distorsión de los valores atípicos que aún no hemos tratado

  1. Apoyarte en la visualización de datos para encontrar valores atípicos.
In [15]:
import matplotlib.pyplot as plt
import seaborn as sns
In [16]:
fig, ax = plt.subplots(1,3)
sns.boxplot(data=df[columnas_numericas[0]], ax=ax[0])
sns.boxplot(data=df[columnas_numericas[1]], ax=ax[1])
sns.boxplot(data=df[columnas_numericas[2]], ax=ax[2])
plt.tight_layout()
No description has been provided for this image

Viendo el gráfico es muy claro que hay:

  • 3 outliers altos en edad
  • 2 altos en satisfaccion
  • 2 altos en horas

Podríamos recalcular cuartiles y con ellos los outliers, pero es mucho más sencillo apoyarnos en los valores del gráfico y revisar exactamente esos valores.

Sin embargo también debemos revisar valores por debajo del minimo y máximo aceptable (negativos, valores irreales...)

In [17]:
# Menores de 13 no deben usar RRSS
valores_extremos1 = df.loc[(df['edad'] < 13) | (df['edad'] > 120), 'edad']
# Un día tiene 24 horas (podríamos discutir si alguien puede estar 24 horas con el móvil...)
valores_extremos2 = df.loc[(df['horas_movil_dia'] < 0) | (df['horas_movil_dia'] > 24), 'horas_movil_dia']
# La satisfacción se medía en una escala de 0 a 10
valores_extremos3 = df.loc[(df['satisfaccion'] < 0) | (df['satisfaccion'] > 10), 'satisfaccion']
pd.concat([valores_extremos1, valores_extremos2, valores_extremos3], axis=1)
Out[17]:
edad horas_movil_dia satisfaccion
54 200.0 NaN NaN
58 -5.0 NaN NaN
59 5.0 NaN NaN
43 NaN 25.0 NaN
55 NaN 30.0 NaN
44 NaN NaN 12.0
57 NaN NaN 15.0

Vemos que hay una edad negativa, un niño de 5 años, una persona de 200, además 2 valores erroneos en horas y otros 2 en satisfacción.

Tanto horas como satisfacción coinciden con el gráfico, pero edad tenemos que tratarla añadiendo los outliers inferiores.

In [18]:
grafico = df['edad'].nlargest(3)
inferiores = df['edad'].nsmallest(2)
pd.concat([grafico, inferiores])
Out[18]:
54    200.0
42    120.0
49     99.0
58     -5.0
59      5.0
Name: edad, dtype: float64

Las edades de 200, 120, -5 y el niño de 5 años son claramente un error pero podemos dudar con el 99

In [19]:
df.loc[[49]]
Out[19]:
edad genero horas_movil_dia satisfaccion red_social_principal
49 99.0 Otro 1.0 5.0 LinkedIn

La fila completa tiene datos "genericos" con valores que perfectamente pueden ser de prueba: edad al máximo, genero indeterminado, unidad de horas, satisfacción media...

No tiene sentido conservarla con ese riesgo

In [20]:
# Eliminamos la fila 49
df = df.drop([49])

Vamos a inspeccionar también las filas de -5, 5, 200 y 120 años por si hay algo que podamos inferir, como un error tipográfico. Si no lo hay las borraremos

In [21]:
# Inspeccionamos las filas completas de los outliers extremos
df.loc[[54, 42, 58, 59]]
Out[21]:
edad genero horas_movil_dia satisfaccion red_social_principal
54 200.0 Femenino 1.5 8.0 Facebook
42 120.0 Femenino 2.0 8.0 Facebook
58 -5.0 Femenino 3.0 8.0 Instagram
59 5.0 Masculino 2.0 9.0 LinkedIn

Como no podemos adivinar la edad real sin inventárnosla borramos las filas que distorsionan el conjunto

In [22]:
df = df.drop([54, 42, 58, 59])
In [23]:
# 2. SATISFACCIÓN:
pd.concat([df['satisfaccion'].nlargest(2)])
Out[23]:
57    15.0
44    12.0
Name: satisfaccion, dtype: float64

En una escala del 1 al 10 está claro que 15 y 12 son incorrectos, podemos eliminar o poner el máximo. Al evaluar el concepto "satisfacción" podemos pensar que en estos valores la intención del usuario era dar la nota máxima, por ello podemos elegir sustituir

In [24]:
df['satisfaccion'] = df['satisfaccion'].clip(upper=10)
In [25]:
# 3. HORAS MÓVIL
df['horas_movil_dia'].nlargest(2)
Out[25]:
55    30.0
43    25.0
Name: horas_movil_dia, dtype: float64

El día tiene 24 horas así que 30 y 25 son errores. Con "satisfacción" considerábamos la intención del usuario por ello elegimos sustituir, pero aquí no es clara la intención así que podemos eliminar el registro completo o el dato en concreto.

Para ello podemos borrar la fila o poner el valor como nulo.

Elegimos poner nulo, para posteriormente tratarlo con el valor correspondiente (ahora viene bien no haber tocado nulos en numéricos)

In [26]:
print(df[df['horas_movil_dia'].isna()])
df.loc[df['horas_movil_dia'] > 24, 'horas_movil_dia'] = np.nan
print(df[df['horas_movil_dia'].isna()])
    edad     genero  horas_movil_dia  satisfaccion red_social_principal
31  28.0  Masculino              NaN           8.0              Twitter
91  61.0   Femenino              NaN           8.0             Facebook
    edad     genero  horas_movil_dia  satisfaccion red_social_principal
31  28.0  Masculino              NaN           8.0              Twitter
43  19.0       Otro              NaN           7.0               TikTok
55  25.0       Otro              NaN           6.0               TikTok
91  61.0   Femenino              NaN           8.0             Facebook
In [27]:
fig, ax = plt.subplots(1,3)
sns.boxplot(data=df[columnas_numericas[0]], ax=ax[0])
sns.boxplot(data=df[columnas_numericas[1]], ax=ax[1])
sns.boxplot(data=df[columnas_numericas[2]], ax=ax[2])
plt.tight_layout()
No description has been provided for this image

No tenemos outliers (aunque podriamos tenerlos si estuviesen justificados), así que en este momento tratamos los nulos numéricos

In [28]:
df[df.isna().any(axis=1)]
Out[28]:
edad genero horas_movil_dia satisfaccion red_social_principal
31 28.0 Masculino NaN 8.0 Twitter
33 NaN Masculino 4.0 5.0 TikTok
35 32.0 Otro 3.0 NaN LinkedIn
43 19.0 Otro NaN 7.0 TikTok
55 25.0 Otro NaN 6.0 TikTok
85 33.0 Otro 3.5 NaN Instagram
91 61.0 Femenino NaN 8.0 Facebook
94 30.0 Masculino 4.0 NaN Twitter

Tenemos nulos en todas las columnas numéricas ¿con que los rellenamos?

Tras eliminar los outliers la media y la mediana son estadísticamente similares en este dataset. Sin embargo tenemos valores enteros (o redondeados).

La media aritmética casi siempre genera decimales, lo cual resulta artificial.

La mediana nos facilitaría mantener valores enteros más realistas.

Aún así también podemos redondear para no tener ninguna posibilidad de decimales.

Finalmente usaremos la media redondeada, aunque la mediana también sería válida.

In [29]:
for col in columnas_numericas:
    media = df[col].mean().round()
    df[col] = df[col].fillna(media)
    print(f"Columna {col} rellenada con: {media}")
Columna edad rellenada con: 35.0
Columna satisfaccion rellenada con: 7.0
Columna horas_movil_dia rellenada con: 4.0
  1. Crear una nueva columna dependencia_movil, calculada mediante la fórmula: horas_movil_dia × (11 – satisfacción)
In [30]:
df['dependencia_movil'] = df['horas_movil_dia'] * (11 - df['satisfaccion'])
df
Out[30]:
edad genero horas_movil_dia satisfaccion red_social_principal dependencia_movil
0 24.0 Femenino 3.5 8.0 Instagram 10.5
1 30.0 Masculino 2.0 7.0 Twitter 8.0
2 18.0 Femenino 5.5 9.0 TikTok 11.0
3 45.0 Masculino 1.5 6.0 LinkedIn 7.5
4 29.0 Otro 4.0 5.0 Instagram 24.0
... ... ... ... ... ... ...
95 20.0 Femenino 6.0 8.0 TikTok 18.0
96 55.0 Otro 1.5 9.0 Facebook 3.0
97 34.0 Masculino 3.0 6.0 Instagram 15.0
98 48.0 Femenino 2.0 7.0 LinkedIn 8.0
99 25.0 Masculino 5.0 5.0 TikTok 30.0

87 rows × 6 columns

Mediante la fórmula horas_movil_dia×(11–satisfaccion), logramos invertir la escala de satisfacción: de este modo, una puntuación baja de felicidad actúa como un multiplicador de impacto, mientras que una satisfacción alta reduce el peso de las horas de uso.

Esta métrica permite diferenciar entre un uso intensivo pero funcional y un uso potencialmente problemático.

Al amplificar los casos donde confluyen muchas horas de pantalla y baja satisfacción, el dataset gana una dimensión analítica que permitiría segmentar a los usuarios por el impacto que dicho comportamiento tiene en su calidad de vida.

  1. Generar un AED básico comentando los estadísticos más importantes
In [31]:
display(df.describe(include='all').round(2))
edad genero horas_movil_dia satisfaccion red_social_principal dependencia_movil
count 87.00 87 87.00 87.00 87 87.00
unique NaN 3 NaN NaN 5 NaN
top NaN Femenino NaN NaN Instagram NaN
freq NaN 38 NaN NaN 24 NaN
mean 35.17 NaN 3.55 7.08 NaN 14.08
std 12.57 NaN 1.52 1.39 NaN 7.75
min 18.00 NaN 1.00 3.00 NaN 2.00
25% 25.00 NaN 2.35 6.00 NaN 8.00
50% 33.00 NaN 3.50 7.00 NaN 12.80
75% 44.50 NaN 4.50 8.00 NaN 20.00
max 65.00 NaN 7.00 10.00 NaN 36.00

Count es igual en todas las columnas confirmando que ya no hay nulos.

No tenemos edades, horas ni niveles de satisfacción fuera de rango.

El análisis más básico nos dice que los datos son plausibles.

Para empezar a sacar conclusiones vamos a ver la relación entre variables, sobre todo con la variable calculada de dependencia móvil.

In [32]:
df.corr(numeric_only=True).round(2)
Out[32]:
edad horas_movil_dia satisfaccion dependencia_movil
edad 1.00 -0.84 0.16 -0.68
horas_movil_dia -0.84 1.00 -0.07 0.74
satisfaccion 0.16 -0.07 1.00 -0.69
dependencia_movil -0.68 0.74 -0.69 1.00

Vemos que hay una clara correlación negativa entre la edad y las horas de uso del móvil (los mayores lo usan menos)

Para la satisfacción no hay correlaciones claras con ninguna variable.

La dependencia móvil la calculamos desde horas y satisfación por lo que no tiene sentido analizar su correlación (sabemos que viene de la fórmula: dependencia_movil = horas_movil_dia × (11 – satisfacción)). Y muestra una ligera correlación con la edad (debido a la diferencia de horas entre distintas edades).

Sin embargo puede ser interesante relacionarla con las variables categóricas.

In [33]:
df.groupby('genero')['dependencia_movil'].agg(['mean', 'count'])
Out[33]:
mean count
genero
Femenino 10.613158 38
Masculino 15.500000 34
Otro 19.633333 15

Respecto al genero vemos grandes diferencias. "Masculino" presenta un índice un 50% superior a "Femenino" y "Otro" es casi el doble, aunque la muestra de "Otro" es más pequeña no es algo excesivo, lo que nos sugiere que este perfil es el más vulnerable a la dependencia tecnológica en nuestro estudio.

In [34]:
tabla = df.groupby('red_social_principal')['dependencia_movil'].agg(['mean', 'count'])
tabla.sort_values(by='mean', ascending=False).round(2)
Out[34]:
mean count
red_social_principal
TikTok 21.40 20
Twitter 15.53 15
Instagram 14.31 24
LinkedIn 9.21 12
Facebook 6.87 16

Respecto a la red social vemos aún más diferencias: TikTok va en cabeza triplicando a Facebook, en la que se nota un uso mucho más esporádico. Podemos pensar que esto tiene más que ver con la edad que con la red social, pero debemos comprobarlo

In [35]:
# Debemos categorizar las edades para poder relacionar con variable categórica
df['rango_edad'] = pd.cut(df['edad'], bins=[0, 30, 45, 100], labels =['Joven', 'Adulto', 'Mayor'])
print(df['rango_edad'].value_counts())
df
rango_edad
Joven     39
Adulto    28
Mayor     20
Name: count, dtype: int64
Out[35]:
edad genero horas_movil_dia satisfaccion red_social_principal dependencia_movil rango_edad
0 24.0 Femenino 3.5 8.0 Instagram 10.5 Joven
1 30.0 Masculino 2.0 7.0 Twitter 8.0 Joven
2 18.0 Femenino 5.5 9.0 TikTok 11.0 Joven
3 45.0 Masculino 1.5 6.0 LinkedIn 7.5 Adulto
4 29.0 Otro 4.0 5.0 Instagram 24.0 Joven
... ... ... ... ... ... ... ...
95 20.0 Femenino 6.0 8.0 TikTok 18.0 Joven
96 55.0 Otro 1.5 9.0 Facebook 3.0 Mayor
97 34.0 Masculino 3.0 6.0 Instagram 15.0 Adulto
98 48.0 Femenino 2.0 7.0 LinkedIn 8.0 Mayor
99 25.0 Masculino 5.0 5.0 TikTok 30.0 Joven

87 rows × 7 columns

In [36]:
media_por_rango = df.groupby('rango_edad')['dependencia_movil'].mean()
moda_red_social = df.groupby('rango_edad')['red_social_principal'].apply(lambda x: x.mode()[0])
pd.concat([media_por_rango, moda_red_social], axis=1)
Out[36]:
dependencia_movil red_social_principal
rango_edad
Joven 19.284615 TikTok
Adulto 12.378571 Instagram
Mayor 6.305000 Facebook

Al cruzar los datos por edad, nuestra sospecha se confirma:

El grupo Joven tiene una dependencia altísima y su red favorita es TikTok.

El grupo Mayor tiene una dependencia muy baja y su red favorita es Facebook.

El ranking de redes sociales es casi un espejo del ranking de edad. No es necesariamente que TikTok tenga más dependencia, sino que su público (los jóvenes) tienen hábitos de conexión mayores.

Correlación es distinto a causalidad.

Si queréis ver más sobre este concepto pasaros por mi blog:

Correlación vs causalidad

Para verlo "en números" debemos usar la librería pingouin.

Deberemos instalar la librería:

  • A) usando consola

      pip install pingouin
  • B) o una celda en Colab

      !pip install pingouin
In [37]:
import pingouin as pg
In [38]:
# 1. Convertimos la red social a números (necesario para la función)
df['red_social_num'] = df['red_social_principal'].astype('category').cat.codes

# 2. Lanzamos la correlación parcial eliminando el factor red_social
resultado_edad = pg.partial_corr(data=df,
                                 x='dependencia_movil',
                                 y='edad',
                                 covar='red_social_num')

display(resultado_edad)
n r CI95% p-val
pearson 87 -0.582935 [-0.71, -0.42] 3.886556e-09

Expliquemos ese dato:

Pregunta: Si todo el mundo usara la misma red social, ¿la edad seguiría influyendo en la dependencia?

Resultado (-0.58): Al estar más cerca de -1 que de 0, nos dice que sí, y mucho. A medida que la edad aumenta, la dependencia del móvil disminuye. La edad tiene un peso propio muy fuerte que no depende de si el usuario está en TikTok o en Facebook.

In [39]:
# 3. Lanzamos la correlación parcial eliminado el factor edad
resultado_red = pg.partial_corr(data=df,
                                 x='dependencia_movil',
                                 y='red_social_num',
                                 covar='edad')

display(resultado_red)
n r CI95% p-val
pearson 87 0.129271 [-0.08, 0.33] 0.235521

Pregunta: Si todo el mundo tuviera la misma edad, ¿la elección de la red social seguiría explicando la dependencia?

Resultado (0.129): Es un valor mucho más cercano a 0

Nuestra sospecha se confirma matemáticamente: el ranking de redes sociales es, efectivamente, un espejo del ranking de edad.

Al realizar la correlación parcial, observamos que mientras la edad mantiene una influencia sólida sobre la dependencia (r=−0.58), el peso de la red social se desploma hasta un residual 0.12 cuando aislamos el efecto de la edad.

Esto demuestra que la red social (como TikTok o Facebook) no es la causa principal de la dependencia, sino una variable que simplemente refleja el perfil generacional del usuario. La dependencia es una cuestión generacional más que de plataforma.

  1. Exportar el DataFrame procesado a un archivo CSV, el cual será descargado.
In [40]:
df.to_csv('tarea.csv', index=False)