Curso: Herramientas Estadísticas y Computacionales para el Análisis de Datos en Investigación Bioquímica
Sesión 1: Introducción a Python y manejo de datos experimentales
Docente: Andrés García Medina
Contacto: andgarm.n@gmail.com
Al terminar esta demostración podrás:
pandas;Los datos son sintéticos y se diseñaron exclusivamente con fines docentes.
datos_actividad_enzimatica_limpios.csv y
datos_actividad_enzimatica_limpios.xlsx.Shift + Enter.import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
Python: 3.12.13 pandas: 2.2.2
Una variable en Python es un nombre que apunta a un objeto. En esta sesión usaremos números, texto, listas y diccionarios antes de trabajar con tablas.
ph = 7
temperatura_c = 37
tratamiento = "pH 7 a 37 °C"
replicas = [1, 2, 3]
condicion = {"ph": ph, "temperatura_c": temperatura_c}
print(tratamiento)
print("Número de réplicas biológicas:", len(replicas))
print("Condición:", condicion)
pH 7 a 37 °C
Número de réplicas biológicas: 3
Condición: {'ph': 7, 'temperatura_c': 37}
pandas representa una tabla como un DataFrame. Cada fila es una observación
y cada columna es una variable.
datos_csv = pd.read_csv("datos_actividad_enzimatica_limpios.csv")
datos_excel = pd.read_excel("datos_actividad_enzimatica_limpios.xlsx",
sheet_name="actividad_enzimatica")
print("CSV:", datos_csv.shape)
print("Excel:", datos_excel.shape)
datos_excel.shape
CSV: (90, 13) Excel: (90, 13)
(90, 13)
print("¿Coinciden las dimensiones?", datos_csv.shape == datos_excel.shape)
¿Coinciden las dimensiones? True
datos_csv.columns
Index(['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'],
dtype='object')
actividad_u_ml(U/mL): actividad enzimática estimada por mililitro de muestra. En este ejercicio se calcula a partir del cambio de absorbancia por minuto y un factor de conversión definido por el método experimental.
Las cuatro preguntas iniciales son:
print("Dimensiones:", datos_csv.shape)
print("\nColumnas:")
print(datos_csv.columns.tolist())
print("\nTipos:")
print(datos_csv.dtypes)
print("\nValores faltantes:")
print(datos_csv.isna().sum())
print("\nIdentificadores duplicados:", datos_csv["muestra_id"].duplicated().sum())
Dimensiones: (90, 13) Columnas: ['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'] Tipos: muestra_id object fecha object lote_enzima object operador object ph int64 temperatura_c int64 replica_biologica int64 replica_tecnica int64 tiempo_reaccion_min float64 absorbancia_inicial_405 float64 absorbancia_final_405 float64 actividad_u_ml float64 observacion float64 dtype: object Valores faltantes: muestra_id 0 fecha 0 lote_enzima 0 operador 0 ph 0 temperatura_c 0 replica_biologica 0 replica_tecnica 0 tiempo_reaccion_min 0 absorbancia_inicial_405 0 absorbancia_final_405 0 actividad_u_ml 0 observacion 90 dtype: int64 Identificadores duplicados: 0
En esta sección transformaremos la tabla completa en un resumen útil para responder una pregunta experimental:
A 37 °C, ¿cómo cambia la actividad enzimática media entre los distintos valores de pH?
El procedimiento se divide en cuatro acciones:
columnas_clave = [
"muestra_id",
"ph",
"temperatura_c",
"replica_biologica",
"replica_tecnica",
"actividad_u_ml"
]
vista = datos_csv[columnas_clave]
vista.head()
| muestra_id | ph | temperatura_c | replica_biologica | replica_tecnica | actividad_u_ml | |
|---|---|---|---|---|---|---|
| 0 | AE-PH5-T25-B1-R1 | 5 | 25 | 1 | 1 | 0.4025 |
| 1 | AE-PH5-T25-B1-R2 | 5 | 25 | 1 | 2 | 0.4243 |
| 2 | AE-PH5-T25-B2-R1 | 5 | 25 | 2 | 1 | 0.4232 |
| 3 | AE-PH5-T25-B2-R2 | 5 | 25 | 2 | 2 | 0.4222 |
| 4 | AE-PH5-T25-B3-R1 | 5 | 25 | 3 | 1 | 0.4824 |
condicion_37 = vista[vista["temperatura_c"] == 37]
condicion_37.head()
| muestra_id | ph | temperatura_c | replica_biologica | replica_tecnica | actividad_u_ml | |
|---|---|---|---|---|---|---|
| 6 | AE-PH5-T37-B1-R1 | 5 | 37 | 1 | 1 | 0.4731 |
| 7 | AE-PH5-T37-B1-R2 | 5 | 37 | 1 | 2 | 0.4955 |
| 8 | AE-PH5-T37-B2-R1 | 5 | 37 | 2 | 1 | 0.4508 |
| 9 | AE-PH5-T37-B2-R2 | 5 | 37 | 2 | 2 | 0.4233 |
| 10 | AE-PH5-T37-B3-R1 | 5 | 37 | 3 | 1 | 0.5618 |
El filtro mantiene únicamente las muestras analizadas a 37 °C. Así, la temperatura permanece constante y podemos concentrarnos en comparar el efecto del pH.
Primero contamos las observaciones disponibles para cada pH:
condicion_37["ph"].value_counts().sort_index()
| count | |
|---|---|
| ph | |
| 5 | 6 |
| 6 | 6 |
| 7 | 6 |
| 8 | 6 |
| 9 | 6 |
Después revisamos cuántas réplicas técnicas existen para cada combinación de pH y réplica biológica:
verificacion_replicas = (
condicion_37
.groupby(["ph", "replica_biologica"], as_index=False)
.agg(
replicas_tecnicas=("replica_tecnica", "nunique"),
observaciones=("muestra_id", "size")
)
)
verificacion_replicas
| ph | replica_biologica | replicas_tecnicas | observaciones | |
|---|---|---|---|---|
| 0 | 5 | 1 | 2 | 2 |
| 1 | 5 | 2 | 2 | 2 |
| 2 | 5 | 3 | 2 | 2 |
| 3 | 6 | 1 | 2 | 2 |
| 4 | 6 | 2 | 2 | 2 |
| 5 | 6 | 3 | 2 | 2 |
| 6 | 7 | 1 | 2 | 2 |
| 7 | 7 | 2 | 2 | 2 |
| 8 | 7 | 3 | 2 | 2 |
| 9 | 8 | 1 | 2 | 2 |
| 10 | 8 | 2 | 2 | 2 |
| 11 | 8 | 3 | 2 | 2 |
| 12 | 9 | 1 | 2 | 2 |
| 13 | 9 | 2 | 2 | 2 |
| 14 | 9 | 3 | 2 | 2 |
También comprobamos si existen datos faltantes o identificadores duplicados:
print("Datos faltantes:")
print(condicion_37.isna().sum())
print("\nIdentificadores duplicados:")
print(condicion_37["muestra_id"].duplicated().sum())
Datos faltantes: muestra_id 0 ph 0 temperatura_c 0 replica_biologica 0 replica_tecnica 0 actividad_u_ml 0 dtype: int64 Identificadores duplicados: 0
Resumir la actividad por tratamiento
resumen_control = (
condicion_37
.groupby("ph", as_index=False)
.agg(
n=("actividad_u_ml", "size"),
actividad_media=("actividad_u_ml", "mean")
)
)
resumen_control
| ph | n | actividad_media | |
|---|---|---|---|
| 0 | 5 | 6 | 0.493700 |
| 1 | 6 | 6 | 1.178717 |
| 2 | 7 | 6 | 1.584667 |
| 3 | 8 | 6 | 1.143617 |
| 4 | 9 | 6 | 0.500167 |
Al seleccionar, filtrar y verificar los datos, confirmamos que cada tratamiento conserva sus muestras y réplicas. Posteriormente, resumimos la actividad enzimática por pH bajo una temperatura constante de 37 °C.
La gráfica sirve como control de calidad y como panorama del patrón, nunca debe pensarse como un reemplazo del análisis estadístico formal (que se desarrollará en sesiones posteriores.)
sns.set_theme(style="whitegrid")
fig, ax = plt.subplots(figsize=(8, 4.5))
sns.boxplot(
data=datos_csv,
x="ph",
y="actividad_u_ml",
hue="temperatura_c",
ax=ax,
)
ax.set(
title="Actividad enzimática sintética por pH y temperatura",
xlabel="pH",
ylabel="Actividad (U/mL)",
)
ax.legend(title="Temperatura (°C)", bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.show()
datos_csv
| 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 | 25 | 1 | 1 | 2.0 | 0.0496 | 0.1301 | 0.4025 | NaN |
| 1 | AE-PH5-T25-B1-R2 | 2026-07-20 | L01 | Ana | 5 | 25 | 1 | 2 | 2.0 | 0.0482 | 0.1331 | 0.4243 | NaN |
| 2 | AE-PH5-T25-B2-R1 | 2026-07-21 | L02 | Luis | 5 | 25 | 2 | 1 | 2.0 | 0.0398 | 0.1244 | 0.4232 | NaN |
| 3 | AE-PH5-T25-B2-R2 | 2026-07-21 | L02 | Luis | 5 | 25 | 2 | 2 | 2.0 | 0.0361 | 0.1205 | 0.4222 | NaN |
| 4 | AE-PH5-T25-B3-R1 | 2026-07-22 | L03 | Ana | 5 | 25 | 3 | 1 | 2.0 | 0.0427 | 0.1392 | 0.4824 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 85 | AE-PH9-T45-B1-R2 | 2026-07-20 | L01 | Ana | 9 | 45 | 1 | 2 | 2.0 | 0.0522 | 0.1235 | 0.3566 | NaN |
| 86 | AE-PH9-T45-B2-R1 | 2026-07-21 | L02 | Luis | 9 | 45 | 2 | 1 | 2.0 | 0.0434 | 0.1293 | 0.4295 | NaN |
| 87 | AE-PH9-T45-B2-R2 | 2026-07-21 | L02 | Luis | 9 | 45 | 2 | 2 | 2.0 | 0.0495 | 0.1398 | 0.4517 | NaN |
| 88 | AE-PH9-T45-B3-R1 | 2026-07-22 | L03 | Ana | 9 | 45 | 3 | 1 | 2.0 | 0.0483 | 0.1405 | 0.4608 | NaN |
| 89 | AE-PH9-T45-B3-R2 | 2026-07-22 | L03 | Ana | 9 | 45 | 3 | 2 | 2.0 | 0.0477 | 0.1388 | 0.4556 | NaN |
90 rows × 13 columns
import matplotlib.pyplot as plt
import seaborn as sns
# Gráfico de barras de la actividad enzimática media por pH
sns.set_theme(style="whitegrid")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
sns.barplot(
data=resumen_control,
x="ph",
y="actividad_media",
ax=ax1,
palette="viridis"
)
ax1.set(title="Actividad Enzimática",
xlabel="pH",
ylabel="Actividad Media (U/mL)")
# Histograma de la actividad enzimática
sns.histplot(
data=datos_csv,
x="actividad_u_ml",
ax=ax2,
color="red"
)
ax2.set(title="Distribución de la Actividad Enzimática",
xlabel="Actividad (U/mL)",
ylabel="Frecuencia")
plt.tight_layout()
plt.show()
/tmp/ipykernel_2579/3646558244.py:8: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. sns.barplot(
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 8))
plt.pie(resumen_control['actividad_media'], labels=resumen_control['ph'], autopct='%1.1f%%', startangle=90, colors=plt.cm.viridis.colors)
plt.title('Proporción de Actividad Enzimática Media por pH')
plt.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.show()
En Colab el archivo aparecerá en el panel Archivos. Cada exportación debe tener un nombre claro y no sobrescribir los datos crudos.
resumen_control.to_csv("resumen_actividad_37C.csv")
¿Qué error de organización en tus propios datos podría cambiar una conclusión científica?