Estadística honesta en vez de "magia de IA": cómo construí un motor de pronóstico sin redes neuronales#
Casi todas las fichas de App Store hoy prometen "AI-powered insights". Abres la app y encuentras un envoltorio sobre un LLM externo, o una caja negra que escupe "riesgo: alto" sin una sola palabra de explicación. Cuando construí MeteoHealth —una app que busca conexiones entre el clima, el sueño, la actividad y el bienestar— elegí deliberadamente otro camino. Dentro no hay red neuronal ni Core ML, sino estadística clásica: correlaciones de Pearson, análisis de varianza con valores p y una corrección de Benjamini–Hochberg para comparaciones múltiples. Todo se calcula en el dispositivo, sin conexión, y —esto es lo que más me importa— cada resultado se puede explicar en una sola frase sencilla. Así funciona ese motor, y por qué creo que la estadística honesta es una decisión de producto más sólida que una etiqueta de moda como "AI-powered".
El hype de "AI-powered" y por qué me hizo dudar#
El problema no es que las redes neuronales sean malas: para reconocer imágenes o generar texto son insustituibles. El problema es que "IA" se convirtió en una palabra de marketing que se pega a cualquier cosa, incluidas simples reglas if-else. En salud digital esto es especialmente peligroso: un usuario ve "78% de riesgo de brote" y no tiene forma de preguntarle a la app "por qué". Los reguladores también lo notaron: la Ley de IA de la UE, por ejemplo, clasifica los sistemas médicos de IA por nivel de riesgo, y desde agosto de 2026 exige que los sistemas de alto riesgo ofrezcan explicaciones que permitan a quienes los usan entender realmente el resultado. La literatura académica llama a esto el "problema de la caja negra": un modelo predice pero no explica su razonamiento, y las técnicas de explicación post-hoc (LIME, SHAP y similares) solo aproximan la lógica real en lugar de reproducirla —hay críticos que argumentan que esto puede enmascarar el problema de confianza en vez de resolverlo.
Para una app de bienestar, eso es doblemente inaceptable. Si MeteoHealth le dice a alguien que su riesgo de dolor de cabeza está subiendo, esa persona debería entender que es porque bajó la presión atmosférica mientras su sueño también se acortó —no porque "el modelo lo decidió así". Esa es la decisión detrás del motor: construirlo sobre estadística donde la explicabilidad está integrada en las propias matemáticas, no añadida después como un módulo aparte.
Correlación de Pearson: la primera herramienta del motor#
La primera y más simple herramienta del motor es el coeficiente de correlación de Pearson. Mide cuán linealmente se mueven juntas dos variables —por ejemplo, la presión atmosférica y una puntuación diaria de bienestar. El valor va de −1 a 1: cero significa que no hay relación, y un valor cercano a cualquiera de los extremos significa una relación fuerte.
/// Pearson correlation coefficient between two data series
func pearsonCorrelation(_ x: [Double], _ y: [Double]) -> Double? {
guard x.count == y.count, x.count > 1 else { return nil }
let n = Double(x.count)
let meanX = x.reduce(0, +) / n
let meanY = y.reduce(0, +) / n
var numerator = 0.0
var sumSqX = 0.0
var sumSqY = 0.0
for i in 0..<x.count {
let dx = x[i] - meanX
let dy = y[i] - meanY
numerator += dx * dy
sumSqX += dx * dx
sumSqY += dy * dy
}
let denominator = (sumSqX * sumSqY).squareRoot()
guard denominator != 0 else { return nil }
return numerator / denominator
}Nada de magia aquí: sumas, medias, una raíz cuadrada. Pero este código sencillo tiene una propiedad que importa mucho: puedo abrirlo en un depurador, meter los datos reales de un usuario y ver exactamente los mismos números que ve el motor. Ninguna red neuronal ofrece ese nivel de transparencia —incluso una pequeña red totalmente conectada con un par de capas ocultas ya tiene miles de pesos, y no hay forma de extraer de ahí un "por qué" legible para un humano.
Análisis de varianza, valores p y el problema de comparaciones múltiples#
Una correlación por sí sola no basta: también hay que saber cuánto confiar en ella dado el volumen de datos disponible. Ahí entran el análisis de varianza (ANOVA) y los valores p: responden a la pregunta "¿pudo haber pasado esto por azar si en realidad no hay ninguna relación?". Cuanto menor el valor p, menos probable es que el patrón sea solo ruido.
Pero aquí se esconde una trampa estadística clásica. MeteoHealth revisa a la vez decenas de pares "factor vs. bienestar": presión, humedad, sueño, pasos, cafeína, HRV y más. Si se prueba cada par por separado con un umbral de p < 0.05, entonces de 20 pruebas, en promedio una parecerá "significativa" solo por azar —ese es el problema de comparaciones múltiples. Sin corregirlo, la app empezaría a mostrar al usuario patrones que se ven convincentes pero que no se sostienen con datos nuevos.
Imagina a un usuario que tuvo clima nublado y dolor de cabeza tres días seguidos. Aislada, esa correlación podría superar el umbral de p < 0.05 —pero considerando otros quince factores probados junto a ella, es casi con certeza una coincidencia. Por eso un solo valor p no basta: hace falta un paso que mire el cuadro completo de una vez, no un par aislado del resto.
La corrección de Benjamini–Hochberg: filtrar coincidencias#
La corrección de Benjamini–Hochberg controla la tasa de falsos descubrimientos entre todos los resultados "significativos" cuando se prueban muchas hipótesis a la vez. La idea es simple: ordenar los valores p de menor a mayor y encontrar el mayor que aún encaje bajo un umbral escalado.
/// Benjamini–Hochberg correction for multiple comparisons.
/// Returns the indices of hypotheses that remain significant
/// after controlling the false discovery rate at `alpha`.
func benjaminiHochberg(pValues: [Double], alpha: Double = 0.05) -> Set<Int> {
let m = Double(pValues.count)
let indexed = pValues.enumerated().sorted { $0.element < $1.element }
var lastSignificantRank = -1
for (rank, item) in indexed.enumerated() {
let k = Double(rank + 1)
let threshold = (k / m) * alpha
if item.element <= threshold {
lastSignificantRank = rank
}
}
guard lastSignificantRank >= 0 else { return [] }
return Set(indexed.prefix(lastSignificantRank + 1).map { $0.offset })
}En la práctica: si el motor prueba el bienestar contra 15 factores y tres vuelven con p < 0.05 por su cuenta, tras la corrección puede que solo sobreviva uno —el que realmente es robusto. Ese es el que se le muestra al usuario, con una frase como "tienes una relación estadísticamente significativa entre humedad por encima del 80% y menor energía"— una afirmación que de verdad superó una prueba contra el azar.
Líneas base personales y adaptación tras ~10 registros#
Los valores absolutos no significan nada sin contexto: una frecuencia cardíaca en reposo de 68 es normal para una persona y ya una desviación para otra. Por eso el motor construye una línea base personal para cada métrica y la actualiza a medida que llegan nuevos registros.
/// Updates a personal baseline using an exponentially weighted moving average,
/// so the model adapts as new daily entries arrive.
struct PersonalBaseline {
private(set) var mean: Double
private(set) var sampleCount: Int
private let smoothing: Double
init(initialMean: Double = 0, smoothing: Double = 0.2) {
self.mean = initialMean
self.sampleCount = 0
self.smoothing = smoothing
}
mutating func update(with value: Double) {
sampleCount += 1
if sampleCount <= 10 {
// Cold start: simple running average for the first ~10 entries
mean = mean + (value - mean) / Double(sampleCount)
} else {
// Warmed up: exponentially weighted average reacts to recent trends
mean = mean + smoothing * (value - mean)
}
}
}Durante aproximadamente los primeros diez registros, el motor usa un promedio simple acumulado —una fase de "arranque en frío" donde los datos escasean y conviene suavizar por igual los cambios bruscos. Después cambia a un promedio exponencialmente ponderado, que reacciona un poco más a los registros recientes sin dejar de ser resistente a valores atípicos puntuales. Sobre estas líneas base personales se construyen justamente los pronósticos de bienestar a 24 y 72 horas: el motor compara las condiciones actuales —clima, sueño, actividad— con el rango normal propio de cada persona y con las correlaciones que resultaron significativas específicamente para ella, en lugar de con datos promediados de todos los usuarios juntos.
Estadística vs. redes neuronales: una comparación honesta#
Podría haber entrenado un modelo pequeño con datos agregados, o conectado un LLM en la nube para "insights inteligentes". Deliberadamente no lo hice, y aquí está el porqué.
| Criterio | Motor estadístico | Red neuronal / IA en la nube |
|---|---|---|
| Explicabilidad | Cada resultado se reduce a una fórmula y un valor p | A menudo una caja negra; necesita explicaciones post-hoc |
| Privacidad | Los datos nunca salen del dispositivo; el análisis es offline | Suele requerir enviar los datos a un servidor |
| Datos necesarios para empezar | Funciona tras unos 10 registros por persona | Normalmente necesita miles de ejemplos etiquetados |
| Funciona sin conexión | Sí, siempre | Casi nunca, sin un modelo integrado |
| Predecibilidad | Determinista, reproducible | Puede variar entre ejecuciones y versiones del modelo |
Una segunda comparación, por tipo de tarea:
| Tarea | Herramienta adecuada |
|---|---|
| Encontrar una relación lineal entre dos métricas | Correlación de Pearson |
| Reconocer un objeto en una foto | Red neuronal (Core ML, Vision) |
| Comprobar si un hallazgo es estadísticamente significativo | ANOVA + valor p |
| Generar texto en lenguaje natural | LLM |
| Filtrar coincidencias entre decenas de hipótesis | Corrección de Benjamini–Hochberg |
Estas tablas dejan claro algo: la estadística y las redes neuronales resuelven clases distintas de problemas, y una no reemplaza a la otra. Pero para esta tarea concreta —encontrar patrones personales, explicables y privados en los datos de salud de una sola persona— la estadística clásica superó a las redes neuronales en todos los criterios prácticos que importaban: funciona con muestras pequeñas, no necesita servidor, no "alucina" y me permite, como desarrollador, mostrarle al usuario el motivo exacto por el que un pronóstico se volvió preocupante. Core ML sería excelente para reconocer emociones en una foto o clasificar actividad a partir del acelerómetro —pero no para la tarea de explicarle a una persona por qué mañana probablemente se sentirá peor.
Todo este motor funciona dentro de MeteoHealth, y por eso ahí cada pronóstico viene con una explicación, no solo un número. El hype de "AI-powered" pasará, pero la confianza del usuario se construye en si una app puede explicar por qué dijo lo que dijo. A veces la respuesta más honesta a "¿dónde está la IA?" es: "no la hay, pero hay estadística, y funciona".
Creo que la salud digital está pasando por la misma etapa que vivió el desarrollo web con los frameworks de JavaScript: primero todos quieren la herramienta de moda, luego todos quieren la herramienta que realmente resuelve el problema. La estadística clásica es menos emocionante que "una red neuronal predice tu salud", pero es reproducible, explicable y no le pide al usuario que confíe en una caja negra. Para una app que trata el bienestar de una persona concreta, eso no es un compromiso: es la única elección sensata.



