Ejercicio coches

"""
╔══════════════════════════════════════════════════════════════════╗
║   EJERCICIO: "NOS HAN DICHO QUE LOS COCHES MÁS POTENTES        ║
║               GASTAN MÁS COMBUSTIBLE... ¿ES CIERTO?"            ║
╚══════════════════════════════════════════════════════════════════╝

LA SITUACIÓN
────────────
  Un amigo mecánico te dice:
  "Oye, está clarísimo: cuantos más caballos tiene un coche,
   más combustible gasta. Siempre ha sido así."

  ¿Tiene razón? ¿Podemos demostrarlo (o refutarlo) con datos?

  En este ejercicio vas a:
    1. Cargar un dataset real de coches
    2. Entender qué contiene
    3. Limpiarlo correctamente
    4. Calcular si realmente existe esa relación
    5. Visualizarla
    6. Responder preguntas para interpretar los resultados

EL DATASET: MPG (Miles Per Gallon)
────────────────────────────────────
  Contiene información de 398 coches fabricados entre 1970 y 1982.
  Fue recopilado originalmente por la revista Consumer Reports y
  es uno de los datasets clásicos del aprendizaje automático.

  Columnas principales:
    mpg          → millas por galón (eficiencia: más = gasta MENOS)
    cylinders    → número de cilindros del motor
    displacement → cilindrada (pulgadas cúbicas)
    horsepower   → caballos de potencia (HP)
    weight       → peso del coche (libras)
    acceleration → aceleración (segundos de 0 a 60 mph)
    model_year   → año del modelo (70 = 1970, 82 = 1982)
    origin       → origen (1=EEUU, 2=Europa, 3=Japón)
    name         → nombre del modelo

  PREGUNTA A INVESTIGAR:
    ¿Existe relación entre los caballos de potencia (horsepower)
    y el consumo de combustible (mpg)?

Librerías necesarias:
  pip install pandas matplotlib seaborn
"""

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns
import numpy as np
from scipy import stats


# ═══════════════════════════════════════════════════════════════════
#  PASO 1 — CARGA DE DATOS
# ═══════════════════════════════════════════════════════════════════
# Cargamos el dataset "mpg" directamente desde seaborn.
# seaborn incluye varios datasets de ejemplo listos para usar;
# no hace falta descargar ningún archivo manualmente.

print("\n" + "═"*60)
print("  PASO 1 — CARGA DE DATOS")
print("═"*60)

coches = sns.load_dataset("mpg")

# Comprobamos que se ha cargado correctamente: cuántas filas y columnas
print(f"\n  Filas    : {coches.shape[0]}")
print(f"  Columnas : {coches.shape[1]}")
print(f"  Nombres  : {coches.columns.tolist()}")


# ═══════════════════════════════════════════════════════════════════
#  PASO 2 — PRIMER VISTAZO
# ═══════════════════════════════════════════════════════════════════
# Antes de analizar nada, siempre hay que MIRAR los datos.
# Es como abrir una caja antes de saber qué hay dentro.

print("\n" + "═"*60)
print("  PASO 2 — PRIMER VISTAZO")
print("═"*60)

print("\n── .head(): primeras 5 filas ──────────────────────────────")
print(coches.head())

print("\n── .tail(): últimas 5 filas ───────────────────────────────")
print(coches.tail())

print("\n── .dtypes: tipo de dato de cada columna ──────────────────")
print(coches.dtypes)
# ATENCIÓN: fíjate en el tipo de 'horsepower'. ¿Es numérico?
# Si aparece como 'object' en lugar de 'float64', significa que
# pandas lo ha interpretado como texto.

print("\n── .describe(): estadísticas rápidas ──────────────────────")
print(coches.describe().round(2))
# describe() solo muestra columnas numéricas por defecto.
# Si 'horsepower' no aparece aquí, confirma que es texto.


# ═══════════════════════════════════════════════════════════════════
#  PASO 3 — ANÁLISIS DE CALIDAD: ¿ESTÁN BIEN LOS DATOS?
# ═══════════════════════════════════════════════════════════════════
# Los datos reales casi nunca son perfectos. Hay que buscar:
#   a) Valores nulos (celdas vacías)
#   b) Tipos de datos incorrectos
#   c) Valores extraños o imposibles

print("\n" + "═"*60)
print("  PASO 3 — ANÁLISIS DE CALIDAD DE LOS DATOS")
print("═"*60)

# ── a) Valores nulos ────────────────────────────────────────────
print("\n── a) ¿Cuántos valores nulos hay por columna? ─────────────")
nulos = coches.isnull().sum()
print(nulos)
print(f"\n  Total de celdas nulas: {nulos.sum()}")

print("\n── c) Rango de 'mpg' ──────────────────────────────────────")
print(f"  Mínimo: {coches['mpg'].min():.1f}  |  Máximo: {coches['mpg'].max():.1f}")


# ═══════════════════════════════════════════════════════════════════
#  PASO 4 — LIMPIEZA DE DATOS
# ═══════════════════════════════════════════════════════════════════
# Ahora que sabemos los problemas, los corregimos uno a uno.
# Es importante hacerlo en un paso separado y documentado.

