Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 80: El lavado de manos del Dr. Semmelweis — distribuciones y pruebas t

100 Días de Código en Python — Día 80: El lavado de manos del Dr. Semmelweis — distribuciones y pruebas t

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 80 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 80. Las secciones 📝 Contenido original del curso provienen de las lecciones 591 a 596; el cuaderno de inicio y los archivos annual_deaths_by_clinic.csv y monthly_deaths.csv están en los recursos del curso (Dr Semmelweis Analysis (start).zip). En varias lecciones parte del código aparece solo en imágenes; aquí se reconstruye y se verifica. Los retos 🟢🟡🟠🔴, las secciones ¿qué es un valor p? y comunicar con datos, el proyecto extra y la etapa 5 del Laboratorio de Datos Abiertos son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, pandas 3.0.6, NumPy 2.5.3, Matplotlib 3.11.2, seaborn 0.13.2, plotly 7.1.0 y SciPy 1.18.1, y con una copia pública de los dos CSV (publicada en GitHub por otro estudiante del mismo curso), se ejecutó todo el código. Los resultados coinciden con el curso: 7,08 % de mortalidad en la década; 9,92 % en la clínica 1 y 3,88 % en la clínica 2; 10,5 % antes y 2,11 % después del lavado de manos (5 veces menos); valor p de 0,0000002985. Hay cuatro observaciones importantes: los datos mensuales corresponden a la clínica 1, no al hospital completo; el lavado obligatorio empezó en junio de 1847 (una lección dice "junio de 1846"); la reducción de 8,4 es de puntos porcentuales, no de "8,4 %"; y la prueba t del curso supone varianzas iguales, cuando la dispersión antes del lavado es casi 5 veces mayor (con la versión de Welch la conclusión se mantiene, con un valor p aún menor).

🏫 Para el aula: el tema incluye muertes de madres después del parto. Conviene presentarlo con respeto y enfocarlo en lo que enseña: cómo los datos, bien analizados y bien comunicados, salvan vidas.


Tabla de contenidos

  1. Antes de empezar: conviértete en el Dr. Semmelweis
  2. Explorar los datos y graficar nacimientos y muertes
  3. Los datos anuales por clínica
  4. El efecto del lavado de manos
  5. Distribuciones y significancia estadística
  6. 📖 ¿Qué es un valor p?
  7. 📖 El final de la historia: comunicar con datos
  8. 🚀 Proyecto del día: el informe que Semmelweis necesitaba
  9. 🌟 Proyecto extra: ¿funcionó la campaña del colegio?
  10. 🏗️ Laboratorio de Datos Abiertos — Etapa 5
  11. 🧩 Comprueba lo aprendido
  12. 🤖 Programar con apoyo de IA
  13. 🛠️ Correcciones técnicas al material original
  14. 📋 Matriz de trazabilidad
  15. 📦 Evidencias de aprendizaje del Día 80

0. Antes de empezar: conviértete en el Dr. Semmelweis

(Fuente: 591-day-80-goals-what-you-will-make-by-the-end-of-the-day.md)

📝 Contenido original del curso

Hoy serás el Dr. Ignaz Semmelweis, médico húngaro nacido en 1818 que trabajó en el Hospital General de Viena. En esa época nadie conocía las bacterias, los gérmenes ni los virus: se creía que las enfermedades venían del "mal aire" o de espíritus malignos. Pero en el siglo XIX los médicos empezaron a estudiar la anatomía, hacer autopsias y argumentar con datos. Semmelweis sospechaba que algo andaba mal en los procedimientos del hospital: ¿por qué morían tantas mujeres de fiebre puerperal en las salas de maternidad?

Aprenderás a construir un argumento convincente con datos; a superponer histogramas; a estimar una distribución con una KDE (Kernel Density Estimate); a usar SciPy para probar la significancia estadística con valores p; a resaltar partes de una serie de tiempo en Matplotlib; a configurar una leyenda; y a usar np.where().

Preparación: descarga el ZIP, sube el cuaderno a Google Drive, ábrelo con Colab y agrega los dos CSV.

Prerequisitos: dos ejes y localizadores de fechas (Día 75), media móvil (Día 73), plotly (Día 76), np.where y broadcasting (Día 77), histogramas y KDE de seaborn (Día 79). En tu computador: pip install scipy.


1. Explorar los datos y graficar nacimientos y muertes

(Fuente: 592-preliminary-data-exploration-and-visualising-births-y-deaths-at-vienna-hospital.md)

🎯 Objetivo de aprendizaje

Explorar dos tablas relacionadas, calcular una tasa y graficar dos series con dos ejes y localizadores de fecha.

📝 Contenido original del curso

Reto 1: ¿forma de df_yearly y df_monthly?, ¿columnas?, ¿años?, ¿NaN o duplicados?, ¿nacimientos y muertes promedio por mes?

