Uruguay 2025:

Proyecto de documentación “Data Report” Uruguay 2025 recorrido ambiental.

Institucional
Author

Mara Destéfanis

Published

December 12, 2025

Análisis y categorización de denuncias ambientales en Uruguay.

En el sitio oficial de la República Oriental del Uruguay en la sección de Catálogo Nacional de Datos Abiertos se comparten dos dataset con la recopilación de denuncias ambientales en Uruguay.

Code
#| echo: false
#| warning: false
#| message: false
#| output: false

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

import plotly.express as px
import plotly.graph_objects as go

import geopandas as gpd
from statsmodels.tsa.seasonal import seasonal_decompose

from pathlib import Path
from IPython.display import display, Markdown
  1. El conjunto de datos presente en el análisis proviene de denuncias ambientales recibidas por el Ministerio de Ambiente de Uruguay. Los datos presentados son dos datasets abiertos ubicados en la página: catalogodatos.gub.uy correspondiente a dos períodos: desde 1010-2019 y 2023. Los datos sufrieron una interrupción durante el período de pandemia. La última actualización registrada por el sitio data de Julio 2024. Hasta la fecha de esta publicación no se han actualizado en catalogodatos información de denuncias ambientales más recientes.

Durante el proceso se realizó una limpieza y perfilado de datos, exploración y visualización. Los códigos se comparten en el respositorio github de Dempathy_Project para colaboradores aunque también visibles en este mismo espacio.

Para unificar los dataset se tuvieron en cuenta unicamente las variables coincidentes y se agregaron nuevas variables categóricas numéricas para el reconocimiento del tipo de denuncia ambiental.

Finalmente, los datos se conforman de tres data sets:

  1. Denuncias ambientales desde el año 2010 al 2019.

  2. Denuncias ambientales 2023

  3. Tabla de Motivos construido para unificar categorias de los motivos de las denuncias.

    Code
    print(amb_uy.head(10))
    Code
    amb_uy["departamento"].value_counts().head(10).plot(kind="bar")
    plt.title("Top 10 departamentos con denuncias")
    plt.tight_layout()
    plt.show()
Code
#| echo: false
#| warning: false
#| message: false

denuncias_motivos = pd.merge(amb_uy, motivos, on="motivo_1")
display(denuncias_motivos.info())
Code
#| echo: false
#| warning: false
#| message: false
#| output: false

# Transformación para exploración posterior. 

# Variable departamento mapeo - procesamiento para que coincidas con shapefile(GeoJSON)

# Reemplazos:
map_departamentos = {
    "San Jose": "San José",
    "Treinta y tres": "Treinta y Tres",
    "Treinta Y Tres": "Treinta y Tres",
    "Río Negro": "Río Negro",
    "Rio Negro": "Río Negro",
    "Canelones, Florida, Lavalleja": np.nan,
    "No aplica": np.nan,
    "N/A - (No aplica)": np.nan,
    "No especifica": np.nan
}

denuncias_motivos["departamento"] = denuncias_motivos["departamento"].str.strip()
denuncias_motivos["departamento"] = denuncias_motivos["departamento"].replace(map_departamentos)

#  formato estándar de título
denuncias_motivos["departamento"] = denuncias_motivos["departamento"].str.title()

# Corrección acentos
denuncias_motivos["departamento"] = denuncias_motivos["departamento"].replace({
    "San Jose": "San José",
    "Rio Negro": "Río Negro",
    "Treinta Y Tres": "Treinta y Tres"
})

# Validación final contra la lista oficial
depart_oficiales = [
    "Artigas", "Canelones", "Cerro Largo", "Colonia", "Durazno", "Flores",
    "Florida", "Lavalleja", "Maldonado", "Montevideo", "Paysandú",
    "Río Negro", "Rivera", "Rocha", "Salto", "San José",
    "Soriano", "Tacuarembó", "Treinta y Tres"
]

denuncias_motivos["departamento"] = denuncias_motivos["departamento"].where(
    denuncias_motivos["departamento"].isin(depart_oficiales),
    np.nan
)

# Tipo de datos

denuncias_motivos["motivo_1"] = denuncias_motivos["motivo_1"].astype("category")
denuncias_motivos["departamento"] = denuncias_motivos["departamento"].astype("category")
denuncias_motivos['fecha_denuncia'] = pd.to_datetime(
    denuncias_motivos['fecha_denuncia'],
    errors='coerce'
)

