/ 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.

20 min de lectura Python · Scikit-learn · FastAPI Datos: composición química de cemento Tipo ICo Aplicable a tesis de ingeniería civil o materiales

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.

¿Por qué este problema es relevante para una tesis? Combina conocimiento de dominio (química de materiales) con ML aplicado. Los datos son propietarios en la industria pero el framework es replicable con datos abiertos de UCI. La contribución es clara: interpretabilidad del modelo para que el ingeniero de planta entienda qué componentes afectan más la resistencia.

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

Terminal — instalar dependencias
pip install scikit-learn pandas numpy matplotlib fastapi pydantic uvicorn openpyxl

Estructura del proyecto

Estructura de carpetas recomendada
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.

Python — carga inicial de datos
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.

Python — limpieza y separación X / y
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.

Python — matriz de correlació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.

Python — PCA para reducción de dimensiones
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.

Python — VarianceThreshold
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

Python — split train/val/test y StandardScaler
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

Python — comparación de modelos con validación cruzada
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.

Python — GridSearchCV sobre Random Forest
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

Python — serializar modelo con pickle
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

Python — evaluación final en conjunto de 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.

Python — importancia de características
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.

Python — endpoint FastAPI para predicción
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

Hallazgo 1

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.

Hallazgo 2

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.

Hallazgo 3

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.

Hallazgo 4

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.
¿Cómo adaptar este proyecto a tu tesis? El framework es completamente reutilizable con el dataset UCI Concrete Compressive Strength, que es público y ampliamente citado en la literatura. Puedes agregar SHAP para interpretabilidad individual, extender el target a predicción multi-output (RC 1, 3, 7 y 28 días simultáneamente), o incorporar variables de proceso si tienes acceso a ellas.