print("\n" + "═"*60)
print("  PASO 4 — LIMPIEZA DE DATOS")
print("═"*60)

# Hacemos una copia para no modificar el dataset original
# (buena práctica: siempre trabajar sobre una copia)
df = coches.copy()

# ── Corrección 2: eliminar filas con NaN en las columnas clave ───
# Solo nos importan 'mpg' y 'horsepower' para este análisis.
# dropna(subset=[...]) elimina solo las filas con NaN en esas columnas.
filas_antes = len(df)
df = df.dropna(subset=["mpg", "horsepower"])
filas_despues = len(df)

print(f"  ✓ Filas eliminadas con NaN: {filas_antes - filas_despues}")
print(f"  ✓ Filas disponibles para el análisis: {filas_despues}")

# ── Verificación final ───────────────────────────────────────────
print(f"\n  Nulos restantes en 'mpg'       : {df['mpg'].isnull().sum()}")
print(f"  Nulos restantes en 'horsepower': {df['horsepower'].isnull().sum()}")
print("\n  Dataset limpio y listo para analizar.")


# ═══════════════════════════════════════════════════════════════════
#  PASO 5 — ESTADÍSTICAS DESCRIPTIVAS DE LAS VARIABLES CLAVE
# ═══════════════════════════════════════════════════════════════════
# Antes de calcular la correlación, conoce bien cada variable
# por separado. ¿En qué rango están? ¿Hay mucha variación?

print("\n" + "═"*60)
print("  PASO 5 — ESTADÍSTICAS DE LAS VARIABLES CLAVE")
print("═"*60)

for col, nombre in [("mpg", "Eficiencia (mpg)"),
                    ("horsepower", "Potencia (HP)")]:
    s = df[col]
    print(f"\n  {nombre}:")
    print(f"    Media         : {s.mean():.1f}")
    print(f"    Mediana       : {s.median():.1f}")
    print(f"    Desv. típica  : {s.std():.1f}")
    print(f"    Mínimo        : {s.min():.1f}")
    print(f"    Máximo        : {s.max():.1f}")


# ═══════════════════════════════════════════════════════════════════
#  PASO 6 — CALCULAR LA CORRELACIÓN
# ═══════════════════════════════════════════════════════════════════
# Ahora sí: calculamos el coeficiente de Pearson entre
# 'horsepower' y 'mpg'.
#
# RECORDATORIO:
#   r cercano a +1 → relación positiva fuerte
#   r cercano a -1 → relación negativa fuerte
#   r cercano a  0 → sin relación lineal clara
#
# OJO: mpg mide eficiencia (más mpg = MENOS consumo).
# Si el amigo tiene razón, esperamos r NEGATIVO:
# más HP → menos mpg (peor eficiencia = más consumo).

print("\n" + "═"*60)
print("  PASO 6 — CORRELACIÓN DE PEARSON")
print("═"*60)

r,p=stats.pearsonr(df["mpg"], df["horsepower"])
print(f"La correlación entre potencia y eficiencia es {r:.2f} con una significación de {p:.2f}.")

# ═══════════════════════════════════════════════════════════════════
#  PASO 7 — PREGUNTAS INTERPRETATIVAS
# ═══════════════════════════════════════════════════════════════════
# Lee los resultados con atención antes de responder.

print("\n" + "═"*60)
print("  PASO 8 — PREGUNTAS PARA REFLEXIONAR")
print("═"*60)
print(f"""
  Antes de responder, fíjate en:
    • El valor de r y su signo

  ──────────────────────────────────────────────────────────
  BLOQUE A — Sobre los datos

  1. ¿Cuántos coches tenía el dataset original?
     ¿Cuántos se han eliminado en la limpieza y por qué?
     ¿Crees que esa pérdida afecta al análisis?



  ──────────────────────────────────────────────────────────
  BLOQUE B — Sobre la correlación

  4. El coeficiente r 
     ¿Es positivo o negativo? ¿Qué significa eso en este contexto?
     (Recuerda: mpg alto = gasta POCO; mpg bajo = gasta MUCHO)

  5. ¿Tenía razón el amigo mecánico?
     Formula la respuesta con datos concretos.

  6. R² 
     Eso significa que la potencia (HP) explica el ___% de las
     diferencias de consumo entre coches.
     ¿Qué otros factores podrían explicar el resto?

  7. El p-valor es 
     ¿Podemos confiar en este resultado o podría ser coincidencia?

  

  ──────────────────────────────────────────────────────────
  BLOQUE D — Reflexión final

  11. "Correlación no implica causalidad."
      ¿Puedes pensar en una variable que esté relacionada tanto
      con la potencia como con el consumo y que pudiera estar
      "confundiendo" la relación? (Pista: mira las columnas del
      dataset)

  12. Si quisieras PREDECIR el consumo de un coche nuevo del que
      solo sabes los HP, ¿usarías este modelo?
      ¿Qué columnas añadirías para mejorar la predicción?
""")



Solución pingüinos

"""
╔══════════════════════════════════════════════════════════════════╗
║       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?
     _______________________________________________________
""")