/ Proyectos / Industria / Cemento
Predecir la resistencia a la compresión del cemento según su composición
Pipeline completo de regresión: EDA, reducción de dimensiones con PCA, benchmarking de SVR, KNN y Random Forest, sintonización con GridSearchCV y despliegue con FastAPI.
Resumen del proyectoWiki
¿Qué encontrarás en este proyecto?
- Cómo predecir la resistencia a la compresión del cemento a 1, 3, 7 y 28 días de fraguado.
- Reducción de 31 atributos de composición química con PCA y VarianceThreshold.
- Benchmarking de SVR, KNN y Random Forest con validación cruzada.
- Sintonización de hiperparámetros con GridSearchCV y despliegue con FastAPI.
Este proyecto resuelve un problema real de la industria cementera: dado que conocemos la composición química de una muestra de cemento (clínker, yeso, caliza, puzolana, etc.), ¿podemos predecir cuánta resistencia a la compresión tendrá a los 28 días de fraguado?
La resistencia a la compresión es la métrica crítica de calidad del cemento. Medirla físicamente requiere fabricar probetas y esperar 28 días. Un modelo de ML permite estimarla desde el día 1 basándose en la composición, acelerando el control de calidad y reduciendo desperdicio.
El dataset contiene 31 atributos de composición química de cemento Tipo ICo, con alta correlación entre variables, lo que hace del problema un caso ideal para explorar técnicas de selección y reducción de características.
Casos de uso del modelo
- Control de calidad temprano: predecir la resistencia a 28 días desde la composición medida en planta, sin esperar el resultado físico.
- Optimización de formulación: simular cómo cambia la resistencia al variar proporciones de ingredientes.
- Detección de anomalías: identificar lotes con composición atípica antes del fraguado.
Stack tecnológico
- Python 3.10+ — lenguaje principal
- Scikit-learn — SVR, KNN, Random Forest, PCA, GridSearchCV, StandardScaler
- pandas / numpy — manipulación de datos
- matplotlib — visualización de predicciones y curvas de aprendizaje
- FastAPI + pydantic — despliegue como API REST
- pickle — serialización del modelo entrenado
Setup del entornoConfiguración del sistema
El proyecto puede correrse localmente o en Google Colab. El notebook original fue desarrollado en Colab con acceso a Google Drive para leer el Excel de datos.
Instalación de dependencias
pip install scikit-learn pandas numpy matplotlib fastapi pydantic uvicorn openpyxl
Estructura del proyecto
proyecto-cemento/
├── data/
│ └── composito_cemento_tipo_ico.xlsx # dataset original
├── models/
│ └── rf_cemento.pkl # modelo Random Forest guardado
├── notebooks/
│ ├── 01_eda.ipynb
│ ├── 02_feature_selection.ipynb
│ ├── 03_benchmarking.ipynb
│ └── 04_deploy.ipynb
├── src/
│ ├── preprocessing.py
│ └── model.py
├── api/
│ └── main.py # endpoint FastAPI
└── requirements.txt
Carga del dataset
El dataset es un Excel con 31 columnas de composición química y 4 columnas target de resistencia a la compresión en PSI (1 día, 3 días, 7 días, 28 días). La primera fila es el header y hay una fila de índice adicional, por lo que se usa header=1.
import pandas as pd
data = pd.read_excel("data/composito_cemento_tipo_ico.xlsx", header=1)
print(data.shape) # (n_muestras, 35)
print(data.head())
print(data.describe())
# Ver los targets disponibles
targets = ["RC 1 día (psi)", "RC 3 días (psi)", "RC 7 días (psi)", "RC 28 días (psi)"]
print(data[targets].describe())
EDA y Feature EngineeringPreparación de datos
Con 31 atributos de composición química y alta correlación entre ellos, la selección y reducción de características es la parte más crítica del pipeline. Un modelo sobre todas las variables sin filtrar puede ser sobreajustado e ininterpretable.
Limpieza inicial
Se eliminan filas con valores nulos y se separa el dataset en atributos (X) y targets (y). La columna Clínker I Total (%) se elimina por ser combinación lineal de otras columnas, y Arcilla Calcinada (%) se descarta porque todos sus valores son cero.
import pandas as pd
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 (%)"]
df_atributes.drop(["Arcilla Calcinada (%)"], axis=1, inplace=True)
# Target: resistencia a 28 días (columna -5 desde el final)
y = data.iloc[:, -5]
print(f"Features: {df_atributes.shape[1]}")
print(f"Muestras: {len(y)}")
print(f"Target stats:\n{y.describe()}")
Análisis de correlación
Antes de reducir dimensiones, se calcula la matriz de correlación de Pearson para entender qué atributos están más relacionados con la resistencia a la compresión.
import matplotlib.pyplot as plt
# Correlación de todos los atributos con los targets
corr_matrix = data.corr()
# Ver correlación de features con RC 28 días
corr_target = corr_matrix["RC 28 días (psi)"].sort_values(ascending=False)
print(corr_target.head(10))
# Scatter matrix de las variables más correlacionadas
columnas = ["RC 28 días (psi)", "Clínker I Conforme (%)", "Clínker I No Conforme (%)"]
pd.plotting.scatter_matrix(data[columnas], figsize=(10, 8), alpha=0.5)
plt.suptitle("Scatter Matrix — variables más correlacionadas con RC 28 días")
plt.tight_layout()
plt.show()
Reducción de dimensiones con PCA
Con 31 atributos altamente correlacionados, PCA reduce la dimensionalidad preservando la varianza explicada. Se grafican los componentes vs. varianza acumulada para elegir el número óptimo.
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# Ajustar PCA completo para ver varianza
pca_full = PCA()
pca_full.fit(df_atributes)
# Gráfica de varianza acumulada
plt.figure(figsize=(10, 5))
plt.plot(np.cumsum(pca_full.explained_variance_ratio_))
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 por número de componentes")
plt.grid(True, alpha=0.3)
plt.show()
# Reducir a 10 componentes (explican ~95% de la 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%}")
Filtrado con VarianceThreshold
Como alternativa al PCA, se aplica VarianceThreshold para eliminar atributos con muy poca varianza — variables casi constantes que no aportan información al modelo.
from sklearn.feature_selection import VarianceThreshold
# Eliminar features con varianza menor al 80% de p*(1-p), p=0.8
sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
df_filtrado = sel.fit_transform(df_atributes)
print(f"Features originales: {df_atributes.shape[1]}")
print(f"Features tras VarianceThreshold: {df_filtrado.shape[1]}")
Split y normalización
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = df_atributes.copy()
# Split: 80% train, 10% val, 10% test
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)
# Normalizar: media 0, varianza 1
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}")
print(f"Val: {X_val_scaled.shape}")
print(f"Test: {X_test_scaled.shape}")
Benchmarking y modeladoEntrenamiento
Se comparan tres modelos de regresión clásicos antes de invertir tiempo en sintonización: SVR, KNN y Random Forest. El criterio de selección es RMSE en validación cruzada.
Benchmarking: SVR, KNN y Random Forest
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_squared_error
import numpy as np
import pandas as pd
modelos = {
"Random Forest": RandomForestRegressor(random_state=42),
"SVR": SVR(C=1.0, epsilon=0.2),
"KNN": KNeighborsRegressor(),
}
errores = {}
for nombre, modelo in modelos.items():
scores = cross_val_score(modelo, X_train_scaled, y_train,
scoring="neg_root_mean_squared_error", cv=5)
errores[nombre] = -scores.mean()
print(f"{nombre}: RMSE = {-scores.mean():.2f} PSI")
eval_modelos = pd.DataFrame(errores.items(), columns=["Modelo", "RMSE"])
print(eval_modelos.sort_values("RMSE"))
Sintonización del mejor modelo: Random Forest con GridSearchCV
Random Forest supera a SVR y KNN en todas las pruebas. Se sintoniza con GridSearchCV para encontrar el número óptimo de árboles, profundidad máxima y mínimo de muestras por hoja.
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestRegressor
param_grid = [
{
"n_estimators": [100, 200, 300],
"max_features": ["sqrt", "log2"],
"max_depth": [None, 10, 20],
"min_samples_leaf": [1, 2, 4],
}
]
grid_search = GridSearchCV(
RandomForestRegressor(random_state=42),
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 el modelo entrenado
import pickle
# Entrenar el modelo final con todos los datos de entrenamiento
RF_final = RandomForestRegressor(**grid_search.best_params_, random_state=42)
RF_final.fit(X_train_scaled, y_train)
# Guardar modelo y scaler
with open("models/rf_cemento.pkl", "wb") as f:
pickle.dump(RF_final, f)
with open("models/scaler_cemento.pkl", "wb") as f:
pickle.dump(scaler, f)
print("Modelo y scaler guardados correctamente.")
Métricas y comparaciónEvaluación
Se evalúan todos los modelos sobre el conjunto de test — datos que ningún modelo vio durante el entrenamiento ni la sintonización.
Evaluación en test
from sklearn.metrics import mean_squared_error, mean_absolute_error
import numpy as np
import matplotlib.pyplot as plt
mejor_modelo = grid_search.best_estimator_
y_pred = mejor_modelo.predict(X_test_scaled)
y_true = y_test
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")
print(f"RMSE: {rmse:.2f} PSI")
print(f"MAPE: {mape:.2f}%")
# Gráfica valor real vs predicho
plt.figure(figsize=(8, 6))
plt.scatter(y_true, y_pred, alpha=0.6)
plt.plot([y_true.min(), y_true.max()], [y_true.min(), y_true.max()],
"r--", label="Predicción perfecta")
plt.xlabel("Resistencia real (PSI)")
plt.ylabel("Resistencia predicha (PSI)")
plt.title("Valor real vs. predicho — Random Forest sintonizado")
plt.legend()
plt.tight_layout()
plt.show()
Tabla comparativa de modelos
| Modelo | MAE (PSI) | RMSE (PSI) | MAPE (%) | Observaciones |
|---|---|---|---|---|
| SVR | ~420 | ~560 | ~14% | Sensible a la escala; funciona bien con normalización |
| KNN | ~380 | ~510 | ~12% | Buen baseline; degradación con features correlacionadas |
| Random Forest (base) | ~280 | ~370 | ~9% | Mejor modelo sin sintonizar |
| Random Forest (GridSearchCV) | ~235 | ~315 | ~7.5% | Mejor modelo tras sintonización — seleccionado para producción |
La sintonización del Random Forest reduce el RMSE en aproximadamente 15% respecto al baseline sin tunear. La mejora más notable es en la captura de muestras con alta resistencia donde el modelo sin tunear tendía a subestimar.
Importancia de características
Una de las ventajas de Random Forest es que provee importancia de características directamente, sin necesitar SHAP. Esto permite identificar qué componentes del cemento más influyen en la resistencia.
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()
plt.title("Top 15 features por importancia — Random Forest")
plt.ylabel("Importancia relativa")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.show()
print("Top 5 features:")
print(importancias.head())
Despliegue y conclusionesHallazgos
Despliegue con FastAPI
El modelo se despliega como una API REST con FastAPI. Cualquier sistema de planta puede enviar la composición de una muestra y recibir la resistencia predicha a 28 días en tiempo real.
from fastapi import FastAPI
from pydantic import BaseModel
import pickle
import numpy as np
# Cargar modelo y scaler al arrancar
with open("models/rf_cemento.pkl", "rb") as f:
modelo = pickle.load(f)
with open("models/scaler_cemento.pkl", "rb") as f:
scaler = pickle.load(f)
app = FastAPI(title="API — Resistencia a la Compresión del Cemento")
class Cemento(BaseModel):
clinker_conforme: float
clinker_no_conforme: float
yeso: float
caliza: float
puzolana: float
# ... resto de atributos de composición
@app.post("/predecir")
def predecir(muestra: Cemento):
datos = np.array([[
muestra.clinker_conforme,
muestra.clinker_no_conforme,
muestra.yeso,
muestra.caliza,
muestra.puzolana,
# ... resto de valores
]])
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
Hallazgos principales
El Clínker Conforme es el predictor más importante
El porcentaje de Clínker Conforme (clínker que cumple especificaciones de calidad) tiene la mayor importancia relativa en el modelo Random Forest. Un aumento en la proporción de clínker conforme está directamente correlacionado con mayor resistencia a la compresión, lo cual es coherente con la química del cemento.
Random Forest supera a SVR y KNN con menos ajuste
Incluso sin sintonización, Random Forest tiene mejor RMSE que SVR y KNN en sus configuraciones por defecto. SVR es muy sensible a C y epsilon; KNN se degrada con atributos correlacionados. Random Forest es más robusto a estos problemas por su naturaleza de ensemble con submuestreo de features.
PCA reduce 31 features a 10 sin pérdida significativa
10 componentes principales explican más del 95% de la varianza del dataset. Usar PCA antes del modelo reduce el tiempo de entrenamiento y mitiga el efecto de la alta correlación entre atributos de composición, aunque en este caso Random Forest sin PCA mostró resultados ligeramente mejores por su manejo nativo de correlaciones.
El modelo es interpretable para el ingeniero de planta
A diferencia de redes neuronales o SVR, el Random Forest provee importancia de características directamente interpretables. El ingeniero puede entender qué componentes afectan más la predicción y actuar sobre la formulación, lo que eleva el valor operacional del modelo.
Limitaciones y trabajo futuro
- El dataset es de una sola planta — la generalización a cementos de otras plantas requiere reentrenamiento o transfer learning.
- El modelo predice RC 28 días pero no modela la curva completa de fraguado (1, 3, 7 y 28 días simultáneamente) — una salida multi-output podría ser más útil en producción.
- No se incorporan variables de proceso (temperatura de cocción, tiempo de molturación) que también afectan la resistencia final.
- Un modelo de SHAP sobre el Random Forest daría explicaciones individuales por muestra, útil para auditar predicciones atípicas.