Demostración: estadística descriptiva y visualización científica¶

Curso: Herramientas Estadísticas y Computacionales para el Análisis de Datos en Investigación Bioquímica
Sesión 2: Estadística descriptiva y visualización de datos científicos
Docente: Andrés García Medina
Contacto: andgarm.n@gmail.com

Los datos son sintéticos y se diseñaron exclusivamente con fines docentes.

Propósito¶

Al terminar esta demostración podrás:

  1. definir la unidad de análisis;
  2. calcular media, mediana, moda, varianza, desviación estándar, error estándar y CV;
  3. construir resúmenes por grupos con groupby y agg;
  4. crear histogramas, diagramas de caja, dispersión y barras con error;

Antes de ejecutar en Google Colab¶

  1. Descarga actividad_enzimatica_limpia_equipo.csv de classroom.
  2. Abre un notebook nuevo.
  3. Arrastra el archivo de datos a la barra lateral izquierda al notebook
  4. Copia y pega cada una de estas celdas.
  5. Ejecuta las celdas en orden con Shift + Enter.

El archivo proviene de la limpieza realizada en la sesión 1.

Pregunta experimental guía¶

¿Cómo cambian el centro y la variabilidad de la actividad enzimática entre niveles de pH y temperatura?

In [ ]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
In [ ]:
datos = pd.read_csv("actividad_enzimatica_limpia_equipo.csv", encoding="utf-8-sig")
datos.head()
Out[ ]:
muestra_id fecha lote_enzima operador ph temperatura_c replica_biologica replica_tecnica tiempo_reaccion_min absorbancia_inicial_405 absorbancia_final_405 actividad_u_ml observacion
0 AE-PH5-T25-B1-R1 2026-07-20 L01 Ana 5.0 25 1 1 2.0 0.0496 0.1301 0.4025 NaN
1 AE-PH5-T25-B1-R2 2026-07-20 L01 Ana 5.0 25 1 2 2.0 0.0482 0.1331 0.4245 NaN
2 AE-PH5-T25-B2-R1 2026-07-21 L02 Luis 5.0 25 2 1 2.0 0.0398 0.1244 0.4230 NaN
3 AE-PH5-T25-B2-R2 2026-07-21 L02 Luis 5.0 25 2 2 2.0 0.0361 0.1205 0.4220 NaN
4 AE-PH5-T25-B3-R1 2026-07-22 L03 Ana 5.0 25 3 1 2.0 0.0427 0.1392 0.4825 NaN

Cada combinación de pH y temperatura contiene tres réplicas biológicas. Cada réplica biológica tiene dos lecturas técnicas. Para describir variabilidad biológica, primero promediamos las dos lecturas técnicas dentro de cada preparación independiente.

In [ ]:
# Nivel 1: lecturas técnicas → unidades biológicas
claves_biologicas = ["ph", "temperatura_c", "replica_biologica"]

biologicas = (
    datos
    .groupby(claves_biologicas, as_index=False)
    .agg(
        actividad_u_ml=("actividad_u_ml", "mean"),
        de_tecnica_u_ml=("actividad_u_ml", "std"),
        n_tecnicas=("actividad_u_ml", "size"),
    )
)

print("Lecturas instrumentales:", len(datos))
print("Unidades biológicas:", len(biologicas))
print("Réplicas técnicas por unidad:", biologicas["n_tecnicas"].unique())
biologicas.head()
Lecturas instrumentales: 90
Unidades biológicas: 45
Réplicas técnicas por unidad: [2]
Out[ ]:
ph temperatura_c replica_biologica actividad_u_ml de_tecnica_u_ml n_tecnicas
0 5.0 25 1 0.41350 0.015556 2
1 5.0 25 2 0.42250 0.000707 2
2 5.0 25 3 0.47900 0.004950 2
3 5.0 37 1 0.48425 0.015910 2
4 5.0 37 2 0.43675 0.019445 2