Los datos cubren de 1841 a 1849. Ambas tablas registran nacimientos y muertes; la anual los separa por clínica. No hay NaN (.info() o .isna().values.any()) ni duplicados: los datos parecen limpios. Con .describe(): en promedio, unos 267 nacimientos y 22,47 muertes por mes.

Reto 2: ¿qué tan peligroso era dar a luz? Con los datos anuales, el porcentaje de mujeres que murieron. (En comparación, Estados Unidos registró 18,5 muertes maternas por 100 000 partos en 2013, un 0,018 %.)

Python
prob = df_yearly.deaths.sum() / df_yearly.births.sum() * 100
print(f'Chances of dying in the 1840s in Vienna: {prob:.3}%')   # 7.08%

"Si alguien me diera una bolsa de 100 dulces y me dijera que 7 me matarían, (probablemente) no me los comería".

Reto 3: gráfico con dos ejes y: localizadores de años y meses, eje x ajustado a los datos, cuadrícula, colores skyblue y crimson, línea discontinua para las muertes, grosor 3 y 2. ¿Notas algo a finales de los 1840?

Python
years = mdates.YearLocator()
months = mdates.MonthLocator()
years_fmt = mdates.DateFormatter('%Y')

plt.figure(figsize=(14, 8), dpi=200)
plt.title('Total Number of Monthly Births and Deaths', fontsize=18)
plt.yticks(fontsize=14)
plt.xticks(fontsize=14, rotation=45)

ax1 = plt.gca()
ax2 = ax1.twinx()
ax1.set_ylabel('Births', color='skyblue', fontsize=18)
ax2.set_ylabel('Deaths', color='crimson', fontsize=18)

ax1.set_xlim([df_monthly.date.min(), df_monthly.date.max()])
ax1.xaxis.set_major_locator(years)
ax1.xaxis.set_major_formatter(years_fmt)
ax1.xaxis.set_minor_locator(months)
ax1.grid(color='grey', linestyle='--')

ax1.plot(df_monthly.date, df_monthly.births, color='skyblue', linewidth=3)
ax2.plot(df_monthly.date, df_monthly.deaths, color='crimson', linewidth=2, linestyle='--')
plt.show()

Algo pasó después de 1847: las muertes bajan aunque los nacimientos suben.

📖 Explicación conceptual (verificada)

La carga de los datos (en el cuaderno) usa parse_dates para convertir la fecha al leer:

Python
df_yearly = pd.read_csv('annual_deaths_by_clinic.csv')
df_monthly = pd.read_csv('monthly_deaths.csv', parse_dates=['date'])
Tabla Forma Periodo Columnas
df_yearly (12, 4) 1841–1846 (6 años × 2 clínicas) year, births, deaths, clinic
df_monthly (98, 3) enero de 1841 – marzo de 1849 (falta diciembre de 1841) date, births, deaths

🛠️ Nota técnica (verificado): ¿de qué clínica son los datos mensuales? El curso habla del "total" de nacimientos y muertes, pero al sumar los datos mensuales por año, las muertes coinciden exactamente con las de la clínica 1 de 1843 a 1846 (274, 260, 241 y 459) y los nacimientos son del orden de esa clínica, no de las dos juntas. Los datos mensuales corresponden a la clínica 1, la de los médicos. Es justo la clínica donde se aplicó el lavado de manos, así que el análisis del curso tiene sentido, pero hay que decirlo: las tasas mensuales no son del hospital completo.

¿Tasa o conteo? El 7,08 % combina las dos clínicas de 1841 a 1846. Para comparar grupos de distinto tamaño siempre se usan tasas (muertes por cada 100 nacimientos), no conteos: una clínica con más partos tendrá más muertes aunque sea igual de segura.

⚠️ Errores frecuentes

  • Olvidar parse_dates: la fecha queda como texto y los localizadores no funcionan.
  • Formato :.3 en porcentajes: son 3 cifras significativas, no 3 decimales (7.08). Para decimales fijos: :.2f.
  • Poner los localizadores en ax2: el eje x es compartido, pero se configura en ax1.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: calcula la tasa de mortalidad por año de toda la tabla anual y grafícala.
  • 🟡 Reto 2: comprueba tú mismo la nota técnica: agrupa df_monthly por año (df_monthly.date.dt.year) y compáralo con cada clínica de df_yearly.

2. Los datos anuales por clínica

(Fuente: 593-analysing-the-yearly-data-split-by-clinic.md)

📝 Contenido original del curso

El hospital tiene dos maternidades: la clínica 1, atendida por médicos y estudiantes de medicina (todos hombres), y la clínica 2, atendida por parteras.

Reto 1: líneas de plotly de nacimientos y muertes por clínica. ¿Cuál es más grande? ¿Aumentaron los pacientes? ¿Máximo de muertes en cada clínica?

Python
line = px.line(df_yearly, x='year', y='births', color='clinic',
               title='Total Yearly Births by Clinic')
line.show()

Cada vez más mujeres daban a luz en el hospital; la clínica 1 era la más ocupada, y también tenía más muertes.

