import numpy as np # Librería esencial para cálculos matemáticos con arrays.
# Alternativa sin instalar nada: import statistics (librería estándar de Python)
# Minutos respecto a las 8:30
# Negativo = llega antes | 0 = en punto | Positivo = llega tarde
# Contexto estadístico: estamos midiendo una DESVIACIÓN respecto a un valor de referencia (8:30).
# Esto es exactamente lo que hace la desviación típica internamente: medir distancias respecto a la media.
# Alumno A — puntual, se adelanta o se retrasa apenas unos minutos
# Contexto: datos con poca dispersión → sigma pequeña esperada
# Alternativa para crear el array: alumno_a = np.array(list(map(int, "-3,2,1,-1,4,-3,4,1,-2,3,-1,2,2,1,-2".split(","))))
alumno_a = np.array([-3, 2, 1, -1, 4, -3, 4, 1, -2, 3, -1, 2, 2, 1, -2])
# Alumno B — casi siempre tarde 5-10 min, un día llega 30 min tarde
# Contexto: los valores -35 y -42 son outliers extremos (llegó muy pronto esos días).
# Esos dos valores "compensan" matemáticamente los retrasos para igualar la media con alumno_a.
# Alternativa para verificar la suma: print(sum([-3, 2, 1, ...])) antes de crear el array
alumno_b = np.array([5, 6, 3, -35, 8, 6, 8, 15, 6, 8, 5, 3, 6, 5, -42])
# Iteramos sobre los dos alumnos con sus etiquetas.
# Alternativa con diccionario: alumnos = {"Alumno A": alumno_a, "Alumno B": alumno_b}
# for nombre, datos in alumnos.items():
for nombre, datos in [("Alumno A (puntual)", alumno_a), ("Alumno B (impuntual)", alumno_b)]:
print(f"{nombre}")
print(f" Datos: {list(datos)}")
# np.mean() = suma de todos los valores / cantidad de valores.
# Contexto: la media puede ser igual para ambos alumnos aunque su comportamiento sea opuesto.
# Esto demuestra que la media sola no cuenta toda la historia → necesitamos la sigma.
# Alternativa: sum(datos) / len(datos) o statistics.mean(datos)
print(f" Media: {np.mean(datos):.1f} min | σ = {np.std(datos):.1f} min | Min: {datos.min()} min | Max: {datos.max()} min")
# Percentil 25 = Q1: el 25% de los datos están POR DEBAJO de este valor.
# Percentil 75 = Q3: el 75% de los datos están POR DEBAJO de este valor.
# Alternativa con pandas: Q1 = pd.Series(datos).quantile(0.25)
Q1 = np.percentile(datos, 25)
Q3 = np.percentile(datos, 75)
# IQR = distancia entre Q1 y Q3. Contiene el 50% central de los datos.
# Contexto: a diferencia de la sigma, el IQR ignora los extremos → más robusto ante outliers.
# Si el IQR es pequeño, la mayoría de los datos están concentrados. Si es grande, hay dispersión.
IQR = Q3 - Q1
# sorted() ordena la lista de menor a mayor para visualizar los datos fácilmente.
# [int(x) for x in datos] convierte de float a entero para una lectura más limpia.
# Alternativa: print(np.sort(datos)) — directamente con NumPy sin convertir a lista
print(sorted([int(x) for x in datos]))
print(f" Cuartil 1 {Q1}, Cuartil 3 {Q3}, Rango intercuartílico {IQR}")
# Regla de Tukey (1977): un valor es outlier si está más allá de 1.5×IQR desde Q1 o Q3.
# El factor 1.5 es el estándar universal. Con factor=3.0 solo detectaría outliers extremos.
# Alternativa: from scipy import stats → stats.iqr(datos) calcula el IQR directamente
inferior=Q1-1.5*IQR
superior=Q3+1.5*IQR
print(f"Límites: {inferior}, {superior}")
# List comprehension: crea una lista filtrando solo los valores que cumplen la condición.
# Es equivalente a un bucle for con un if dentro, pero en una sola línea.
# Alternativa con NumPy (más eficiente para datos grandes): datos[datos < inferior]
print(f"Valores atípicos por debajo {[int(x) for x in datos if x <inferior]}")
print(f"Valores atípicos por encima {[int(x) for x in datos if x > superior]}")
# Contexto: alumno_b tendrá outliers (-35 y -42) porque son días excepcionales.
# Alumno_a probablemente no tenga ninguno, sus datos son consistentes.
print()
# Segunda parte: aplicamos el mismo análisis a un conjunto de alturas reales.
# Contexto: cambiamos de dominio (minutos → cm) pero el método estadístico es idéntico.
# Esto demuestra la universalidad del método IQR para detectar outliers en cualquier dataset.
# Alternativa para cargar datos reales: alturas = pd.read_csv("alturas.csv")["altura"].tolist()
alturas=[172, 173, 154, 168, 175, 177, 168, 164, 167, 162, 173, 159, 182, 160, 161, 177, 154, 154, 167, 179, 179, 178, 172, 165, 173, 174, 165, 172, 166, 187, 179, 176, 171, 155, 163, 160, 187, 167, 167, 171, 168, 158, 170, 181, 174, 169, 176, 164, 175, 166, 167, 176, 171, 159, 166, 177, 159, 162, 167, 169, 161, 185, 165, 173, 171, 173, 172, 177, 164, 169, 162, 169, 181, 174, 169, 175, 174, 173, 185, 172, 166, 148, 159, 167, 163, 167, 164, 169, 173, 189, 169, 171, 177, 161, 168, 178, 170, 181, 158, 188]
# ¿Hay valores atípicos?
# Con 100 datos ya no tiene sentido revisarlos a ojo → necesitamos el método automático.
# Contexto: en distribuciones normales (como las alturas) esperamos muy pocos outliers,
# ya que el 99.7% de los datos caen dentro de ±3σ. El IQR confirmará lo mismo de otra forma.
# Reutilizamos exactamente el mismo bloque de cálculo que con los alumnos.
# Contexto: esto muestra que el método IQR es independiente del dominio de los datos.
# Alternativa completa con pandas: pd.Series(alturas).describe() muestra Q1, Q3 y más de golpe
Q1=np.percentile(alturas, 25)
Q3=np.percentile(alturas, 75)
IQR = Q3 - Q1
# inferior y superior definen la "valla" de Tukey.
# Todo lo que quede fuera de [inferior, superior] se considera estadísticamente inusual.
# Contexto: en alturas humanas, esperamos que los outliers sean alturas muy extremas
# (alguien muy bajo o muy alto respecto al grupo analizado).
inferior=Q1-1.5*IQR
superior=Q3+1.5*IQR
# Los valores atípicos son los menores del valor inferior o mayores del valor superios
print(f"Límites: {inferior}, {superior}")
# Si la lista resultante está vacía ([]) significa que no hay outliers en ese extremo.
# Contexto: en una distribución normal pura, ~0.7% de los datos serían outliers por este método,
# lo que con 100 datos equivale a esperar 0 o 1 outlier aproximadamente.
# Alternativa con NumPy: print(alturas[np.array(alturas) < inferior])
print(f"Valores atípicos por debajo {[int(x) for x in alturas if x < inferior]}")
print(f"Valores atípicos por encima {[int(x) for x in alturas if x > superior]}")