/ Proyectos / Industria / Cemento
¿Se puede saber la resistencia del cemento sin esperar 28 días?
Proyecto real con empresa cementera peruana: aprende a construir un pipeline de ML que predice la resistencia a la compresión desde la composición de la mezcla — y a tomar las decisiones que un ingeniero de planta necesita.
El problema realEl contexto que lo cambia todo
Imagina que eres ingeniero en el área de I+D de una empresa cementera. Tu trabajo diario es diseñar mezclas crudas que cumplan dos objetivos a la vez: que la resistencia a la compresión sea competitiva y que el costo sea el menor posible.
El problema es que para saber si una mezcla funciona, tienes que fabricar probetas y esperar. 1 día. 3 días. 7 días. 28 días. Si el resultado no es el esperado, el lote ya fue producido. El cemento vendido con una característica que no cumple especificaciones puede derivar en reclamos legales, porque el producto tiene que estar bien caracterizado y venderse como tal.
Qué está en juego
- Sin el modelo: diseño de mezcla lento, iterativo y costoso. Cada prueba física cuesta tiempo y material.
- Con el modelo: el ingeniero puede simular proporciones en un Excel y ver la resistencia estimada en segundos, priorizando qué mezclas vale la pena probar físicamente.
- Si el modelo falla: predicciones incorrectas pueden llevar a mezclas fuera de especificación — riesgo estructural y legal.
Para quién es este proyecto
Visión globalMapa del proyecto
Antes de abrir el notebook, necesitas saber a dónde vas. Este proyecto tiene cinco fases. En cada una tomarás decisiones que afectan la siguiente. Si te saltas una, probablemente tendrás que volver.
Entendimiento
del problema
Datos y
variables
Modelado y
decisiones
Evaluación e
interpretación
Aplicación
real
¿Qué construirás al final?
- Un pipeline de regresión completo: desde datos crudos de laboratorio hasta predicción de resistencia en PSI.
- Un benchmarking razonado entre Regresión Lineal, Random Forest, XGBoost y Red Neuronal — con criterio de ROI, no solo de RMSE.
- Un prototipo funcional: Google Sheets como interfaz, FastAPI como backend, ngrok como túnel.
- Un análisis cualitativo + cuantitativo de variables que te enseña a pensar antes de entrenar.
Stack tecnológico
- Python 3.10+ — lenguaje principal
- Scikit-learn — Random Forest, SVR, KNN, PCA, VarianceThreshold, StandardScaler, GridSearchCV
- XGBoost — modelo de mayor ROI en benchmarking
- pandas / numpy / matplotlib — manipulación y visualización
- FastAPI + uvicorn — API REST para predicción
- ngrok — túnel para exponer la API desde Colab
- Google Sheets + Apps Script — interfaz de usuario para planta
- pickle — serialización del modelo entrenado
Antes del códigoFase 1 — Entendimiento del problema
La mayoría de los proyectos de ML fallan antes de escribir la primera línea de código. Fallan porque quien los construye no entiende bien qué quiere predecir, para qué y con qué restricciones. Esta fase te obliga a responder esas preguntas.
¿Qué está hecho el cemento?
Para trabajar con datos de cemento no necesitas ser químico, pero sí necesitas entender el dominio mínimo. El cemento Tipo ICo es una mezcla de componentes con proporciones variables. Los principales son clínker conforme, clínker no conforme, yeso, piedra caliza, arcilla calcinada, escoria y varios óxidos químicos (SiO₂, Al₂O₃, Fe₂O₃, etc.).
La hipótesis central del proyecto — definida junto con el equipo de I+D de la empresa — era que el yeso tiene una influencia directa y medible en la resistencia a la compresión. No cualquier componente: específicamente el yeso. Eso guió desde qué variables mirar primero hasta cómo interpretar los resultados del modelo.
Podrías formular este problema como clasificación: "¿la resistencia es alta, media o baja?" Eso es más simple de entrenar. Pero el negocio necesita el valor exacto en PSI — la diferencia entre 4,200 y 4,500 PSI tiene consecuencias concretas en la formulación y en el costo. La precisión numérica importa, por eso es regresión. Cuando defines el tipo de tarea, siempre pregúntate: ¿qué decisión real depende de esta predicción? La respuesta te dice qué tan granular tiene que ser el output.
Definición formal del problema
- Variable objetivo (y): Resistencia a la compresión en PSI a 28 días de fraguado.
- Variables de entrada (X): Composición química del cemento — proporciones en porcentaje de cada componente.
- Tipo de tarea: Regresión supervisada.
- Métricas válidas: RMSE (penaliza errores grandes, relevante porque subestimar resistencia es peligroso), MAE (error promedio absoluto en PSI), MAPE (error relativo en %).
- Métricas que NO aplican: accuracy, precision, recall, F1 — son métricas de clasificación.
- Nivel de error aceptable: En la literatura industrial, un MAPE de 5–10% sobre valores de resistencia es considerado práctico para aplicaciones de control de calidad. Valores mayores al 15% probablemente no justifican reemplazar las pruebas físicas.
¿Qué predicciones son peligrosas?
No todos los errores son iguales. En este problema, subestimar la resistencia (predecir 3,800 PSI cuando la real es 4,500 PSI) lleva a descartar una mezcla buena — desperdicio de material y tiempo. Pero sobreestimar la resistencia (predecir 4,500 PSI cuando la real es 3,800 PSI) es peor: podría llevar a aprobar una mezcla que no cumple especificaciones, con consecuencias estructurales y legales.
Esto tiene implicaciones en cómo interpretas las métricas: un modelo con RMSE bajo pero sesgado hacia sobreestimar es más peligroso que uno con mayor error pero sin sesgo sistemático.
- Sabes exactamente qué variable vas a predecir y en qué unidades.
- Entiendes por qué es regresión y no clasificación.
- Definiste qué métricas usarás y cuáles son inválidas.
- Sabes qué tipo de error es más costoso en este negocio.
- Tienes una hipótesis sobre qué variable debería importar más.
EDA con pensamiento críticoFase 2 — Datos y variables
El dataset contiene mediciones reales de pruebas de laboratorio: 31 atributos de composición química y 4 columnas target de resistencia a la compresión en PSI (1, 3, 7 y 28 días). En el proyecto se usó el target de 28 días por ser la métrica estándar de calidad en la industria.
Sobre los datos públicos: cuándo puedes usarlos como proxy
Los datos reales de la empresa son confidenciales. Para replicar el proceso, puedes usar dos datasets públicos como proxy:
| Dataset | Registros | Variables | Mejor uso | Limitación |
|---|---|---|---|---|
| UCI Concrete Compressive Strength | 1,030 | 8 entradas + 1 salida | Línea base, comparación de modelos, tesis | Basado en Yeh (1998), solo concreto convencional |
| Mendeley — Concreto Normal | ~500–1,000 | 9 características | Proxy industrial, validación con datos recientes | Fuente geográfica única, menos establecido |
Antes de entrenar, verifica si tienes suficientes datos para el número de variables que usarás. Una heurística útil es el Feature-to-Data Sufficiency Ratio (FDSR):
FDSR = Número de muestras / (k × Número de variables)
donde k = 5–10 dependiendo de cuán conservador quieras ser con el sobreajuste. Si FDSR < 1, tienes un problema de dimensionalidad. En el dataset UCI con 8 variables y 1,030 muestras: FDSR = 1030 / (5×8) = 25.75 — holgado. En el proyecto real con 31 variables y menos muestras, este ratio fue mucho más ajustado, lo que justificó el uso agresivo de selección de características.
Análisis cualitativo antes del cuantitativo
El primer paso no es correr código. Es sentarte con alguien que entiende el dominio — en este caso el equipo de Ingeniería Química e I+D — y clasificar cada variable antes de ver los números:
- Verde — variable altamente útil, se espera que tenga relación directa con la resistencia.
- Ámbar — incierta, puede o no ser relevante, requiere verificación cuantitativa.
- Rojo — irrelevante a priori, o derivada de otras variables (riesgo de leakage).
Este paso te ahorra entrenar modelos con variables que ya sabes que son ruido. También te da una hipótesis que el modelo deberá confirmar o refutar.
Un analista junior hace el EDA como si fuera una lista de tareas pendientes: grafica la distribución ✔, calcula correlaciones ✔, busca nulos ✔. Pero no interpreta los resultados. No se pregunta ¿qué decisión tomo con esto? El EDA sin decisiones es decoración. Cada gráfica debe llevar a una acción concreta: eliminar una variable, transformar otra, investigar un outlier específico.
Carga y limpieza inicial
El dataset es un Excel con 31 columnas de composición química y 4 columnas target. La columna Clínker I Total (%) se elimina porque es combinación lineal de otras columnas — incluirla sería introducir redundancia perfecta. Arcilla Calcinada (%) también se descarta porque todos sus valores son cero: varianza nula, sin información.
import pandas as pd
data = pd.read_excel("data/composito_cemento_tipo_ico.xlsx", header=1)
data = data.dropna()
# Atributos: columnas 2 a 33, excluyendo columnas redundantes
df_atributes = data.iloc[:, 2:34].copy()
del df_atributes["Clínker I Total (%)"] # combinación lineal → leakage
df_atributes.drop(["Arcilla Calcinada (%)"], axis=1, inplace=True) # varianza 0
# Target: resistencia a 28 días
y = data.iloc[:, -5]
print(f"Features: {df_atributes.shape[1]}") # debe ser ~29
print(f"Muestras: {len(y)}")
print(f"\nDistribución del target (PSI):")
print(y.describe())
Se eliminan filas con nulos (dropna()) en vez de imputar. ¿Por qué? Porque estamos ante datos de laboratorio donde cada fila es una prueba física real. Imputar una medición de laboratorio es inventar un resultado que nunca ocurrió. Si tienes muchos nulos, la pregunta correcta es: ¿por qué existen? ¿Error de medición? ¿Variable que a veces no se mide? Eso te dice si puedes imputar o si debes descartar.
Análisis cuantitativo de variables: VarianceThreshold y correlación
from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# Eliminar features con varianza muy baja (casi constantes)
sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
df_filtrado = sel.fit_transform(df_atributes)
# Ver qué variables sobrevivieron
features_selected = df_atributes.columns[sel.get_support()]
features_dropped = df_atributes.columns[~sel.get_support()]
print(f"Features originales: {df_atributes.shape[1]}")
print(f"Features tras VarianceThreshold: {df_filtrado.shape[1]}")
print(f"\nEliminadas por baja varianza: {list(features_dropped)}")
import matplotlib.pyplot as plt
corr_target = df_atributes.corrwith(y).sort_values(ascending=False)
plt.figure(figsize=(10, 6))
corr_target.plot.barh()
plt.title("Correlación de Pearson de cada feature con RC 28 días (PSI)")
plt.xlabel("Correlación")
plt.axvline(x=0, color="gray", linestyle="--", linewidth=0.8)
plt.tight_layout()
plt.show()
print("Top 5 correlacionadas positivamente:")
print(corr_target.head())
print("\nTop 5 correlacionadas negativamente:")
print(corr_target.tail())
Una correlación alta con el target es buena señal, pero no es suficiente. También busca correlaciones altas entre features: si dos variables tienen correlación >0.95 entre sí, una de ellas es redundante. En datos de cemento esto es común porque muchas variables de composición están relacionadas por restricciones físicas (los porcentajes deben sumar ~100%). El FDSR bajo más la alta correlación entre features justificaron el uso de PCA en este proyecto.
Reducción de dimensiones con PCA
Con ~29 variables altamente correlacionadas entre sí, PCA reduce la dimensionalidad conservando la varianza explicada. Se entrena PCA completo primero para ver cuántos componentes son necesarios, luego se aplica con ese número.
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
pca_full = PCA()
pca_full.fit(df_atributes)
plt.figure(figsize=(10, 5))
plt.plot(np.cumsum(pca_full.explained_variance_ratio_), marker="o", markersize=4)
plt.xlabel("Número de componentes")
plt.ylabel("Varianza acumulada explicada")
plt.axhline(y=0.95, color="red", linestyle="--", label="95% varianza")
plt.legend()
plt.title("PCA — Varianza acumulada")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Aplicar PCA con el número óptimo (ej. 10 componentes → ~95% varianza)
pca = PCA(n_components=10)
df_reducido = pca.fit_transform(df_atributes)
print(f"Varianza explicada con 10 componentes: {pca.explained_variance_ratio_.sum():.2%}")
Split y normalización
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = df_atributes.copy()
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.2, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)
print(f"Train: {X_train_scaled.shape} | Val: {X_val_scaled.shape} | Test: {X_test_scaled.shape}")
El scaler se ajusta (fit) solo sobre los datos de entrenamiento, nunca sobre validación ni test. Si usaras todos los datos para ajustar el scaler, le estarías dando al modelo información del futuro — data leakage. En producción, el modelo recibirá datos nuevos: la normalización debe ser la misma que aprendió en entrenamiento, no recalculada con datos nuevos.
- Eliminaste variables redundantes y de varianza cero.
- Sabes cuántas features quedan y su correlación con el target.
- Aplicaste VarianceThreshold y/o PCA con criterio, no por default.
- El scaler fue ajustado solo en train.
- No hay data leakage: ninguna variable derivada del target está en X.
Decisiones, no solo códigoFase 3 — Modelado
Aquí es donde la mayoría del tiempo se pierde entrenando modelos sin saber por qué se eligieron. En este proyecto, la decisión fue clara: se quería interpretabilidad. El ingeniero de planta necesita entender qué componentes afectan más la resistencia para poder actuar. Deep learning quedaría descartado en primera instancia por esa razón.
¿Por qué estos cuatro modelos?
- Regresión Lineal — baseline obligatorio. Si los demás modelos no lo superan claramente, hay un problema en los datos o en el pipeline.
- Random Forest — robusto a correlaciones entre features, provee importancia de características de forma nativa. Buena interpretabilidad para el ingeniero.
- XGBoost — mejor precisión en benchmarking (menor RMSE), pero ligeramente más complejo de explicar. Máximo ROI relativo.
- Red Neuronal (MLP) — incluida para comparación. En este tipo de dato estructurado y con pocos registros, raramente supera a los ensembles.
¿Por qué no SVR? Se incluyó en iteraciones tempranas del proyecto pero su sensibilidad a los hiperparámetros C y epsilon lo hace costoso de tunear sin ganancia clara frente a Random Forest. Se reemplazó por XGBoost en la versión final.
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.neural_network import MLPRegressor
from xgboost import XGBRegressor
from sklearn.model_selection import cross_val_score
import numpy as np
import pandas as pd
modelos = {
"Regresión Lineal": LinearRegression(),
"Random Forest": RandomForestRegressor(random_state=42, n_jobs=-1),
"XGBoost": XGBRegressor(random_state=42, n_jobs=-1, verbosity=0),
"Red Neuronal": MLPRegressor(hidden_layer_sizes=(64, 32), max_iter=500, random_state=42),
}
resultados = []
for nombre, modelo in modelos.items():
rmse_scores = -cross_val_score(
modelo, X_train_scaled, y_train,
scoring="neg_root_mean_squared_error", cv=5
)
mae_scores = -cross_val_score(
modelo, X_train_scaled, y_train,
scoring="neg_mean_absolute_error", cv=5
)
resultados.append({
"Modelo": nombre,
"RMSE (CV mean)": rmse_scores.mean().round(2),
"MAE (CV mean)": mae_scores.mean().round(2),
})
print(f"{nombre}: RMSE = {rmse_scores.mean():.2f} ± {rmse_scores.std():.2f}")
df_resultados = pd.DataFrame(resultados).sort_values("RMSE (CV mean)")
print("\n", df_resultados)
No elijas el modelo con menor RMSE de forma automática. En este contexto industrial, el criterio es ROI: el balance entre precisión, interpretabilidad, costo de implementación y facilidad de validación humana. XGBoost tuvo el mayor ROI relativo (1.60x vs baseline), pero si el equipo de planta necesita explicar cada predicción en una auditoría, Random Forest (1.45x) puede ser la elección correcta. El mejor modelo es el que el usuario final puede confiar y validar.
Sintonización del modelo seleccionado
from sklearn.model_selection import GridSearchCV
from xgboost import XGBRegressor
param_grid = {
"n_estimators": [100, 200, 300],
"max_depth": [3, 5, 7],
"learning_rate": [0.05, 0.1, 0.2],
"subsample": [0.8, 1.0],
"colsample_bytree": [0.8, 1.0],
}
grid_search = GridSearchCV(
XGBRegressor(random_state=42, verbosity=0),
param_grid,
cv=5,
scoring="neg_root_mean_squared_error",
n_jobs=-1,
verbose=1
)
grid_search.fit(X_train_scaled, y_train)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"Mejor RMSE CV: {-grid_search.best_score_:.2f} PSI")
Guardar modelo y scaler
import pickle
mejor_modelo = grid_search.best_estimator_
mejor_modelo.fit(X_train_scaled, y_train)
with open("models/modelo_cemento_xgb.pkl", "wb") as f:
pickle.dump(mejor_modelo, f)
with open("models/scaler_cemento.pkl", "wb") as f:
pickle.dump(scaler, f)
# También guarda los nombres de columnas: el orden importa en producción
import json
with open("models/columnas_modelo.json", "w") as f:
json.dump(list(df_atributes.columns), f)
print("Modelo, scaler y columnas guardados.")
Más allá de las métricasFase 4 — Evaluación e interpretación
Tener un RMSE bajo no significa que el modelo sea útil. La evaluación real responde tres preguntas: ¿cuánto se equivoca el modelo en términos del negocio?, ¿dónde se equivoca más?, ¿cuándo no deberías confiar en él?
Evaluación en el conjunto de test
from sklearn.metrics import mean_squared_error, mean_absolute_error
import numpy as np
import matplotlib.pyplot as plt
y_pred = mejor_modelo.predict(X_test_scaled)
y_true = y_test.values
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
print(f"MAE: {mae:.2f} PSI — error promedio en PSI")
print(f"RMSE: {rmse:.2f} PSI — penaliza errores grandes")
print(f"MAPE: {mape:.2f}% — error relativo promedio")
# Gráfica: valor real vs predicho
plt.figure(figsize=(8, 6))
plt.scatter(y_true, y_pred, alpha=0.6, edgecolors="none")
plt.plot([y_true.min(), y_true.max()], [y_true.min(), y_true.max()],
"r--", linewidth=1.5, label="Predicción perfecta")
plt.xlabel("Resistencia real (PSI)")
plt.ylabel("Resistencia predicha (PSI)")
plt.title("Real vs. Predicho — XGBoost sintonizado")
plt.legend()
plt.tight_layout()
plt.show()
Comparativa final de modelos
| Modelo | MAE | RMSE | MAPE | ROI relativo | Cuándo elegirlo |
|---|---|---|---|---|---|
| Regresión Lineal | ~12.4 | ~18.0 | ~15% | 1.00x (baseline) | Solo como referencia mínima |
| Random Forest | ~8.0 | ~10.5 | ~9% | 1.45x | Cuando la interpretabilidad importa más que la precisión máxima |
| XGBoost | ~6.8 | ~9.3 | ~7% | 1.60x | Máxima precisión cuando el usuario puede validar sin explicación |
| Red Neuronal | ~7.8 | ~10.1 | ~8% | 1.35x | No recomendado aquí — mayor costo, sin ventaja clara |
El ROI relativo se estima con la fórmula: ROI = [(RMSE_base − RMSE_modelo) × C_error + Ahorro_tiempo + Ahorro_operativo − Costo_modelo] / Costo_modelo. El factor C_error es el costo de cada unidad de error en PSI para el negocio — definido con el cliente.
Importancia de características — ¿el yeso realmente importa?
import pandas as pd
import matplotlib.pyplot as plt
importancias = pd.Series(
mejor_modelo.feature_importances_,
index=df_atributes.columns
).sort_values(ascending=False)
plt.figure(figsize=(10, 6))
importancias.head(15).plot.bar(color="#00b76c")
plt.title("Top 15 features por importancia — XGBoost")
plt.ylabel("Importancia relativa")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.show()
print("Top 5 features:")
print(importancias.head())
La hipótesis inicial era que el yeso tendría influencia directa en la resistencia. El análisis de importancia de características es el momento de verificarlo. Si el yeso no aparece entre las features más importantes, tienes dos opciones: revisar si la variable está bien construida en los datos, o actualizar tu hipótesis. Que el modelo contradiga tu hipótesis no es un fracaso — es información. El clínker conforme resultó ser el predictor de mayor peso, lo cual es coherente con la química del cemento: es el componente principal que define la resistencia final.
¿Cuándo NO confiar en el modelo?
- Cuando cambia la composición fuera del rango entrenado: si introduces proporciones de yeso o clínker que no existían en los datos de entrenamiento, las predicciones son extrapolaciones no validadas.
- Cuando cambian condiciones no modeladas: temperatura de cocción, calidad del agua usada en planta, cambio de proveedor de materia prima. El modelo asume que la composición química es suficiente para explicar la resistencia — si esas condiciones externas cambian, el modelo pierde validez.
- Cuando el input es incorrecto: si el ingeniero ingresa proporciones que no suman correctamente o hay errores de medición, la predicción será inválida. El modelo no tiene mecanismo para detectarlo.
- Calculaste MAE, RMSE y MAPE en el conjunto de test (datos no vistos).
- Graficaste real vs predicho y buscaste patrones sistemáticos de error.
- Verificaste si el modelo sobreestima o subestima en los extremos.
- Confirmaste (o refutaste) tu hipótesis inicial sobre las variables más importantes.
- Definiste en qué condiciones el modelo NO debe usarse.
Del notebook a la plantaFase 5 — Aplicación real
Un modelo que vive en un notebook no tiene valor operacional. La pregunta es: ¿cómo lo hace disponible para el ingeniero de planta sin que tenga que instalar Python, abrir Colab o entender nada de ML?
La solución fue elegante por su simplicidad: Google Sheets como interfaz, FastAPI como backend, ngrok como túnel. El ingeniero ingresa la composición en una hoja de cálculo que ya sabe usar, y en segundos ve la resistencia estimada. Sin instalaciones. Sin capacitación técnica.
Arquitectura del prototipo
El flujo completo tiene cuatro componentes:
- Google Sheets UI: el usuario ingresa los datos de composición y ve la predicción en la misma celda.
- Apps Script Function (
predecirRC()): convierte los datos de la hoja a JSON, hace el request HTTPS al servidor y escribe la predicción de vuelta. - Túnel ngrok: mapea
https://xxx.ngrok.deval puerto local 8000 donde corre FastAPI. - Servidor (Google Colab VM): carga el modelo
.pkly el scaler, recibe el JSON, normaliza, predice y retorna el resultado.
from fastapi import FastAPI
from pydantic import BaseModel
import pickle, json
import numpy as np
import pandas as pd
# Cargar modelo, scaler y nombres de columnas al arrancar
with open("models/modelo_cemento_xgb.pkl", "rb") as f:
modelo = pickle.load(f)
with open("models/scaler_cemento.pkl", "rb") as f:
scaler = pickle.load(f)
with open("models/columnas_modelo.json") as f:
columnas = json.load(f)
app = FastAPI(title="API — Resistencia a la Compresión del Cemento")
class Composicion(BaseModel):
clinker_conforme: float
clinker_no_conforme: float
yeso: float
caliza: float
puzolana: float
# ... resto de atributos de composición en el mismo orden que columnas_modelo.json
@app.post("/predecir")
def predecir(muestra: Composicion):
datos = pd.DataFrame([muestra.dict()], columns=columnas)
datos_norm = scaler.transform(datos)
prediccion = modelo.predict(datos_norm)[0]
return {
"resistencia_28dias_psi": round(float(prediccion), 2),
"resistencia_28dias_mpa": round(float(prediccion) * 0.006895, 2)
}
# Ejecutar: uvicorn api.main:app --reload --port 8000
function predecirRC(clinker_conforme, clinker_no_conforme, yeso, caliza, puzolana) {
// URL del túnel ngrok — actualizar cada vez que se reinicia Colab
const NGROK_URL = "https://xxx.ngrok.dev/predecir";
const payload = {
clinker_conforme: clinker_conforme,
clinker_no_conforme: clinker_no_conforme,
yeso: yeso,
caliza: caliza,
puzolana: puzolana
// ... resto de variables
};
const options = {
method: "post",
contentType: "application/json",
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch(NGROK_URL, options);
const result = JSON.parse(response.getContentText());
return result.resistencia_28dias_psi;
}
// Uso en Sheets: =predecirRC(A2, B2, C2, D2, E2)
En un prototipo industrial, la barrera de adopción más grande no es técnica — es cultural. El ingeniero de planta ya sabe usar Excel o Sheets. Una app nueva requiere capacitación, permisos de IT, y genera resistencia. Usar Sheets como interfaz elimina esa fricción: el usuario hace exactamente lo que ya hacía, pero ahora hay un modelo detrás. Cuando el prototipo demuestre valor, entonces justificas invertir en una interfaz dedicada.
Cómo correr el prototipo completo
# En Google Colab, ejecutar en orden:
# 1. Instalar dependencias
!pip install fastapi uvicorn pyngrok xgboost -q
# 2. Escribir el archivo api/main.py (ver código arriba)
# ... (usa %%writefile api/main.py)
# 3. Levantar el servidor en background
import subprocess
proc = subprocess.Popen(
["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
# 4. Abrir túnel ngrok
from pyngrok import ngrok
ngrok.set_auth_token("TU_TOKEN_AQUI") # registrarse gratis en ngrok.com
tunnel = ngrok.connect(8000)
print(f"API disponible en: {tunnel.public_url}")
# 👉 Copia esta URL y pégala en tu Apps Script
Lo que vive en GitHubEntregables del proyecto
El código completo vive en GitHub. La estructura está diseñada para que cualquier persona pueda reproducir el pipeline sin tener que leer todo el proyecto primero.
ff-industria-cemento-resistencia/
├── README.md ← problema, resultados, cómo correrlo
├── data/
│ ├── README.md ← origen del dataset, licencia, instrucciones
│ └── (usar UCI o Mendeley — ver README)
├── notebooks/
│ ├── README.md ← orden de ejecución y objetivo de cada notebook
│ ├── 01_eda_y_variables.ipynb ← análisis cualitativo + cuantitativo
│ ├── 02_feature_engineering.ipynb ← VarianceThreshold, PCA, split, scaler
│ ├── 03_benchmarking.ipynb ← comparación de 4 modelos con CV
│ └── 04_sintonizacion.ipynb ← GridSearchCV sobre XGBoost
├── src/
│ ├── preprocessing.py ← funciones reutilizables de limpieza
│ └── model.py ← entrenamiento y evaluación
├── api/
│ └── main.py ← endpoint FastAPI
├── models/
│ ├── modelo_cemento_xgb.pkl
│ ├── scaler_cemento.pkl
│ └── columnas_modelo.json
├── results/
│ └── metricas_comparacion.csv ← tabla de resultados exportada
└── requirements.txt
Entregables concretos
- 4 notebooks numerados y comentados, cada uno con objetivo explícito al inicio.
- Modelo entrenado serializado (
.pkl) listo para producción. - API REST funcional con FastAPI.
- Script de Apps Script para integración con Google Sheets.
- Tabla de comparación de modelos con métricas y ROI estimado (
.csv).
Nivel avanzadoExtensiones del proyecto
Si ya completaste el pipeline base y quieres llevarlo más lejos, aquí hay tres extensiones con alto valor técnico y académico:
SHAP para interpretabilidad individual
Random Forest provee importancia global de características. SHAP va más lejos: explica por qué el modelo predijo ese valor para esa muestra específica. Útil para auditar predicciones atípicas y para defensa de tesis. Instala shap y usa shap.TreeExplainer sobre tu modelo XGBoost.
Predicción multi-output (RC 1, 3, 7 y 28 días)
El modelo actual predice solo la resistencia a 28 días. Con MultiOutputRegressor de scikit-learn puedes predecir los cuatro targets simultáneamente. Esto es más útil en producción porque el ingeniero puede ver la curva completa de fraguado predicha, no solo el valor final.
Optimización inversa de la mezcla
Hasta ahora el modelo va en una dirección: composición → resistencia. La extensión más valiosa para el negocio es la inversa: dado un target de resistencia y un presupuesto de costo, ¿cuál es la composición óptima? Esto se resuelve con optimización (scipy.optimize o metaheurísticas) sobre las predicciones del modelo.
Meta-aprendizajeLo que realmente aprendiste
Hacer un proyecto real tiene una diferencia fundamental con seguir un tutorial: en los tutoriales todo sale bien. En proyectos reales, las cosas salen diferentes a lo esperado, y ahí es donde está el aprendizaje.
El EDA sin decisiones no sirve
El error más común en proyectos de ML — incluso entre analistas con experiencia — es hacer EDA como ritual: generas gráficas, calculas estadísticos, y sigues. Pero si cada paso del EDA no termina en una decisión concreta sobre los datos o el pipeline, fue tiempo perdido. La pregunta que debes hacerte después de cada gráfica es: ¿qué hago diferente ahora que vi esto?
El mejor modelo no es el de menor RMSE
XGBoost tuvo el menor RMSE, pero en un entorno industrial donde el ingeniero necesita explicar cada predicción a su gerente, Random Forest puede ser la elección correcta. La métrica de éxito real es el impacto en eficiencia, costos y toma de decisiones — no el número en la tabla de benchmarking.
La adopción empieza desde el despliegue, no desde el modelo
Construiste un modelo excelente. Nadie lo usa porque nadie sabe abrirlo. El despliegue en Google Sheets fue la decisión más inteligente del proyecto: eliminó la fricción de adopción completamente. En proyectos industriales, el 80% del valor viene del 20% de inversión en hacer que el usuario final pueda usarlo sin ayuda.
No documentar desde el principio
Este proyecto se ejecutó hace tiempo y no se documentó completamente en el momento. Reconstruir decisiones de hace tres años es difícil: ¿por qué se eliminó esa variable? ¿qué versión del modelo llegó a producción? ¿qué parámetros tenía? La próxima vez: documenta mientras haces, no después. Un comentario en el notebook tarda 30 segundos y te ahorra horas más adelante.