Reto 2: para comparar "manzanas con manzanas", la proporción de muertes por clínica:

Python
df_yearly['pct_deaths'] = df_yearly.deaths / df_yearly.births

clinic_1 = df_yearly[df_yearly.clinic == 'clinic 1']
avg_c1 = clinic_1.deaths.sum() / clinic_1.births.sum() * 100
print(f'Average death rate in clinic 1 is {avg_c1:.3}%.')   # 9.92%

clinic_2 = df_yearly[df_yearly.clinic == 'clinic 2']
avg_c2 = clinic_2.deaths.sum() / clinic_2.births.sum() * 100
print(f'Average death rate in clinic 2 is {avg_c2:.3}%.')   # 3.88%

1842 fue un año terrible: murió cerca del 16 % en la clínica 1 y cerca del 7,6 % en la clínica 2. Aun así, la clínica 2 tuvo siempre una mortalidad menor. Eso desconcertaba a Semmelweis.

La historia continúa: primero pensó que era la posición del parto (de lado en la clínica de parteras, de espaldas en la de médicos): cambiarla no tuvo efecto. Luego, que el sacerdote que atravesaba la clínica 1 tocando una campana tras cada muerte asustaba tanto a las mujeres que enfermaban: cambió su ruta y no tuvo efecto. Frustrado, se fue de vacaciones a Venecia. Al volver supo que un colega patólogo había muerto tras cortarse el dedo durante la autopsia de una mujer fallecida por fiebre puerperal, con los mismos síntomas. Su nueva teoría: los médicos y estudiantes llevaban en las manos "partículas de cadáver" de las autopsias a la sala de partos.

📖 Resultados verificados y pensamiento científico

Clínica Nacimientos (1841–1846) Muertes Tasa Peor año
Clínica 1 (médicos) 20 042 1 989 9,92 % 1842: 15,8 % (518 muertes)
Clínica 2 (parteras) 17 791 691 3,88 % 1842: 7,6 % (202 muertes)

Semmelweis siguió el método científico: observó una diferencia → propuso hipótesis (posición, sacerdote) → las probó cambiando una sola cosa → las descartó cuando los datos no cambiaron → formuló una nueva hipótesis a partir de una observación clave (la muerte del patólogo). Descartar hipótesis también es avanzar.

🛠️ Nota técnica: el reto pregunta por la "tasa mensual más alta" de cada clínica, pero la tabla por clínica es anual; con estos datos solo se puede responder con años.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: grafica pct_deaths por clínica como porcentaje (multiplica por 100) y con etiquetas en español.
  • 🟡 Reto 2: calcula cuántas veces más riesgosa era la clínica 1 que la clínica 2 en cada año.
  • 🔴 Reto 3 — Pensamiento computacional: ¿qué otras explicaciones (distintas de las manos) podrían explicar la diferencia entre clínicas? Propón dos y diseña cómo las pondrías a prueba con datos.

3. El efecto del lavado de manos

(Fuente: 594-the-effect-of-handwashing.md)

🎯 Objetivo de aprendizaje

Dividir una serie de tiempo en "antes" y "después", calcular sus tasas y resaltar cada tramo en un gráfico con leyenda.

📝 Contenido original del curso

Semmelweis ordenó que todo el personal se lavara las manos y los instrumentos no solo con agua y jabón, sino con una solución de cloro (un excelente desinfectante, aunque él no lo sabía: lo eligió para quitar el olor de las autopsias).

Reto 1: columna pct_deaths en df_monthly; dos subconjuntos, antes y después; tasas de cada periodo.

Python
handwashing_start = pd.to_datetime('1847-06-01')   # definida en el cuaderno

df_monthly['pct_deaths'] = df_monthly.deaths / df_monthly.births
before_washing = df_monthly[df_monthly.date < handwashing_start]
after_washing = df_monthly[df_monthly.date >= handwashing_start]

bw_rate = before_washing.deaths.sum() / before_washing.births.sum() * 100
aw_rate = after_washing.deaths.sum() / after_washing.births.sum() * 100
print(f'Average death rate before 1847 was {bw_rate:.4}%')   # 10.53%
print(f'Average death rate AFTER 1847 was {aw_rate:.3}%')    # 2.15%

Reto 2: media móvil de 6 meses antes del lavado. Pista: pon las fechas como índice para que la columna de fechas no se pierda.

Python
roll_df = before_washing.set_index('date')
roll_df = roll_df.rolling(window=6).mean()

Reto 3: un gráfico con 3 líneas (tasa antes, tasa después y media móvil), con leyenda: antes como línea negra delgada discontinua; la media móvil más gruesa y carmesí; después, skyblue con marcadores redondos.

Python
plt.figure(figsize=(14, 8), dpi=200)
plt.title('Percentage of Monthly Deaths over Time', fontsize=18)
plt.ylabel('Percentage of Deaths', color='crimson', fontsize=18)

