Figure Friday 2025 - week 20

Hi all,
this updated USA Dams Dashboard shows:

  • Total Dams: 10,186
  • Average Height: 31.7 ft
  • Most Common Hazard: Low

The map displays dams by Hazard Potential Classification.

Filters on the left allow users to sort by State, Hazard Potential, and Year Completed.
I randomly clicked on a dump, the details of which can be seen in the card on the right.

5 Likes

@Ester it’s really useful that you added the link to each summary card of the dam, after the dam is clicked in the map.

One small enhancement I would suggest is to make the dropdowns dynamically tied to each other. So if a person chooses a state and that state only has high and low hazard potential, the only options for that second dropdown would be high and low.

1 Like

Hey all,
Just a reminder that our Figure Friday session starts today at noon Eastern Time, in 1 hour and 15 minutes.

This is the code with the isolation forest code, s

import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings('ignore')

# Cargar los datos

# Ajusta el nombre del archivo según sea necesario
try:
    df = pd.read_csv('nation-dams.csv')
    print(f"Datos cargados correctamente. Forma: {df.shape}")
except Exception as e:
    print(f"Error al cargar los datos: {e}")
    # Si no se puede cargar el archivo, creamos un DataFrame vacío con las columnas necesarias
    df = pd.DataFrame()

# Función para identificar y seleccionar características numéricas adecuadas
def get_numeric_features(dataframe):
    # Seleccionamos columnas numéricas con menos de 30% de valores nulos
    numeric_cols = dataframe.select_dtypes(include=['int64', 'float64']).columns.tolist()
    valid_cols = []
    
    for col in numeric_cols:
        if dataframe[col].notnull().sum() / len(dataframe) > 0.7:  # Menos de 30% de nulos
            valid_cols.append(col)
    
    # Excluimos Latitude y Longitude para el análisis de anomalías
    exclude_cols = ['Latitude', 'Longitude']
    valid_cols = [col for col in valid_cols if col not in exclude_cols]
    
    return valid_cols

# Función principal para detectar anomalías por categoría de peligro
def detect_anomalies_by_hazard(df, contamination=0.05):
    if df.empty:
        print("El DataFrame está vacío. No se pueden detectar anomalías.")
        return df
    
    # Verificar que la columna 'Hazard Potential Classification' existe
    if 'Hazard Potential Classification' not in df.columns:
        print("No se encontró la columna 'Hazard Potential Classification'")
        return df
    
    # Obtener categorías únicas de peligro
    hazard_categories = df['Hazard Potential Classification'].unique()
    print(f"Categorías de peligro identificadas: {hazard_categories}")
    
    # Crear un DataFrame para almacenar resultados
    results = []
    
    # Procesar cada categoría de peligro por separado
    for category in hazard_categories:
        print(f"\nProcesando categoría: {category}")
        
        # Filtrar por categoría de peligro
        category_df = df[df['Hazard Potential Classification'] == category]
        print(f"  Número de represas en esta categoría: {len(category_df)}")
        
        # Seleccionar características numéricas relevantes
        numeric_features = get_numeric_features(category_df)
        print(f"  Características numéricas seleccionadas: {numeric_features}")
        
        if len(numeric_features) < 2:
            print(f"  No hay suficientes características numéricas para la categoría {category}")
            category_df['Anomaly_Score'] = np.nan
            results.append(category_df[['NID ID', 'Dam Name', 'Hazard Potential Classification', 'Anomaly_Score']])
            continue
        
        # Extraer y preprocesar datos
        X = category_df[numeric_features].copy()
        
        # Escalar datos
        scaler = StandardScaler()
        X_scaled = scaler.fit_transform(X.fillna(X.mean()))
        
        # Ajustar contaminación basado en el tamaño del conjunto de datos
        actual_contamination = min(contamination, 0.2)  # Máximo 20% de anomalías
        
        # Entrenar modelo Isolation Forest
        model = IsolationForest(
            contamination=actual_contamination,
            random_state=42,
            n_jobs=-1  # Usar todos los núcleos disponibles
        )
        
        # Entrenar el modelo y predecir anomalías
        model.fit(X_scaled)
        
        # Obtener puntuaciones de anomalía (valores más negativos = más anómalos)
        anomaly_scores = model.decision_function(X_scaled)
        
        # Normalizar puntuaciones para facilitar la interpretación (0-1, donde 0 = más anómalo)
        normalized_scores = (anomaly_scores - anomaly_scores.min()) / (anomaly_scores.max() - anomaly_scores.min())
        normalized_scores = 1 - normalized_scores  # Invertir para que valores más altos = más anómalos
        
        # Añadir puntuaciones al DataFrame
        category_df = category_df.copy()
        category_df['Anomaly_Score'] = normalized_scores
        
        # Añadir etiqueta de anomalía (-1 para anomalías, 1 para normales)
        category_df['Is_Anomaly'] = model.predict(X_scaled)
        category_df['Is_Anomaly'] = category_df['Is_Anomaly'].map({1: 0, -1: 1})  # Convertir a 0 (normal) y 1 (anomalía)
        
        # Identificar top anomalías
        n_top = min(10, len(category_df))
        top_anomalies = category_df.sort_values('Anomaly_Score', ascending=False).head(n_top)
        
        print(f"  Top {n_top} anomalías detectadas en la categoría {category}:")
        for idx, row in top_anomalies.iterrows():
            print(f"    - {row['Dam Name']} (ID: {row['NID ID']}): Score {row['Anomaly_Score']:.4f}")
        
        # Guardar resultados
        results.append(category_df[['NID ID', 'Dam Name', 'State', 'Hazard Potential Classification', 
                                    'Anomaly_Score', 'Is_Anomaly']])
    
    # Combinar resultados
    final_results = pd.concat(results)
    print(f"\nProceso completado. {len(final_results)} represas analizadas.")
    
    return final_results

# Ejecutar la detección de anomalías
if not df.empty:
    anomaly_results = detect_anomalies_by_hazard(df)
    
    # Fusionar los resultados con el DataFrame original
    df_with_anomalies = df.merge(
        anomaly_results[['NID ID', 'Anomaly_Score', 'Is_Anomaly']], 
        on='NID ID', 
        how='left'
    )
    
    # Guardar resultados
    try:
        df_with_anomalies.to_csv('represas_con_anomalias.csv', index=False)
        print("Resultados guardados en 'represas_con_anomalias.csv'")
    except Exception as e:
        print(f"Error al guardar resultados: {e}")
    
    # Mostrar estadísticas de anomalías por categoría
    print("\nEstadísticas de anomalías por categoría de peligro:")
    anomaly_stats = df_with_anomalies.groupby('Hazard Potential Classification')['Is_Anomaly'].agg(['sum', 'count'])
    anomaly_stats['porcentaje'] = (anomaly_stats['sum'] / anomaly_stats['count'] * 100).round(2)
    print(anomaly_stats)
2 Likes