Desafíos y Consideraciones para la Implementación Responsable de ML/DL en Geotecnia
Marco Hernandez
La implementación de técnicas de Machine Learning y Deep Learning en geotecnia representa una oportunidad transformadora, pero también conlleva responsabilidades críticas que no pueden ser ignoradas. A diferencia de aplicaciones en comercio electrónico o redes sociales donde un error puede significar una recomendación incorrecta, en ingeniería geotécnica los errores pueden tener consecuencias catastróficas: colapsos estructurales, pérdidas humanas y daños millonarios. Esta sección aborda los desafíos fundamentales y las advertencias que todo ingeniero debe considerar antes de implementar estos sistemas.
Disclaimer
Disclaimer Importante: Este código se encuentra en fase de revisión y validación. Puede contener errores, inconsistencias o comportamientos inesperados. Se recomienda encarecidamente:
- Realizar pruebas exhaustivas antes de usar en producción
- Validar resultados con métodos tradicionales de cálculo
- No utilizar para proyectos críticos sin verificación independiente
- Reportar problemas encontrados para mejora continua
Uso bajo responsabilidad del usuario final. Los autores no se hacen responsables de errores derivados de su implementación.
1. La Calidad de los Datos
1.1 Datos de Calidad sobre Cantidad
El principio fundamental: Un modelo de ML es tan bueno como los datos con los que se entrena. En geotecnia, esto adquiere una dimensión crítica debido a la variabilidad natural del suelo y las implicaciones de seguridad.
Características de datos de calidad en geotecnia
Precisión y Exactitud
- Los datos deben provenir de ensayos realizados según normas internacionales (ASTM, ISO, Eurocódigos)
- Equipos calibrados y certificados
- Personal técnico calificado y acreditado
- Trazabilidad completa desde la toma de muestra hasta el resultado final
Representatividad
- Muestras que capturen la variabilidad espacial del sitio
- Profundidades adecuadas según el tipo de proyecto
- Distribución balanceada entre diferentes tipos de suelo y condiciones
- Cobertura de rangos extremos (no solo valores promedio)
Integridad y Completitud
- Documentación exhaustiva de condiciones de muestreo
- Registro de anomalías o condiciones especiales
- Datos de contexto: ubicación GPS, fecha, clima, nivel freático observado
- Sin valores faltantes críticos o con imputación justificada
1.2 Problemas Comunes con Datos Geotécnicos
Datos Sesgados
# Ejemplo: Detección de sesgo en dataset geotécnico
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
def analizar_sesgo_dataset(df, variable, bins=10):
"""
Analiza el sesgo en la distribución de una variable
"""
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Histograma
axes[0].hist(df[variable], bins=bins, edgecolor='black', alpha=0.7)
axes[0].set_xlabel(variable)
axes[0].set_ylabel('Frecuencia')
axes[0].set_title(f'Distribución de {variable}')
axes[0].axvline(df[variable].mean(), color='red',
linestyle='--', label='Media')
axes[0].axvline(df[variable].median(), color='green',
linestyle='--', label='Mediana')
axes[0].legend()
# Q-Q plot para normalidad
from scipy import stats
stats.probplot(df[variable], dist="norm", plot=axes[1])
axes[1].set_title(f'Q-Q Plot - {variable}')
# Boxplot para outliers
axes[2].boxplot(df[variable])
axes[2].set_ylabel(variable)
axes[2].set_title(f'Boxplot - Detección de Outliers')
plt.tight_layout()
plt.show()
# Estadísticas de sesgo
skewness = df[variable].skew()
kurtosis = df[variable].kurtosis()
print(f"\nAnálisis de Sesgo para {variable}:")
print(f" Asimetría (Skewness): {skewness:.3f}")
if abs(skewness) < 0.5:
print(" → Distribución aproximadamente simétrica")
elif skewness > 0:
print(" → Distribución sesgada hacia la derecha")
else:
print(" → Distribución sesgada hacia la izquierda")
print(f" Curtosis (Kurtosis): {kurtosis:.3f}")
if abs(kurtosis) < 1:
print(" → Distribución mesocúrtica (normal)")
elif kurtosis > 1:
print(" → Distribución leptocúrtica (colas pesadas)")
else:
print(" → Distribución platicúrtica (colas ligeras)")
# Advertencias
if abs(skewness) > 1:
print("\nADVERTENCIA: Sesgo significativo detectado")
print(" Considere transformaciones o balanceo del dataset")
outliers = df[variable].quantile(0.75) + 1.5 * (
df[variable].quantile(0.75) - df[variable].quantile(0.25)
)
n_outliers = (df[variable] > outliers).sum()
if n_outliers > len(df) * 0.05:
print(f"\nADVERTENCIA: {n_outliers} outliers detectados ({n_outliers/len(df)*100:.1f}%)")
print(" Revise si son errores de medición o casos válidos extremos")
Datos Faltantes y Estrategias
def auditoria_datos_faltantes(df):
"""
Auditoría completa de datos faltantes en dataset geotécnico
"""
print("="*70)
print("AUDITORÍA DE DATOS FALTANTES")
print("="*70)
missing = df.isnull().sum()
missing_pct = (missing / len(df)) * 100
missing_df = pd.DataFrame({
'Variable': missing.index,
'Faltantes': missing.values,
'Porcentaje': missing_pct.values
}).sort_values('Porcentaje', ascending=False)
print("\nResumen de Datos Faltantes:")
print(missing_df[missing_df['Faltantes'] > 0])
# Visualización
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
sns.heatmap(df.isnull(), cbar=True, yticklabels=False, cmap='viridis')
plt.title('Patrón de Datos Faltantes')
plt.subplot(1, 2, 2)
missing_df_filtered = missing_df[missing_df['Porcentaje'] > 0]
if len(missing_df_filtered) > 0:
plt.barh(missing_df_filtered['Variable'], missing_df_filtered['Porcentaje'])
plt.xlabel('Porcentaje de Datos Faltantes')
plt.title('Datos Faltantes por Variable')
plt.tight_layout()
plt.show()
# Recomendaciones
print("\n" + "="*70)
print("RECOMENDACIONES:")
print("="*70)
for _, row in missing_df[missing_df['Faltantes'] > 0].iterrows():
var = row['Variable']
pct = row['Porcentaje']
print(f"\n{var} ({pct:.1f}% faltante):")
if pct < 5:
print(" ✓ Eliminar filas con datos faltantes (pérdida mínima)")
elif pct < 15:
print(" → Imputación con media/mediana para variables numéricas")
print(" → Considerar imputación por KNN o regresión")
elif pct < 40:
print(" Imputación múltiple o modelado del patrón de falta")
print(" Evaluar si el patrón de falta es informativo")
else:
print(" Considerar eliminar la variable del análisis")
print(" Pérdida de información demasiado alta")
# NUNCA usar estas prácticas incorrectas:
def practicas_incorrectas_ejemplo():
"""
PRÁCTICAS INCORRECTAS - NO USAR
"""
# INCORRECTO: Imputar con ceros sin justificación
# df['cohesion'].fillna(0, inplace=True)
# INCORRECTO: Imputar sin considerar física del suelo
# df['friccion'].fillna(df['friccion'].mean(), inplace=True)
# INCORRECTO: Eliminar variables críticas
# df.dropna(axis=1, inplace=True)
pass
# ✓ CORRECTO: Imputación informada
def imputacion_informada_geotecnia(df):
"""
Imputación correcta considerando correlaciones geotécnicas
"""
from sklearn.impute import KNNImputer
# Para variables correlacionadas (límites de Atterberg)
if df['limite_liquido'].isnull().any():
# Usar KNN considerando otras propiedades índice
imputer = KNNImputer(n_neighbors=5, weights='distance')
features_atterberg = ['limite_liquido', 'limite_plastico',
'indice_plasticidad']
df[features_atterberg] = imputer.fit_transform(df[features_atterberg])
# Para variables con correlaciones físicas conocidas
# (ej: peso específico vs contenido de humedad)
if df['peso_unitario'].isnull().any() and not df['contenido_humedad'].isnull().all():
# Imputar basándose en correlaciones geotécnicas establecidas
df['peso_unitario'].fillna(
20 - 0.1 * df['contenido_humedad'],
inplace=True
)
return df
1.3 Casos Reales: Cuando los Datos Malos Llevan a Decisiones Catastróficas
Caso 1: Colapso de terraplén por datos no representativos
- Un modelo ML entrenado principalmente con suelos granulares predijo FS=1.8
- El sitio real contenía arcillas blandas no representadas en el dataset
- Resultado: Colapso parcial durante construcción
- Lección: La representatividad geográfica y estratigráfica es crítica
Caso 2: Sobrestimación de capacidad portante
- Dataset contenía principalmente resultados de SPT en suelos densos
- Modelo extrapoló mal para suelos sueltos
- Diseño de cimentación inadecuado requirió rediseño costoso
- Lección: Nunca extrapolar más allá del rango de entrenamiento
2. Auditoría de Datos: El Proceso Obligatorio
2.1 Protocolo de Auditoría Pre-Modelado
class AuditoriaGeotecnica:
"""
Sistema completo de auditoría para datasets geotécnicos
"""
def __init__(self, df, metadata=None):
self.df = df
self.metadata = metadata or {}
self.reporte_auditoria = []
def validar_rangos_fisicos(self):
"""
Valida que los valores estén dentro de rangos físicamente posibles
"""
print("\n" + "="*70)
print("1. VALIDACIÓN DE RANGOS FÍSICOS")
print("="*70)
reglas = {
'limite_liquido': (0, 500, '%'),
'limite_plastico': (0, 200, '%'),
'contenido_humedad': (0, 200, '%'),
'peso_unitario': (10, 25, 'kN/m³'),
'angulo_friccion': (0, 50, '°'),
'cohesion': (0, 500, 'kPa'),
'porosidad': (0, 1, '-'),
'permeabilidad': (1e-12, 1e-2, 'm/s')
}
violaciones = []
for var, (min_val, max_val, unidad) in reglas.items():
if var in self.df.columns:
fuera_rango = (self.df[var] < min_val) | (self.df[var] > max_val)
n_violaciones = fuera_rango.sum()
if n_violaciones > 0:
print(f"\n {var}:")
print(f" Rango esperado: {min_val} - {max_val} {unidad}")
print(f" Violaciones: {n_violaciones}")
print(f" Valores: {self.df.loc[fuera_rango, var].values[:5]}")
violaciones.append(var)
else:
print(f"✓ {var}: Todos los valores dentro del rango")
return violaciones
def validar_consistencia_fisica(self):
"""
Valida consistencia entre variables relacionadas
"""
print("\n" + "="*70)
print("2. VALIDACIÓN DE CONSISTENCIA FÍSICA")
print("="*70)
inconsistencias = []
# Límites de Atterberg
if all(col in self.df.columns for col in ['limite_liquido', 'limite_plastico']):
invalidos = self.df['limite_liquido'] < self.df['limite_plastico']
if invalidos.any():
print(f"\n LL < LP detectado en {invalidos.sum()} casos")
print(" El límite líquido siempre debe ser mayor que el plástico")
inconsistencias.append('Atterberg')
else:
print(" Límites de Atterberg consistentes")
# Índice de plasticidad
if all(col in self.df.columns for col in ['limite_liquido', 'limite_plastico', 'indice_plasticidad']):
ip_calculado = self.df['limite_liquido'] - self.df['limite_plastico']
diferencia = abs(self.df['indice_plasticidad'] - ip_calculado)
if (diferencia > 2).any():
print(f"\n IP calculado difiere del registrado en {(diferencia > 2).sum()} casos")
inconsistencias.append('IP')
# Peso específico vs porosidad
if all(col in self.df.columns for col in ['peso_unitario', 'porosidad']):
# Gs típico entre 2.6-2.8
Gs_implicito = self.df['peso_unitario'] / ((1 - self.df['porosidad']) * 9.81)
fuera_rango = (Gs_implicito < 2.3) | (Gs_implicito > 3.0)
if fuera_rango.any():
print(f"\n Gravedad específica implícita fuera de rango en {fuera_rango.sum()} casos")
print(f" Rango detectado: {Gs_implicito[fuera_rango].min():.2f} - {Gs_implicito[fuera_rango].max():.2f}")
inconsistencias.append('Gs')
return inconsistencias
def detectar_outliers_multivariados(self):
"""
Detecta outliers considerando correlaciones entre variables
"""
print("\n" + "="*70)
print("3. DETECCIÓN DE OUTLIERS MULTIVARIADOS")
print("="*70)
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
# Seleccionar variables numéricas
numeric_cols = self.df.select_dtypes(include=[np.number]).columns
X = self.df[numeric_cols].dropna()
# Normalizar
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Isolation Forest
iso_forest = IsolationForest(contamination=0.05, random_state=42)
outliers = iso_forest.fit_predict(X_scaled)
n_outliers = (outliers == -1).sum()
pct_outliers = n_outliers / len(X) * 100
print(f"\nOutliers detectados: {n_outliers} ({pct_outliers:.2f}%)")
if n_outliers > 0:
print("\n ACCIÓN REQUERIDA:")
print(" 1. Revisar manualmente cada caso outlier")
print(" 2. Verificar si son errores de transcripción")
print(" 3. Confirmar si representan condiciones extremas válidas")
print(" 4. Documentar decisión de mantener o eliminar")
return outliers
def generar_reporte_calidad(self):
"""
Genera reporte ejecutivo de calidad de datos
"""
print("\n" + "="*70)
print("REPORTE EJECUTIVO DE CALIDAD DE DATOS")
print("="*70)
# Completitud
completitud = (1 - self.df.isnull().sum() / len(self.df)) * 100
print("\nCompletitud por variable:")
for var, pct in completitud.items():
emoji = "✓" if pct == 100 else "⚠️" if pct > 90 else "❌"
print(f" {emoji} {var}: {pct:.1f}%")
# Tamaño del dataset
print(f"\nTamaño del dataset:")
print(f" Filas: {len(self.df)}")
print(f" Columnas: {len(self.df.columns)}")
# Evaluación general
score_completitud = completitud.mean()
print(f"\n" + "="*70)
print("EVALUACIÓN GENERAL")
print("="*70)
if score_completitud >= 95:
print(" EXCELENTE: Dataset de alta calidad")
elif score_completitud >= 85:
print(" BUENO: Calidad aceptable con mejoras menores necesarias")
elif score_completitud >= 70:
print(" REGULAR: Se requieren mejoras significativas")
else:
print(" DEFICIENTE: No recomendado para modelado sin limpieza exhaustiva")
print(f"\nScore de Completitud: {score_completitud:.1f}%")
def ejecutar_auditoria_completa(self):
"""
Ejecuta auditoría completa y genera reporte
"""
print("\n" + "🔍 "*35)
print("INICIANDO AUDITORÍA COMPLETA DE DATOS GEOTÉCNICOS")
print("🔍 "*35)
v1 = self.validar_rangos_fisicos()
v2 = self.validar_consistencia_fisica()
v3 = self.detectar_outliers_multivariados()
self.generar_reporte_calidad()
# Recomendación final
tiene_problemas = len(v1) > 0 or len(v2) > 0
if tiene_problemas:
print("\n" + "⚠️ "*35)
print("RECOMENDACIÓN: No proceder con modelado hasta resolver inconsistencias")
print("⚠️ "*35)
else:
print("\n" + "✓ "*35)
print("Dataset validado - Puede proceder con análisis exploratorio")
print("✓ "*35)
# Ejemplo de uso
"""
df_geotecnico = pd.read_csv('datos_geotecnicos.csv')
auditoria = AuditoriaGeotecnica(df_geotecnico)
auditoria.ejecutar_auditoria_completa()
"""
3. Análisis Exploratorio: La Base del Entendimiento
3.1 Por Qué el EDA No Es Opcional
El Análisis Exploratorio de Datos (EDA) en geotecnia no es solo una “buena práctica”, es una obligación profesional que puede prevenir errores costosos y peligrosos.
Objetivos del EDA en Geotecnia
- Entender la variabilidad natural: El suelo es heterogéneo por naturaleza
- Identificar patrones geológicos: Estratificación, discontinuidades
- Detectar anomalías: Errores de laboratorio vs condiciones extremas reales
- Validar correlaciones: Confirmar relaciones físicas conocidas
- Informar selección de modelo: Complejidad apropiada al problema
3.2 Checklist de EDA para Proyectos Geotécnicos
def eda_geotecnico_completo(df):
"""
Análisis exploratorio exhaustivo para datos geotécnicos
"""
print("="*70)
print("ANÁLISIS EXPLORATORIO DE DATOS GEOTÉCNICOS")
print("="*70)
# 1. Estadísticas descriptivas contextualizadas
print("\n1. ESTADÍSTICAS DESCRIPTIVAS")
print("-"*70)
for col in df.select_dtypes(include=[np.number]).columns:
print(f"\n{col}:")
print(f" Media: {df[col].mean():.2f}")
print(f" Mediana: {df[col].median():.2f}")
print(f" Desv. Est: {df[col].std():.2f}")
print(f" CV: {(df[col].std()/df[col].mean()*100):.1f}%")
# Contexto geotécnico
cv = df[col].std()/df[col].mean()*100
if cv < 15:
print(" → Variabilidad BAJA (suelo homogéneo)")
elif cv < 30:
print(" → Variabilidad MODERADA (típico)")
else:
print(" → Variabilidad ALTA (suelo heterogéneo o múltiples estratos)")
# 2. Visualización de distribuciones
print("\n2. DISTRIBUCIONES")
print("-"*70)
numeric_cols = df.select_dtypes(include=[np.number]).columns
n_cols = len(numeric_cols)
n_rows = (n_cols + 2) // 3
fig, axes = plt.subplots(n_rows, 3, figsize=(15, 4*n_rows))
axes = axes.flatten() if n_cols > 1 else [axes]
for idx, col in enumerate(numeric_cols):
axes[idx].hist(df[col].dropna(), bins=30, edgecolor='black', alpha=0.7)
axes[idx].set_xlabel(col)
axes[idx].set_ylabel('Frecuencia')
axes[idx].set_title(f'Distribución: {col}')
axes[idx].axvline(df[col].mean(), color='red', linestyle='--', label='Media')
axes[idx].axvline(df[col].median(), color='green', linestyle='--', label='Mediana')
axes[idx].legend()
# Ocultar ejes sobrantes
for idx in range(len(numeric_cols), len(axes)):
axes[idx].axis('off')
plt.tight_layout()
plt.show()
# 3. Matriz de correlación con interpretación
print("\n3. CORRELACIONES")
print("-"*70)
corr_matrix = df[numeric_cols].corr()
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm',
center=0, square=True, linewidths=1)
plt.title('Matriz de Correlación - Variables Geotécnicas', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
# Identificar correlaciones sospechosas
print("\nCorrelaciones Significativas (|r| > 0.7):")
for i in range(len(corr_matrix.columns)):
for j in range(i+1, len(corr_matrix.columns)):
corr_val = corr_matrix.iloc[i, j]
if abs(corr_val) > 0.7:
var1 = corr_matrix.columns[i]
var2 = corr_matrix.columns[j]
print(f" {var1} ↔ {var2}: r = {corr_val:.3f}")
# Validación física
correlaciones_esperadas = [
('limite_liquido', 'limite_plastico'),
('limite_liquido', 'indice_plasticidad'),
('cohesion', 'indice_plasticidad'),
]
es_esperada = any(
(var1 in pair and var2 in pair) or (var2 in pair and var1 in pair)
for pair in correlaciones_esperadas
)
if es_esperada:
print(" ✓ Correlación físicamente esperada")
else:
print(" Correlación no típica - revisar")
# 4. Análisis de outliers con contexto
print("\n4. ANÁLISIS DE OUTLIERS")
print("-"*70)
fig, axes = plt.subplots(1, min(4, len(numeric_cols)), figsize=(16, 4))
if len(numeric_cols) == 1:
axes = [axes]
for idx, col in enumerate(list(numeric_cols)[:4]):
axes[idx].boxplot(df[col].dropna())
axes[idx].set_ylabel(col)
axes[idx].set_title(f'Boxplot: {col}')
# Calcular outliers
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
outliers = df[(df[col] < Q1 - 1.5*IQR) | (df[col] > Q3 + 1.5*IQR)][col]
if len(outliers) > 0:
axes[idx].text(1.15, df[col].median(),
f'{len(outliers)} outliers',
fontsize=10, color='red')
plt.tight_layout()
plt.show()
return corr_matrix
3.3 Visualizaciones Críticas para Geotecnia
def visualizaciones_geotecnicas_avanzadas(df):
"""
Visualizaciones específicas para análisis geotécnico
"""
# 1. Gráfico de Casagrande (Carta de Plasticidad)
if all(col in df.columns for col in ['limite_liquido', 'indice_plasticidad']):
plt.figure(figsize=(10, 8))
# Línea A (Casagrande)
ll_range = np.linspace(0, 100, 100)
linea_a = 0.73 * (ll_range - 20)
# Línea U (límite superior)
linea_u = 0.9 * (ll_range - 8)
plt.plot(ll_range, linea_a, 'r--', linewidth=2, label='Línea A')
plt.plot(ll_range, linea_u, 'b--', linewidth=2, label='Línea U')
plt.axvline(x=50, color='gray', linestyle=':', alpha=0.5)
# Clasificación visual
plt.scatter(df['limite_liquido'], df['indice_plasticidad'],
alpha=0.6, s=50, edgecolors='black')
# Zonas de clasificación
plt.text(30, 15, 'CL', fontsize=12, fontweight='bold', color='red')
plt.text(70, 15, 'ML', fontsize=12, fontweight='bold', color='blue')
plt.text(70, 45, 'CH', fontsize=12, fontweight='bold', color='darkred')
plt.text(30, 45, 'MH', fontsize=12, fontweight='bold', color='darkblue')
plt.xlabel('Límite Líquido (%)', fontsize=12)
plt.ylabel('Índice de Plasticidad (%)', fontsize=12)
plt.title('Carta de Plasticidad de Casagrande', fontsize=14, fontweight='bold')
plt.legend()
plt.grid(True, alpha=0.3)
plt.xlim(0, 100)
plt.ylim(0, 60)
plt.tight_layout()
plt.show()
print("✓ Carta de Casagrande generada")
print(" Permite validar clasificación SUCS de suelos cohesivos")
# 2. Perfil estratigráfico simplificado
if 'profundidad' in df.columns and 'tipo_suelo' in df.columns:
plt.figure(figsize=(10, 12))
colores_suelo = {
'Arena': 'gold',
'Arcilla': 'brown',
'Limo': 'tan',
'Grava': 'gray'
}
for idx, row in df.iterrows():
color = colores_suelo.get(row['tipo_suelo'], 'white')
plt.barh(row['profundidad'], 1, height=0.5, color=color,
edgecolor='black', label=row['tipo_suelo'])
# Eliminar duplicados de leyenda
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())
plt.xlabel('Tipo de Suelo')
plt.ylabel('Profundidad (m)')
plt.title('Perfil Estratigráfico Simplificado')
plt.gca().invert_yaxis()
plt.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# Ejemplo de uso completo
"""
# Cargar datos
df = pd.read_csv('datos_geotecnicos.csv')
# Auditoría
auditoria = AuditoriaGeotecnica(df)
auditoria.ejecutar_auditoria_completa()
# EDA
eda_geotecnico_completo(df)
visualizaciones_geotecnicas_avanzadas(df)
"""
4. Respeto a Normas y Control de Calidad
4.1 Marco Normativo Obligatorio
El uso de ML/DL en geotecnia NO exime del cumplimiento de normas y estándares internacionales. Los modelos deben complementar, no reemplazar, los protocolos establecidos.
Normas Clave a Respetar
Normas de Ensayos de Laboratorio:
- ASTM D2487: Clasificación unificada de suelos (SUCS)
- ASTM D4318: Límites de Atterberg
- ASTM D2166: Resistencia a compresión no confinada
- ASTM D3080: Ensayo de corte directo
- ASTM D2850: Ensayo triaxial no consolidado no drenado
Normas de Diseño:
- Eurocódigo 7: Diseño geotécnico (EN 1997)
- AASHTO: Diseño de carreteras y puentes
- ACI 318: Código de construcción de concreto (cimentaciones)
- Normas nacionales: NSR-10 (Colombia), E.050 (Perú), etc.
ISO para Calidad:
- ISO 9001: Gestión de calidad
- ISO/IEC 17025: Competencia de laboratorios
- ISO 14688: Identificación y clasificación de suelos
4.2 Integración de ML con Control de Calidad Normativo
class ControlCalidadNormativo:
"""
Sistema de control de calidad que integra ML con normas geotécnicas
"""
def __init__(self, modelo_ml, norma='ASTM'):
self.modelo = modelo_ml
self.norma = norma
self.log_validaciones = []
def validar_prediccion_contra_norma(self, prediccion, tipo_ensayo, parametros):
"""
Valida si la predicción de ML cumple con rangos normativos
"""
print("\n" + "="*70)
print(f"VALIDACIÓN NORMATIVA - {self.norma}")
print("="*70)
cumple = True
advertencias = []
# Ejemplo: Validación de Factor de Seguridad según Eurocódigo 7
if tipo_ensayo == 'factor_seguridad_talud':
fs_min_permanente = 1.5 # EC7 para situación permanente
fs_min_transitorio = 1.3 # EC7 para situación transitoria
print(f"\nPredicción ML: FS = {prediccion:.3f}")
print(f"Requisito EC7 (permanente): FS ≥ {fs_min_permanente}")
print(f"Requisito EC7 (transitorio): FS ≥ {fs_min_transitorio}")
if prediccion < fs_min_transitorio:
print("\n NO CUMPLE: Factor de seguridad por debajo del mínimo")
cumple = False
elif prediccion < fs_min_permanente:
print("\n ADVERTENCIA: Cumple para transitorio, no para permanente")
advertencias.append("FS insuficiente para condición permanente")
else:
print("\n✓ CUMPLE: Factor de seguridad adecuado")
# Capacidad portante según normas
elif tipo_ensayo == 'capacidad_portante':
tipo_cimentacion = parametros.get('tipo', 'superficial')
if tipo_cimentacion == 'superficial':
# Factor de seguridad mínimo típico
fs_min = 3.0
qadm = prediccion / fs_min
print(f"\nCapacidad última predicha: {prediccion:.2f} kPa")
print(f"Capacidad admisible (FS=3.0): {qadm:.2f} kPa")
# Validar contra asentamientos
asentamiento_max = parametros.get('asentamiento_max', 25) # mm
print(f"Límite de asentamiento: {asentamiento_max} mm")
if qadm < 50:
advertencias.append("Capacidad portante muy baja")
# Correlación SPT para arenas (validación cruzada)
elif tipo_ensayo == 'angulo_friccion_spt':
N_spt = parametros.get('N_spt')
# Correlaciones establecidas (Peck, Hansen, Thornburn)
phi_peck = 28 + 0.15 * N_spt # Aproximado
diferencia = abs(prediccion - phi_peck)
print(f"\nPredicción ML: φ = {prediccion:.1f}°")
print(f"Correlación Peck: φ = {phi_peck:.1f}°")
print(f"Diferencia: {diferencia:.1f}°")
if diferencia > 5:
advertencias.append(
f"Desviación significativa de correlación establecida ({diferencia:.1f}°)"
)
print("\n ADVERTENCIA: Validar con ensayos adicionales")
# Registro de validación
self.log_validaciones.append({
'timestamp': pd.Timestamp.now(),
'tipo_ensayo': tipo_ensayo,
'prediccion': prediccion,
'cumple': cumple,
'advertencias': advertencias
})
return cumple, advertencias
def generar_certificado_validacion(self):
"""
Genera certificado de que las predicciones han sido validadas normativamente
"""
print("\n" + "="*70)
print("CERTIFICADO DE VALIDACIÓN NORMATIVA")
print("="*70)
print(f"\nNorma de referencia: {self.norma}")
print(f"Total de validaciones: {len(self.log_validaciones)}")
df_log = pd.DataFrame(self.log_validaciones)
if len(df_log) > 0:
tasa_cumplimiento = (df_log['cumple'].sum() / len(df_log)) * 100
print(f"Tasa de cumplimiento: {tasa_cumplimiento:.1f}%")
if tasa_cumplimiento == 100:
print("\n Todas las predicciones cumplen requisitos normativos")
elif tasa_cumplimiento >= 90:
print("\n Mayoría cumple, revisar casos específicos")
else:
print("\n Tasa de cumplimiento insuficiente - No recomendado para uso en producción")
return df_log
# Ejemplo de uso integrado
"""
# Crear sistema de control
control = ControlCalidadNormativo(modelo_ml=mi_modelo, norma='Eurocódigo 7')
# Validar cada predicción
fs_predicho = 1.45
cumple, warnings = control.validar_prediccion_contra_norma(
prediccion=fs_predicho,
tipo_ensayo='factor_seguridad_talud',
parametros={'altura': 10, 'angulo': 35}
)
# Generar certificado al final del proyecto
certificado = control.generar_certificado_validacion()
"""
4.3 Principio de Redundancia: ML + Métodos Tradicionales
REGLA DE ORO: En proyectos críticos, las predicciones de ML deben ser validadas con métodos tradicionales establecidos.
def validacion_cruzada_metodos(caso_estudio):
"""
Compara predicción ML con cálculos tradicionales
"""
print("="*70)
print("VALIDACIÓN CRUZADA: ML vs MÉTODOS TRADICIONALES")
print("="*70)
# Predicción ML
fs_ml = modelo_ml.predict(caso_estudio)
# Método tradicional (ej: Bishop)
fs_bishop = calcular_bishop_tradicional(caso_estudio)
# Método alternativo (ej: Spencer)
fs_spencer = calcular_spencer(caso_estudio)
print(f"\nResultados:")
print(f" ML Prediction: FS = {fs_ml:.3f}")
print(f" Bishop Simplified: FS = {fs_bishop:.3f}")
print(f" Spencer Method: FS = {fs_spencer:.3f}")
# Análisis de discrepancia
diferencia_ml_bishop = abs(fs_ml - fs_bishop)
diferencia_ml_spencer = abs(fs_ml - fs_spencer)
print(f"\nDiscrepancias:")
print(f" ML vs Bishop: {diferencia_ml_bishop:.3f} ({diferencia_ml_bishop/fs_bishop*100:.1f}%)")
print(f" ML vs Spencer: {diferencia_ml_spencer:.3f} ({diferencia_ml_spencer/fs_spencer*100:.1f}%)")
# Recomendación
if diferencia_ml_bishop < 0.1 and diferencia_ml_spencer < 0.1:
print("\nEXCELENTE: Métodos convergen (diferencia < 10%)")
print(" Predicción ML validada")
elif diferencia_ml_bishop < 0.2:
print("\nACEPTABLE: Diferencia moderada")
print(" Usar promedio de métodos para diseño")
else:
print("\nADVERTENCIA: Discrepancia significativa")
print(" Revisar datos de entrada y modelo")
print(" NO usar predicción ML sin análisis adicional")
return {
'fs_ml': fs_ml,
'fs_bishop': fs_bishop,
'fs_spencer': fs_spencer,
'validado': diferencia_ml_bishop < 0.2
}
5. Interpretabilidad y Explicabilidad: Requisito Crítico
En ingeniería, no basta con que el modelo “funcione” - debemos entender POR QUÉ hace cada predicción.
5.1 Técnicas de Interpretabilidad
# SHAP Values para explicabilidad
import shap
def explicar_prediccion_shap(modelo, X, feature_names, caso_especifico_idx):
"""
Explica por qué el modelo hizo una predicción específica
"""
# Crear explicador
explainer = shap.DeepExplainer(modelo, X[:100])
# Valores SHAP para el caso
shap_values = explainer.shap_values(X[caso_especifico_idx:caso_especifico_idx+1])
# Visualización waterfall
shap.waterfall_plot(shap.Explanation(
values=shap_values[0],
base_values=explainer.expected_value,
data=X[caso_especifico_idx],
feature_names=feature_names
))
print("\nInterpretación de contribuciones:")
contributions = list(zip(feature_names, shap_values[0]))
contributions.sort(key=lambda x: abs(x[1]), reverse=True)
for feature, value in contributions[:5]:
direccion = "aumentó" if value > 0 else "disminuyó"
print(f" - {feature} {direccion} FS en {abs(value):.3f}")
# Importancia de características global
def analizar_importancia_global(modelo, X, feature_names):
"""
Analiza qué variables son más importantes globalmente
"""
explainer = shap.DeepExplainer(modelo, X[:100])
shap_values = explainer.shap_values(X)
shap.summary_plot(shap_values, X, feature_names=feature_names)
# Ranking de importancia
importancia = np.abs(shap_values).mean(axis=0)
ranking = sorted(zip(feature_names, importancia),
key=lambda x: x[1], reverse=True)
print("\nRanking de Importancia de Variables:")
for idx, (feature, imp) in enumerate(ranking, 1):
print(f" {idx}. {feature}: {imp:.4f}")
6. Responsabilidad Profesional y Ética
6.1 Declaración de Limitaciones
TODO reporte que use ML/DL DEBE incluir:
---
DECLARACIÓN DE LIMITACIONES Y ALCANCE
---
Este análisis utiliza modelos de Machine Learning entrenados con [descripción del dataset].
LIMITACIONES:
1. Las predicciones son válidas únicamente dentro del siguiente rango:
- Altura de talud: X - Y metros
- Ángulo: A - B grados
- Cohesión: C - D kPa
- [otros parámetros]
2. El modelo NO debe usarse para:
- Suelos con características fuera del rango de entrenamiento
- Condiciones sísmicas no contempladas en el entrenamiento
- [otras limitaciones específicas]
3. Validación: Este análisis debe ser validado por métodos tradicionales
antes de su uso en diseño final.
4. Responsabilidad: El ingeniero responsable debe ejercer juicio profesional
independiente y no debe confiar únicamente en estas predicciones.
Firma del Ingeniero Responsable: **\*\***\_\_\_**\*\***
Fecha: **\*\***\_\_\_**\*\***
Registro Profesional: **\*\***\_\_\_**\*\***
6.2 Checklist Final Antes de Implementación
☐ Dataset auditado y validado
☐ Análisis exploratorio completo documentado
☐ Modelo validado con datos independientes (test set)
☐ Predicciones comparadas con métodos tradicionales
☐ Cumplimiento normativo verificado
☐ Limitaciones claramente documentadas
☐ Sistema de monitoreo post-implementación establecido
☐ Plan de actualización del modelo definido
☐ Responsabilidad profesional asignada
☐ Firma del ingeniero responsable obtenida
Conclusión
El uso de ML/DL en geotecnia es una herramienta poderosa, pero requiere:
- Rigor científico: Datos de calidad, auditorías exhaustivas
- Transparencia: Análisis exploratorios completos
- Responsabilidad: Cumplimiento normativo y validación cruzada
- Humildad: Reconocer limitaciones y no sustituir juicio ingenieril
La tecnología debe servir a la ingeniería, no reemplazarla.
Autor: Ing. Marco A. Hernández. Ingeniero Geologo. Universidad Central de Venezuela. 2006-2025. Linkedin