ax = plt.gca()
ax.xaxis.set_major_locator(years)
ax.xaxis.set_major_formatter(years_fmt)
ax.xaxis.set_minor_locator(months)
ax.set_xlim([df_monthly.date.min(), df_monthly.date.max()])
plt.grid(color='grey', linestyle='--')

ma_line, = plt.plot(roll_df.index, roll_df.pct_deaths, color='crimson',
                    linewidth=3, linestyle='--', label='6m Moving Average')
bw_line, = plt.plot(before_washing.date, before_washing.pct_deaths, color='black',
                    linewidth=1, linestyle='--', label='Before Handwashing')
aw_line, = plt.plot(after_washing.date, after_washing.pct_deaths, color='skyblue',
                    linewidth=3, marker='o', label='After Handwashing')

plt.legend(handles=[ma_line, bw_line, aw_line], fontsize=18)
plt.show()

.plot() devuelve más de una cosa (una lista), por eso la coma: ma_line, = ... toma solo el primer elemento.

📖 Explicación conceptual (verificada)

🛠️ Nota técnica: la lección dice que la orden se dio "en junio de 1846", pero el cuaderno usa 1847-06-01 y la lección siguiente habla de "antes de junio de 1847". Los datos confirman 1847: la tasa pasa de 12,2 % en mayo de 1847 a 2,2 % en junio. Históricamente, Semmelweis introdujo el lavado con cloro a mediados de 1847. Además, los mensajes before 1847 y AFTER 1847 deberían decir "antes/después de junio de 1847".

¿Por qué set_index('date')? Sin él, rolling().mean() intenta promediar también la columna de fechas. Verificado en pandas 3: DataError: Cannot aggregate non-numeric type: datetime64[us]. Con la fecha en el índice, solo se promedian las columnas numéricas y las fechas quedan disponibles para el eje x.

¿Por qué la coma en ma_line, =? Verificado: plt.plot(...) devuelve una lista de líneas (una por cada serie dibujada). Con la coma se "desempaca" la única línea. Alternativa más simple: poner label= en cada plot y llamar plt.legend() sin handles.

Una observación de los datos (adaptación): de enero a marzo de 1847, antes del lavado obligatorio, la tasa ya era baja (3,2 %, 1,9 %, 3,6 %), y en abril subió a 18,3 %. Los datos reales son "ruidosos": por eso no basta con mirar un par de meses, y hacen falta las herramientas de la sección 4.

⚠️ Errores frecuentes

  • Comparar fechas con texto: df_monthly.date < '1847-06-01' funciona en pandas, pero si date no es fecha, compara texto; convierte siempre con pd.to_datetime.
  • Olvidar label=: la leyenda aparece vacía.
  • Usar <= en ambos subconjuntos: junio de 1847 quedaría en los dos grupos.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: cambia los textos del gráfico al español y el eje y a porcentaje (PercentFormatter de matplotlib.ticker).
  • 🟡 Reto 2: agrega una línea vertical en la fecha del cambio con plt.axvline(handwashing_start) y una anotación.
  • 🟠 Reto 3: resalta con color de fondo el periodo "después" con ax.axvspan(inicio, fin, alpha=0.2).

4. Distribuciones y significancia estadística

(Fuente: 595-visualising-distributions-and-testing-for-statistical-significance.md)

🎯 Objetivo de aprendizaje

Comparar dos grupos con estadísticas, cajas, histogramas y KDE, y aplicar una prueba t para decidir si la diferencia puede deberse al azar.

📝 Contenido original del curso

Reto 1: tasa mensual promedio antes y después; diferencia; cuántas veces menor.

Python
avg_prob_before = before_washing.pct_deaths.mean() * 100
print(f'Chance of death during childbirth before handwashing: {avg_prob_before:.3}%.')  # 10.5%

avg_prob_after = after_washing.pct_deaths.mean() * 100
print(f'Chance of death during childbirth AFTER handwashing: {avg_prob_after:.3}%.')    # 2.11%

mean_diff = avg_prob_before - avg_prob_after
print(f'Handwashing reduced the monthly proportion of deaths by {mean_diff:.3}%!')      # 8.4%

times = avg_prob_before / avg_prob_after
print(f'This is a {times:.2}x improvement!')                                           # 5.0x

Reto 2: con np.where() (se abre en otra pestaña), una columna que indique antes o después, y un diagrama de caja de plotly. ¿Cómo cambiaron la media, el máximo, el mínimo y los cuartiles?

Python
df_monthly['washing_hands'] = np.where(df_monthly.date < handwashing_start, 'No', 'Yes')

box = px.box(df_monthly, x='washing_hands', y='pct_deaths', color='washing_hands',
             title='How Have the Stats Changed with Handwashing?')
box.update_layout(xaxis_title='Washing Hands?', yaxis_title='Percentage of Monthly Deaths')
box.show()

Se ve el pico de finales de 1842: más del 30 % de las mujeres que dieron a luz ese mes murieron. No solo bajó el promedio: también el rango (el máximo y el tercer cuartil).