La tabla pasó de 90 lecturas a 45 unidades biológicas: 15 tratamientos por 3 preparaciones independientes. El tamaño de muestra por tratamiento es, por tanto, n = 3, no n = 6.

In [ ]:
# Nivel 2: unidades biológicas → resumen por tratamiento
claves_tratamiento = ["ph", "temperatura_c"]

resumen = (
    biologicas
    .groupby(claves_tratamiento, as_index=False)
    .agg(
        n=("actividad_u_ml", "size"),
        media=("actividad_u_ml", "mean"),
        mediana=("actividad_u_ml", "median"),
        varianza=("actividad_u_ml", "var"),
        de=("actividad_u_ml", "std"),
        ee=("actividad_u_ml", "sem"),
    )
)

resumen["cv_pct"] = 100 * resumen["de"] / resumen["media"]
resumen.round({
    "media": 4,
    "mediana": 4,
    "varianza": 6,
    "de": 4,
    "ee": 4,
    "cv_pct": 2,
})
Out[ ]:
ph temperatura_c n media mediana varianza de ee cv_pct
0 5.0 25 3 0.4383 0.4225 0.001261 0.0355 0.0205 8.10
1 5.0 37 3 0.4935 0.4842 0.003831 0.0619 0.0357 12.54
2 5.0 45 3 0.4245 0.4072 0.001086 0.0330 0.0190 7.76
3 6.0 25 3 0.8981 0.9265 0.005078 0.0713 0.0411 7.93
4 6.0 37 3 1.1787 1.1837 0.000973 0.0312 0.0180 2.65
5 6.0 45 3 0.9907 0.9895 0.000067 0.0082 0.0047 0.83
6 7.0 25 3 1.2840 1.2830 0.000086 0.0093 0.0054 0.72
7 7.0 37 3 1.5847 1.5827 0.000100 0.0100 0.0058 0.63
8 7.0 45 3 1.3283 1.3250 0.000051 0.0071 0.0041 0.54
9 8.0 25 3 0.8658 0.8688 0.000041 0.0064 0.0037 0.74
10 8.0 37 3 1.1436 1.1458 0.000629 0.0251 0.0145 2.19
11 8.0 45 3 1.0042 1.0065 0.004409 0.0664 0.0383 6.61
12 9.0 25 3 0.4253 0.3922 0.003435 0.0586 0.0338 13.78
13 9.0 37 3 0.5000 0.5145 0.000984 0.0314 0.0181 6.27
14 9.0 45 3 0.4184 0.4405 0.002954 0.0544 0.0314 12.99

Podemos hacer un query solo para temperatura_c = 37

In [ ]:
resumen_37 = resumen.query("temperatura_c == 37").copy()
resumen_37.sort_values("ph").round({
    "media": 4,
    "mediana": 4,
    "varianza": 6,
    "de": 4,
    "ee": 4,
    "cv_pct": 2,
})
Out[ ]:
ph temperatura_c n media mediana varianza de ee cv_pct
1 5.0 37 3 0.4935 0.4842 0.003831 0.0619 0.0357 12.54
4 6.0 37 3 1.1787 1.1837 0.000973 0.0312 0.0180 2.65
7 7.0 37 3 1.5847 1.5827 0.000100 0.0100 0.0058 0.63
10 8.0 37 3 1.1436 1.1458 0.000629 0.0251 0.0145 2.19
13 9.0 37 3 0.5000 0.5145 0.000984 0.0314 0.0181 6.27

Histograma: panorama global¶

In [ ]:
fig, ax = plt.subplots(figsize=(8, 4.6))
PALETTE = sns.color_palette("viridis")
sns.histplot(
    data=biologicas,
    x="actividad_u_ml",
    bins="auto",
    color=PALETTE[0],
    edgecolor="white",
    ax=ax,
)
ax.set(
    title="Distribución global de la actividad biológica media",
    xlabel="Actividad enzimática (U/mL)",
    ylabel="Frecuencia",
)
plt.tight_layout()
plt.show()