Evolución temporal de denuncias ambientales

Code
#| echo: false
#| warning: false
#| message: false

#| label: denuncias-por-año
#| fig-cap: "Evolución temporal de denuncias ambientales"
#| fig-width: 10
#| fig-height: 6
#| fig-align: center


fig, ax = plt.subplots(figsize=(10, 6))

denuncias_por_año.plot(
    kind='bar',
    ax=ax,
    color=palette,
    edgecolor='black'
)

ax.set_title('Cantidad de denuncias por año', fontsize=14, fontweight='bold')
ax.set_xlabel('Años de las denuncias', fontsize=12)
ax.set_ylabel('Cantidad de denuncias, nros absolutos', fontsize=12)
ax.grid(axis='y', alpha=0.3)

fig.tight_layout()
plt.show()

Tabla de frecuencias por departamento

Denuncias mensuales - últimos 5 años -

Tendencia temporal suavizada de denuncias ( media móvil 30 días)

Code
# Asegura datetime
denuncias_motivos["fecha_denuncia"] = pd.to_datetime(
    denuncias_motivos["fecha_denuncia"],
    errors="coerce"
)

# Serie diaria con frecuencia explícita
serie_diaria = (
    denuncias_motivos
    .set_index("fecha_denuncia")
    .resample("D")
    .size()
)

# Media móvil de 30 días (temporalmente correcta)
media_movil_30 = serie_diaria.rolling(
    window=30,
    min_periods=15
).mean()

# Gráfico explícito para Quarto
fig, ax = plt.subplots()

serie_diaria.plot(
    ax=ax,
    alpha=0.3,
    label="Denuncias diarias"
)

media_movil_30.plot(
    ax=ax,
    linewidth=2,
    label="Media móvil 30 días"
)

ax.set_title("Tendencia temporal suavizada")
ax.set_xlabel("Fecha")
ax.set_ylabel("Cantidad de denuncias")
ax.legend()

fig.tight_layout()
plt.show()

Cantidad total de denuncias por motivo

Perfil estacional agregado,representa patrones mensuales acumulados

Heatmap motivos conteo absoluto por departamento No es comparable, solo referencial, entendiendo que hay departamentos con más población

Denuncias ambientales por departamento en Uruguay

Code
import math
import matplotlib.pyplot as plt

# ---------------------------
# Top 5 motivos más denunciados
# ---------------------------
top5_motivos = (
    denuncias_motivos[motivos_binarios]
    .sum()
    .sort_values(ascending=False)
    .head(5)
    .index
)

# ---------------------------
# Configuración del faceteado
# ---------------------------
n = len(top5_motivos)
cols = 3
rows = math.ceil(n / cols)

fig, axes = plt.subplots(rows, cols)
axes = axes.flatten()

# ---------------------------
# Loop de mapas
# ---------------------------
for i, motivo in enumerate(top5_motivos):

    df_motivo = denuncias_motivos[denuncias_motivos[motivo] == 1]

    conteo_dep = (
        df_motivo
        .groupby('departamento')
        .size()
        .reset_index(name='Cantidad')
    )

    conteo_dep['departamento'] = conteo_dep['departamento'].str.upper()
    conteo_dep['Proporcion'] = conteo_dep['Cantidad'] / conteo_dep['Cantidad'].sum()

    mapa_data = uruguay_map.merge(
        conteo_dep,
        left_on='Departamento',
        right_on='departamento',
        how='left'
    )

    mapa_data['Proporcion'] = mapa_data['Proporcion'].fillna(0)

    mapa_data.plot(
        column='Proporcion',
        ax=axes[i],
        cmap='YlOrRd',
        edgecolor='white',
        linewidth=0.6,
        legend=True if i == 0 else False,
        legend_kwds={
            'label': 'Proporción de denuncias',
            'shrink': 0.6,
            'format': '%.1%%'
        }
    )

    axes[i].set_title(
        f"{motivo}\nDistribución relativa por departamento",
        fontsize=11,
        weight='bold'
    )
    axes[i].axis('off')

# ---------------------------
# Eliminar ejes vacíos
# ---------------------------
for j in range(i + 1, len(axes)):
    fig.delaxes(axes[j])

plt.tight_layout()
plt.show()