Reto 3: histogramas superpuestos con plotly: color para dos grupos, histnorm='percent' (los periodos tienen distinta longitud), transparencia, distintos nbins y la caja arriba con marginal.

Python
hist = px.histogram(df_monthly, x='pct_deaths', color='washing_hands',
                    nbins=30, opacity=0.6, barmode='overlay',
                    histnorm='percent', marginal='box')
hist.update_layout(xaxis_title='Proportion of Monthly Deaths', yaxis_title='Count')
hist.show()

Con unos 98 datos el histograma es irregular. Una KDE estima cómo sería la distribución suavizada.

Reto 4: dos KDE con sns.kdeplot() (se abre en otra pestaña), con fill. ¿Qué debilidad tiene con los parámetros por defecto? Arréglala con clip.

Python
plt.figure(dpi=200)
sns.kdeplot(before_washing.pct_deaths, fill=True, clip=(0, 1))
sns.kdeplot(after_washing.pct_deaths, fill=True, clip=(0, 1))
plt.title('Est. Distribution of Monthly Death Rate Before and After Handwashing')
plt.xlim(0, 0.40)
plt.show()

Sin clip, la estimación incluye tasas de muerte negativas: "los médicos se sorprenderían si un cadáver volviera a la vida después de una autopsia".

Reto 5: una prueba t para saber si la diferencia de medias es estadísticamente significativa. "Si el valor p es menor que 1 %, podemos estar 99 % seguros de que el lavado de manos marcó una diferencia".

Python
import scipy.stats as stats

t_stat, p_value = stats.ttest_ind(a=before_washing.pct_deaths,
                                  b=after_washing.pct_deaths)
print(f'p-palue is {p_value:.10f}')     # 0.0000002985
print(f't-statstic is {t_stat:.4}')     # 5.512

El valor p es 0,0000002985, muy por debajo del 1 %: la diferencia es altamente significativa y "podemos publicar nuestro artículo".

📖 Resultados verificados

Estadística de pct_deaths Sin lavado (76 meses) Con lavado (22 meses)
Media 0,1051 (10,5 %) 0,0211 (2,1 %)
Mediana 0,1052 0,0206
Desviación estándar 0,0707 0,0152
Tercer cuartil 0,150 0,030
Máximo 0,314 (diciembre de 1842) 0,049

Dos formas de promediar (adaptación): el 10,53 % de la sección 3 divide el total de muertes entre el total de nacimientos (cada parto pesa igual); el 10,5 % de aquí promedia las tasas mensuales (cada mes pesa igual). Son parecidos porque los meses tienen un número similar de partos; con grupos muy desiguales podrían diferir bastante.

🛠️ Nota técnica: puntos porcentuales. Pasar de 10,5 % a 2,1 % es una reducción de 8,4 puntos porcentuales, no "de 8,4 %". En términos relativos, la tasa bajó un 80 % (es 5 veces menor). Confundir "puntos" y "por ciento" es de los errores más comunes en noticias y presentaciones.

🛠️ Nota técnica: con histnorm='percent', el eje y muestra porcentajes, no conteos: la etiqueta 'Count' del curso es incorrecta; debería decir "Porcentaje de meses". Y en la caja, la línea central es la mediana (el reto habla de "la media").

🛠️ Nota técnica (verificado): ¿varianzas iguales? ttest_ind supone por defecto que los dos grupos tienen la misma dispersión (equal_var=True). Aquí la desviación antes (0,071) es casi 5 veces la de después (0,015). La versión recomendada en ese caso es la prueba t de Welch:

Python
stats.ttest_ind(before_washing.pct_deaths, after_washing.pct_deaths, equal_var=False)
# estadístico t ≈ 9.61, p ≈ 1.4e-15

La conclusión no cambia (el valor p es aún menor), pero la elección de la prueba debe justificarse.

⚠️ Errores frecuentes

  • KDE sin clip para una proporción: aparecen valores imposibles (negativos o mayores que 1). Verificado: sin clip, el eje llega a −0,11.
  • Histogramas de grupos de distinto tamaño sin normalizar: el grupo grande "tapa" al pequeño.
  • barmode='overlay' sin opacity: un histograma oculta al otro.
  • Interpretar el valor p como "probabilidad de que la hipótesis sea cierta" (sección 5).

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: cambia nbins a 10, 20 y 50. ¿Cuál comunica mejor? ¿Por qué?
  • 🟡 Reto 2: repite la prueba con equal_var=False y con la prueba no paramétrica de Mann-Whitney (stats.mannwhitneyu). (Verificado: p ≈ 1,3 × 10⁻⁸.) ¿Cambia la conclusión?
  • 🟠 Reto 3: calcula un intervalo de confianza del 95 % para la diferencia de medias (pista: el resultado de ttest_ind tiene el método .confidence_interval()).

5. 📖 ¿Qué es un valor p?

(Sección creada para esta adaptación.)

La pregunta: si el lavado de manos no hubiera servido para nada, ¿qué tan raro sería ver, solo por azar, una diferencia tan grande como la observada (8,4 puntos)?