El histograma mezcla los 15 tratamientos. Sirve para observar la distribución global, pero no representa la forma de una condición específica. Con sólo tres réplicas biológicas por tratamiento, un histograma separado para cada grupo sería poco informativo.

Diagrama de caja: comparar tratamientos¶

In [ ]:
fig, ax = plt.subplots(figsize=(9, 4.8))
sns.boxplot(
    data=biologicas,
    x="ph",
    y="actividad_u_ml",
    hue="temperatura_c",
    palette="viridis",
    ax=ax,
)
ax.set(
    title="Actividad por pH y temperatura",
    xlabel="pH",
    ylabel="Actividad enzimática (U/mL)",
)
ax.legend(title="Temperatura (°C)", bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.show()

La caja resume mediana e intervalo intercuartílico.

Dispersión: mostrar réplicas biológicas¶

In [ ]:
# Seleccionar los datos obtenidos a 37 °C
foco = biologicas[
    biologicas["temperatura_c"] == 37
]

# Crear la gráfica
plt.figure(figsize=(8, 5))

sns.scatterplot(
    data=foco,
    x="ph",
    y="actividad_u_ml",
    hue="replica_biologica",
    palette="viridis",
    s=90,
)

plt.title("Réplicas biológicas a 37 °C")
plt.xlabel("pH")
plt.ylabel("Actividad enzimática (U/mL)")
plt.legend(title="Réplica biológica")
plt.tight_layout()
plt.show()

Barras con error: declarar DE o EE¶

In [ ]:
# Ordenar los resultados por pH
orden = resumen_37.sort_values("ph")

plt.figure(figsize=(8, 5))

# Curva de medias con barras de desviación estándar
plt.errorbar(
    orden["ph"],
    orden["media"],
    yerr=orden["de"],
    marker="o",
    linestyle="-",
    label="Media ± 1 DE",
)

# Mostrar las réplicas biológicas
plt.scatter(
    foco["ph"],
    foco["actividad_u_ml"],
    color="black",
    s=35,
    label="Réplicas biológicas",
)

plt.title("Actividad enzimática a 37 °C según el pH")
plt.xlabel("pH")
plt.ylabel("Actividad enzimática (U/mL)")
plt.xticks(orden["ph"])
plt.legend()
plt.tight_layout()
plt.show()

Estas barras representan media ± 1 desviación estándar. Si se usara EE, el mensaje sería precisión de la media y las barras serían menores. En ambos casos, el pie de figura debe indicar n = 3 réplicas biológicas por pH.

In [ ]:
import matplotlib.pyplot as plt

# Seleccionar y ordenar los resultados obtenidos a 37 °C
orden = (
    resumen_37
    .sort_values("ph")
)

# Crear dos gráficas con la misma escala vertical
fig, ejes = plt.subplots(
    1, 2,
    figsize=(13, 5),
    sharey=True
)

# Panel 1: media ± desviación estándar
ejes[0].bar(
    orden["ph"],
    orden["media"],
    yerr=orden["de"],
    capsize=5,
    color="steelblue",
    alpha=0.85
)

ejes[0].set_title("Media ± 1 DE")
ejes[0].set_xlabel("pH")
ejes[0].set_ylabel("Actividad enzimática (U/mL)")
ejes[0].set_xticks(orden["ph"])

# Panel 2: media ± error estándar
ejes[1].bar(
    orden["ph"],
    orden["media"],
    yerr=orden["ee"],
    capsize=5,
    color="#2a9d9f",
    alpha=0.85
)

ejes[1].set_title("Media ± 1 EE")
ejes[1].set_xlabel("pH")
ejes[1].set_xticks(orden["ph"])

# Título general y nota al pie
fig.suptitle(
    "La barra izquierda se refiere a Desviación Estándar, mientras que la derecha a Error Estándar",
    fontsize=18
)

fig.text(
    0.5,
    0.02,
    "n = 3 réplicas biológicas por pH a 37 °C",
    ha="center",
    fontsize=11
)

plt.tight_layout()
plt.show()