"""
╔══════════════════════════════════════════════════════════════════╗
║ EJERCICIO: ¿QUÉ VARIABLES DE LOS PINGÜINOS SE ║
║ RELACIONAN MÁS ENTRE SÍ? ║
╚══════════════════════════════════════════════════════════════════╝
Dataset: Palmer Penguins 🐧
Contiene medidas físicas de 344 pingüinos de 3 especies distintas
recogidas en las islas Palmer (Antártida).
Variables numéricas disponibles:
• bill_length_mm → longitud del pico (mm)
• bill_depth_mm → profundidad del pico (mm)
• flipper_length_mm → longitud de la aleta (mm)
• body_mass_g → masa corporal (gramos)
Variable categórica:
• species → especie (Adelie, Chinstrap, Gentoo)
Objetivo del ejercicio:
Descubrir qué par de variables numéricas tiene la correlación
más fuerte (positiva o negativa) usando pandas y stats.
Librerías necesarias:
pip install pandas matplotlib seaborn
Estructura del ejercicio:
PASO 0 — Cargar los datos
PASO 1 — Explorar el dataset
PASO 2 — Limpiar valores nulos
PASO 3 — Calcular la correlación entre TODAS las variables
PASO 4 — Encontrar el par con mayor correlación
★ RETO EXTRA — Escribe tus propias conclusiones
"""
# pandas: manejo de tablas de datos (DataFrames)
import pandas as pd
# seaborn: visualización estadística; también incluye datasets de ejemplo
import seaborn as sns
# numpy: operaciones numéricas con arrays (importado por convención,
# aunque en este script no se usa directamente)
import numpy as np
# stats de scipy: funciones estadísticas avanzadas;
# aquí la usamos para calcular la correlación de Pearson con su valor p
from scipy import stats
# ===========================================================================
# PASO 0 — CARGAR LOS DATOS
# ===========================================================================
# seaborn incluye el dataset de pingüinos de forma gratuita.
# load_dataset() lo descarga automáticamente la primera vez.
print("\n" + "="*60)
print(" PASO 0 — CARGAR LOS DATOS")
print("="*60)
# Carga el dataset "penguins" directamente desde seaborn.
# El resultado es un DataFrame de pandas con 344 filas y 7 columnas.
pinguinos = sns.load_dataset("penguins")
# Confirmamos cuántas filas y columnas tiene el DataFrame
# .shape devuelve una tupla (filas, columnas)
print(f"\n Dataset cargado con {pinguinos.shape[0]} filas y {pinguinos.shape[1]} columnas.")
print(f" Columnas: {pinguinos.columns.tolist()}")
# ===========================================================================
# PASO 1 — EXPLORAR EL DATASET
# ===========================================================================
# Antes de cualquier análisis, siempre hay que entender qué contiene
# la tabla. Usa .head(), .info() y .describe() para hacerte una idea.
print("\n" + "="*60)
print(" PASO 1 — EXPLORAR EL DATASET")
print("="*60)
# .head() muestra las 5 primeras filas del DataFrame.
# Muy útil para ver rápidamente la estructura y los valores reales.
print("\n── Primeras 5 filas (.head()) ──")
print(pinguinos.head())
# .info() muestra un resumen técnico: nombre de columna, tipo de dato
# (int, float, object…) y cuántos valores NO nulos tiene cada columna.
# Si una columna tiene menos valores que el total de filas, hay nulos.
print("\n── Información general (.info()) ──")
pinguinos.info()
# .describe() calcula automáticamente estadísticas básicas para cada
# columna numérica: media, desviación típica, mínimo, máximo y cuartiles.
# .round(1) redondea a 1 decimal para que sea más legible.
print("\n── Estadísticas descriptivas (.describe()) ──")
print(pinguinos.describe().round(1))
# .value_counts() cuenta cuántas veces aparece cada valor único en la columna.
# Aquí lo usamos para saber cuántos pingüinos hay de cada especie.
print("\n── Pingüinos por especie (.value_counts()) ──")
print(pinguinos["species"].value_counts())
# ===========================================================================
# PASO 2 — LIMPIAR VALORES NULOS
# ===========================================================================
# Algunos pingüinos tienen medidas incompletas (NaN).
# La correlación no funciona bien con valores nulos, así que los eliminamos.
print("\n" + "="*60)
print(" PASO 2 — LIMPIAR VALORES NULOS")
print("="*60)
# Eliminamos la columna "sex" porque no la vamos a usar en el análisis
# y además contiene nulos que nos complicarían el recuento.
# inplace=True modifica el DataFrame original sin necesidad de reasignarlo.
pinguinos.drop(columns=["sex"], inplace=True)
# .isnull() devuelve un DataFrame de True/False indicando dónde hay nulos.
# .sum() suma los True de cada columna (True = 1, False = 0),
# dando el total de nulos por columna.
print("\n── ¿Cuántos nulos hay en cada columna? ──")
print(pinguinos.isnull().sum())
# .dropna() elimina todas las filas que tengan AL MENOS un valor nulo
# en cualquier columna. Devuelve un nuevo DataFrame sin modificar el original.
pinguinos_limpio = pinguinos.dropna()
# Mostramos cuántas filas hemos perdido en la limpieza
print(f"\n Filas antes de limpiar : {len(pinguinos)}")
print(f" Filas después de limpiar: {len(pinguinos_limpio)}")
print(f" Filas eliminadas : {len(pinguinos) - len(pinguinos_limpio)}")
# ===========================================================================
# PASO 3 — CALCULAR LA CORRELACIÓN ENTRE LAS VARIABLES
# ===========================================================================
# El coeficiente de Pearson (r) mide la fuerza de la relación lineal
# entre dos variables numéricas. Siempre está entre -1 y +1:
# r = 1.0 → correlación perfecta positiva (cuando X sube, Y sube)
# r = -1.0 → correlación perfecta negativa (cuando X sube, Y baja)
# r = 0.0 → sin correlación lineal
# |r| > 0.7 se considera correlación FUERTE
print("\n" + "="*60)
print(" PASO 3 — MATRIZ DE CORRELACIÓN")
print("="*60)
# Usamos un alias corto para no repetir "pinguinos_limpio" en cada línea
df = pinguinos_limpio
# stats.pearsonr(x, y) devuelve dos valores:
# r → el coeficiente de correlación de Pearson (de -1 a +1)
# p → el valor p (p-value): probabilidad de obtener esta correlación
# por puro azar. Si p < 0.05, la correlación es estadísticamente
# significativa (muy poco probable que sea casualidad).
# Correlación entre masa corporal y longitud del pico
r1, p1 = stats.pearsonr(df["body_mass_g"], df["bill_length_mm"])
print(r1, p1)
# Correlación entre masa corporal y profundidad del pico
r2, p2 = stats.pearsonr(df["body_mass_g"], df["bill_depth_mm"])
print(r2, p2)
# Correlación entre masa corporal y longitud de la aleta
r3, p3 = stats.pearsonr(df["body_mass_g"], df["flipper_length_mm"])
print(r3, p3)
# --- PARA EXPERTOS: calcular TODAS las combinaciones posibles ---
# En lugar de comparar solo contra body_mass_g, calculamos el coeficiente
# entre CADA par posible de variables numéricas.
columnas_numericas = ["bill_length_mm", "bill_depth_mm",
"flipper_length_mm", "body_mass_g"]
# Lista donde iremos acumulando los resultados de cada par
correlaciones = []
# Doble bucle anidado para generar todos los pares sin repetir.
# El truco está en que j empieza en i+1:
# cuando i=0 → j recorre 1, 2, 3 (3 pares con bill_length_mm)
# cuando i=1 → j recorre 2, 3 (2 pares con bill_depth_mm)
# cuando i=2 → j recorre 3 (1 par con flipper_length_mm)
# Total: 6 pares únicos sin duplicados ni la diagonal (variable consigo misma)
for i in range(len(columnas_numericas)):
for j in range(i + 1, len(columnas_numericas)):
var1 = columnas_numericas[i]
var2 = columnas_numericas[j]
# Calculamos r y p para este par concreto
r, p = stats.pearsonr(df[var1], df[var2])
# Guardamos la tupla (variable1, variable2, r redondeado, p redondeado)
# float() convierte los valores numpy a float nativo de Python
correlaciones.append((var1, var2, float(round(r, 2)), float(round(p, 2))))
# ===========================================================================
# PASO 4 — ENCONTRAR EL PAR CON MAYOR CORRELACIÓN
# ===========================================================================
# Buscamos el par de variables con |r| más alto (en valor absoluto,
# para tratar igual las correlaciones positivas y las negativas).
print("\n" + "="*60)
print(" PASO 4 — PAR CON MAYOR CORRELACIÓN")
print("="*60)
# Ordenamos la lista de menor a mayor según |r| (valor absoluto de r).
# key=lambda x: abs(x[2]) indica que el criterio de orden es el
# tercer elemento de cada tupla (el coeficiente r), tomado en valor absoluto.
# Con esto el último elemento de la lista será el par más correlacionado.
correlaciones.sort(key=lambda x: abs(x[2]))
# Mostramos todos los pares ordenados de menor a mayor correlación
for var1, var2, corr, p in correlaciones:
print(var1, var2, corr, p)
# ===========================================================================
# ★ RETO EXTRA — PREGUNTAS PARA REFLEXIONAR
# ===========================================================================
print("\n" + "="*60)
print(" ★ RETO EXTRA — RESPONDE ESTAS PREGUNTAS")
print("="*60)
print("""
Completa estas frases con lo que has descubierto:
1. El par de variables con MAYOR correlación es:
__________ y __________ con r = _______
2. El par de variables con MENOR correlación es:
__________ y __________ con r = _______
3. ¿La correlación más alta es positiva o negativa?
_______________________________________________________
4. ¿Qué significa en la práctica que esas dos variables
tengan una correlación tan alta?
_______________________________________________________
5. ¿La correlación cambia mucho entre especies?
¿Qué especie tiene la correlación más diferente al global?
_______________________________________________________
6. Correlación NO implica causalidad. ¿Puedes pensar en una
razón biológica que explique la relación encontrada?
_______________________________________________________
""")