Un experimento con código (prueba de permutación): si las etiquetas "antes" y "después" no importaran, podríamos barajarlas al azar y la diferencia seguiría siendo parecida. Hagámoslo 10 000 veces:

Python
rng = np.random.default_rng(1)
antes = before_washing.pct_deaths.to_numpy()
despues = after_washing.pct_deaths.to_numpy()
observada = antes.mean() - despues.mean()          # 0.084

todos = np.concatenate([antes, despues])
veces_igual_o_mayor = 0
for _ in range(10_000):
    rng.shuffle(todos)                             # etiquetas al azar
    diferencia = todos[:76].mean() - todos[76:].mean()
    if diferencia >= observada:
        veces_igual_o_mayor += 1

print(veces_igual_o_mayor / 10_000)                # 0.0 (verificado)

En ninguna de las 10 000 barajadas el azar produjo una diferencia tan grande. Eso es lo que expresa un valor p muy pequeño.

Cómo leerlo bien:

✅ Correcto ❌ Incorrecto
"Si el lavado no tuviera efecto, una diferencia así sería extremadamente improbable" "Hay un 99,99997 % de probabilidad de que el lavado funcione"
"La diferencia es estadísticamente significativa al 1 %" "El efecto es grande" (eso lo dice la diferencia, no el valor p)
"Los datos son coherentes con que el lavado redujo las muertes" "Queda demostrado que el lavado causó toda la reducción"

Límites del análisis: es un estudio de "antes y después", no un experimento con grupo de control. Otras cosas pudieron cambiar al mismo tiempo (las estaciones, el personal, la cantidad de autopsias). Lo que hace convincente el caso de Semmelweis es que todo encaja: la diferencia entre clínicas, el mecanismo (las autopsias), el momento del cambio y la magnitud del efecto.


6. 📖 El final de la historia: comunicar con datos

(Fuente: 596-learning-points-y-summary.md)

📝 Contenido original del curso

Resumen del día: histogramas para ver distribuciones; superponer histogramas de series de distinta longitud; suavizar con una KDE y mejorarla con límites; valores p con SciPy; resaltar partes de una serie de tiempo; leyendas; np.where().

La trágica historia: a pesar de la evidencia, la comunidad médica rechazó la teoría de Semmelweis. En parte porque no fue diplomático: hacía ver que los médicos causaban la fiebre (lo que era cierto), y nadie quería oírlo. Pero también porque publicó sus datos en largas tablas sin visualizaciones, que hacían muy difícil ver lo que pasaba, y porque los argumentos estadísticos eran poco comunes en medicina. Su campaña beligerante le ganó enemigos poderosos; perdió su puesto y los médicos dejaron de lavarse las manos con cloro. Fue internado en un asilo a los 47 años, donde probablemente fue golpeado, y murió de sepsis, una infección de la sangre parecida a la que él luchó por prevenir. Solo unos 20 años después, con la teoría de los gérmenes de Louis Pasteur, su trabajo fue aceptado.

🛠️ Nota técnica: el texto dice que fue internado "en 1965"; es 1865 (nació en 1818 y murió a los 47 años).

📖 Reflexión (adaptación)

Semmelweis tenía razón y tenía los datos, pero no logró convencer. Lecciones para cualquier proyecto de datos:

  1. Un buen gráfico vale más que una tabla larga. El gráfico de la sección 3 muestra en un segundo lo que sus tablas ocultaban.
  2. La forma de comunicar importa. Acusar genera resistencia; mostrar evidencia y proponer soluciones genera aliados.
  3. Hace falta un mecanismo. Sin la teoría de los gérmenes, "partículas de cadáver" sonaba a especulación. Un resultado estadístico convence más cuando se explica por qué ocurre.
  4. La evidencia no siempre gana sola. Por eso los científicos también necesitan saber escribir, graficar y dialogar.

7. 🚀 Proyecto del día: el informe que Semmelweis necesitaba

(Integra las lecciones 592 a 596.)

Problema

Viaja en el tiempo: prepara el informe que habría convencido a la junta del hospital en 1848.

Contexto

La junta no sabe estadística y desconfía de Semmelweis. Tu informe debe ser claro, visual y respetuoso.

Objetivo

Un cuaderno con el análisis completo y un informe de una página con 2 gráficos y un párrafo de conclusiones.

Requisitos funcionales

  1. Exploración de ambas tablas, aclarando qué clínica cubren los datos mensuales.
  2. Tasas por clínica (tabla y gráfico).
  3. Gráfico antes/después con media móvil, línea vertical en junio de 1847 y leyenda.
  4. Caja, histograma normalizado y KDE con clip.
  5. Prueba t (clásica y de Welch) con su interpretación correcta.
  6. Conclusión que diferencie puntos porcentuales de porcentaje relativo.

Restricciones técnicas

  • Todos los textos de los gráficos en español.
  • Fechas definidas una sola vez (handwashing_start) y reutilizadas.

Planificación

  1. Comprender la historia. 2. Entradas: dos CSV. 3. Procesos: explorar → tasas → dividir → graficar → probar. 4. Salidas: gráficos, estadísticas, informe. 5. Una sección por lección. 6. Bocetar los 2 gráficos del informe. 7. Implementar. 8. Probar. 9. Corregir. 10. Redactar para la junta.

Construcción paso a paso

  • Versión 1: exploración y tasa global.
  • Versión 2: clínicas.
  • Versión 3: antes y después con el gráfico resaltado.
  • Versión 4: distribuciones y prueba t.
  • Versión final: informe de una página.

Pruebas

Comprobación Valor esperado (verificado)
Formas (12, 4) y (98, 3)
Tasa global 1841–1846 7,08 %
Tasas clínica 1 / clínica 2 9,92 % / 3,88 %
Meses antes / después 76 / 22
Tasa total antes / después 10,53 % / 2,15 %
Media mensual antes / después 10,5 % / 2,11 %
Valor p (clásica / Welch) 2,985 × 10⁻⁷ / 1,4 × 10⁻¹⁵

🧪 Prueba y depuración

Python
df_monthly = pd.read_csv('monthly_deaths.csv')
handwashing_start = '1846-06-01'
before = df_monthly[df_monthly.date <= handwashing_start]
after = df_monthly[df_monthly.date >= handwashing_start]
roll = before.rolling(window=6).mean()
print(f'Reducción: {(before.pct_deaths.mean() - after.pct_deaths.mean()) * 100:.3}%')

Encuentra seis problemas: la fecha no se convirtió (parse_dates), así que se comparan textos; la fecha del cambio es de 1846 (es junio de 1847); <= y >= ponen el mes del cambio en ambos grupos; rolling sin set_index('date') falla con la columna de fechas (DataError); pct_deaths no se ha creado; y la diferencia debería expresarse en puntos porcentuales.

⭐ Extensión del proyecto

  • Reto 1: recrea el gráfico de Semmelweis como lo haría un periódico: título que diga la conclusión ("Las muertes cayeron 5 veces tras el lavado de manos"), anotaciones y fuente.
  • Reto 2: investiga qué es la "sepsis materna" hoy y cuál es la tasa de mortalidad materna en Colombia (fuente: DANE o Instituto Nacional de Salud). Compárala con la de Viena en 1840.

8. 🌟 Proyecto extra: ¿funcionó la campaña del colegio?

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

El comité ambiental lanzó una campaña de reciclaje. ¿Aumentó de verdad el material reciclado, o fue casualidad?

Requisitos

  1. Registren (con el comité) los kilos de material reciclado por semana durante al menos 8 semanas antes y 8 después de la campaña. Si el colegio ya tiene registros, úsenlos.
  2. Grafiquen la serie con el "antes" y el "después" resaltados, una línea vertical en el inicio de la campaña y leyenda.
  3. Comparen las distribuciones con cajas e histogramas normalizados.
  4. Apliquen la prueba t de Welch y la de permutación de la sección 5.
  5. Escriban una conclusión para la comunidad: cuánto cambió (en kilos y en porcentaje), si la diferencia es significativa y qué otras explicaciones podría haber (vacaciones, eventos, clima).

Decisiones que debes tomar (y justificar)

  • ¿Qué haces con semanas atípicas (una semana sin clases)?
  • Si el valor p es 0,08, ¿qué le dirías al comité?

Casos de prueba

  • Ninguna semana está en los dos grupos.
  • El informe distingue "diferencia significativa" de "diferencia importante".

9. 🏗️ Laboratorio de Datos Abiertos — Etapa 5

(Proyecto integrador creado para esta adaptación; etapas anteriores en los Días 76 a 79.)

Etapa 5 (hoy):

  1. Formula una hipótesis que compare dos grupos de tus datos (por ejemplo, zona urbana contra rural, antes y después de un año, dos regiones).
  2. Crea la columna de grupo con np.where.
  3. Compara con cajas, histogramas normalizados (histnorm='percent') y KDE con clip si la variable tiene límites.
  4. Aplica la prueba t de Welch y escribe la interpretación siguiendo la tabla de la sección 5.
  5. Anota al menos dos explicaciones alternativas que tus datos no pueden descartar.

Producto de la etapa: sección 6 de laboratorio.ipynb.


10. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué comparar tasas y no conteos de muertes entre clínicas?
  2. ¿Qué diferencia hay entre una reducción de 8,4 puntos porcentuales y una del 8,4 %?
  3. ¿Para qué sirve histnorm='percent'?
  4. ¿Por qué hace falta clip en la KDE de una proporción?
  5. Explica con tus palabras qué significa un valor p de 0,0000003.

Análisis de código

Python
df_monthly['washing_hands'] = np.where(df_monthly.date < handwashing_start, 'No', 'Yes')
df_monthly.groupby('washing_hands').pct_deaths.agg(['mean', 'median', 'max'])

¿Qué tabla produce? ¿Cuántas filas y columnas?

Predicción

Si en un grupo la tasa pasa de 20 % a 15 %, ¿cuántos puntos porcentuales bajó? ¿Qué porcentaje relativo bajó?

Depuración

"Mi media móvil da DataError: Cannot aggregate non-numeric type". ¿Qué pasó y cómo se arregla?

Modificación

Cambia el gráfico de la sección 3 para que la media móvil sea de 3 meses y también se calcule para el periodo "después".

Creación

Escribe comparar_grupos(serie_a, serie_b) que imprima las medias, la diferencia en puntos, la razón entre medias y el valor p de Welch.


11. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Interpretar un valor p. Pregunta a una IA qué significa un valor p de 0,0000003 en este análisis. Si responde "99,99997 % de probabilidad de que el lavado funcione", explícale el error con la sección 5 y compara con una fuente confiable de estadística.

Actividad 2 — Elegir la prueba. Pide a una IA la prueba t para estos datos. ¿Menciona el supuesto de varianzas iguales? Consulta en la documentación de scipy.stats.ttest_ind qué hace equal_var=False.

Actividad 3 — Verificar la historia. Pregunta a una IA en qué año Semmelweis introdujo el lavado con cloro y en qué año murió. Contrasta con dos fuentes confiables y con los datos (el cambio de junio de 1847).


12. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
592 Datos del "total" de nacimientos y muertes, de 1841 a 1849 Los mensuales son de la clínica 1 (enero de 1841 – marzo de 1849, sin diciembre de 1841); los anuales, de 1841 a 1846 (verificado) Nota técnica de la sección 1
594 Lavado obligatorio desde "junio de 1846" El cuaderno y los datos indican junio de 1847 (verificado) Nota técnica de la sección 3
594 Mensajes "before 1847" / "AFTER 1847" Antes/después de junio de 1847 Nota técnica de la sección 3
595 "Reducción del 8,4 %" Son 8,4 puntos porcentuales; en relativo, 80 % Nota técnica de la sección 4
595 "Si p < 1 %, estamos 99 % seguros de que el lavado marcó la diferencia" Interpretación incorrecta del valor p Sección 5
595 ttest_ind con varianzas iguales Dispersiones muy distintas; Welch recomendado (p ≈ 1,4 × 10⁻¹⁵, verificado) Nota técnica de la sección 4
595 Eje y "Count" con histnorm='percent' Es porcentaje Nota técnica de la sección 4
595 La caja muestra "la media" Muestra la mediana Nota técnica de la sección 4
593 "Tasa mensual más alta" por clínica Los datos por clínica son anuales Nota técnica de la sección 2
596 Internado "en 1965" 1865 Nota técnica de la sección 6
595 Enlace docs.scipy.org/%5Ddoc/...; "p-palue", "t-statstic" Erratas —

13. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Historia de Semmelweis, fiebre puerperal, qué se aprende, cuaderno y CSV 591 Sección 0 Preparar el cuaderno — Sección 7
Exploración, NaN, duplicados, describe, tasa de la década, dos ejes con localizadores 592 Sección 1 Retos 1–3 Tasa anual; verificar la clínica Sección 7
Clínicas de médicos y parteras, líneas de plotly, pct_deaths, tasas por clínica, hipótesis de Semmelweis 593 Sección 2 Retos 1 y 2 Razón de riesgo; explicaciones alternativas Secciones 7 y 9
Cloro, antes/después, tasas, media móvil con set_index, 3 líneas, leyenda con handles 594 Sección 3 Retos 1–3 axvline, axvspan, porcentajes Secciones 7, 8 y 9
Diferencia de medias, np.where, caja, histograma con histnorm y marginal, KDE con clip, ttest_ind 595 Secciones 4 y 5 Retos 1–5 Welch, Mann-Whitney, intervalo de confianza, permutación Secciones 7, 8 y 9
Resumen del día; final trágico de Semmelweis; tablas sin gráficos 596 Sección 6 — Comunicar con datos Sección 7
(No existe en el curso original) — Sección 5 — Valor p con permutaciones Secciones 7, 8 y 9
(No existe en el curso original) — — — Campaña de reciclaje Sección 8
(No existe en el curso original) — — — Laboratorio de Datos Abiertos, etapa 5 Sección 9

14. 📦 Evidencias de aprendizaje del Día 80

  • El cuaderno del análisis con los gráficos y la tabla de pruebas superada.
  • El informe de una página para la junta del hospital.
  • El análisis de la campaña de reciclaje (proyecto extra).
  • La etapa 5 del Laboratorio de Datos Abiertos.
  • Una reflexión escrita: ¿qué habrías hecho distinto a Semmelweis para convencer a sus colegas?
  • Cuaderno de errores: fechas sin convertir, meses en ambos grupos, rolling con columna de fechas, ma_line sin coma, KDE sin clip, histogramas sin normalizar, puntos contra porcentaje, interpretación del valor p, prueba t con varianzas distintas.