Análisis de datos del mercado eléctrico español con archivos I90
Descarga, procesa y analiza archivos I90 de REE con Python — tendencias de curtailment, mix de generación y correlación con precios del mercado eléctrico español.
Los archivos I90 son el conjunto de datos públicos más completo para entender el mercado eléctrico español a nivel de unidad de programación. Contienen los programas diarios de generación, las restricciones en tiempo real y los datos de curtailment publicados por REE (Red Eléctrica de España). Sin embargo, la mayoría de analistas siguen descargándolos manualmente desde la web de ESIOS y lidiando con su formato Excel de múltiples hojas.

En este artículo automatizaremos todo el proceso — desde la descarga de archivos I90 a través de la API de ESIOS hasta la generación de visualizaciones interactivas de patrones de curtailment, mix de generación y correlaciones con precios. Usamos cinco semanas de datos para que la ejecución sea rápida, pero el mismo código escala a cualquier rango de fechas. Utilizaremos python-esios para el acceso a la API y plotly para los gráficos.
Preguntas
- ¿Cómo descargar archivos I90 programáticamente desde la API de ESIOS?
- ¿Cómo parsear la estructura Excel de múltiples hojas en DataFrames listos para análisis?
- ¿Cuáles son las tendencias de curtailment por tecnología?
- ¿Cómo se correlaciona el curtailment con los precios de la electricidad?
- ¿Qué unidades de programación son las más afectadas?
Descargar archivos I90
Los archivos I90 están disponibles a través de la API de archivos de ESIOS. El archivo I90DIA (datos diarios de unidades de programación) tiene el ID de archivo 34:
from esios import ESIOSClient
client = ESIOSClient()
archive = client.archives.get(34)
print(f"Archive: {archive.name} (ID: {archive.id})") Archive: I90DIA (ID: 34)Descarguemos cinco semanas de datos. El método download() itera día a día y cachea cada archivo en local — volver a ejecutar es instantáneo. Devuelve la lista de rutas de archivos descargados:
START_DATE = "2025-05-05"
END_DATE = "2025-06-08"
xls_files = archive.download(start=START_DATE, end=END_DATE) Para descargar un año completo, simplemente cambia el rango de fechas a start="2025-01-01", end="2025-12-31". La caché hace que solo descargues cada día una vez.
Se descargaron 35 archivos I90.
Parsear con I90Book
Cada archivo I90 es un libro Excel con múltiples hojas — programas de generación (PBF), restricciones en tiempo real, curtailment, y más. La clase I90Book de python-esios se encarga del parseo:
from esios.processing.i90 import I90Book
# Parsear un solo archivo para explorar la estructura
book = I90Book(xls_files[0])
Las hojas clave para nuestro análisis:
- I90DIA26: PBF — Programa Base de Funcionamiento (generación programada por unidad)
- I90DIA03: Restricciones en el Mercado Diario — restricciones del mercado diario (curtailment)
- I90DIA08: Restricciones en Tiempo Real — restricciones en tiempo real
Extraigamos los datos de curtailment de la hoja I90DIA03:
sheet = book["I90DIA03"]
df_sample = sheet.df
El DataFrame tiene un índice multinivel (Unidad de Programación, Sentido, tipo de restricción, etc.) y un DatetimeIndex con resolución horaria. La columna value contiene la energía en MWh.
Construir el dataset completo
Parseemos todos los archivos y concatenémoslos en un solo DataFrame:
all_curtailment = []
for xls_path in xls_files:
try:
book = I90Book(xls_path)
sheet = book["I90DIA03"]
df = sheet.df
if not df.empty:
all_curtailment.append(df.reset_index())
except Exception:
continue # Algunos días pueden tener variaciones de formato
df_curtailment = pd.concat(all_curtailment, ignore_index=True) La hoja I90DIA03 contiene ajustes de restricciones tanto de Subir como de Bajar. El curtailment — energía que podría haberse generado pero no se generó — corresponde a la dirección Bajar.
También necesitamos excluir el tipo de redespacho ECO, que representa redespacho económico (reequilibrio basado en mercado) y no curtailment técnico:
df_curtailment = df_curtailment[
(df_curtailment["Sentido"] == "Bajar")
& (~df_curtailment["Redespacho"].str.startswith("ECO"))
].copy()
df_curtailment["value"] = df_curtailment["value"].abs() Tamaño del dataset: 46.560 filas, 246 unidades de programación únicas, cubriendo del 2025-05-05 al 2025-06-08.
Análisis de curtailment
Tendencias diarias de curtailment
Agreguemos el curtailment por día para ver cómo varía a lo largo de las semanas:

Curtailment por tecnología
Los códigos de unidad del I90 por sí solos no indican la tecnología — se necesita el registro de unidades de programación de REE. Descarga el CSV desde esios.ree.es/en/programming-units y guárdalo como data/programming_units.csv. Luego cárgalo como tabla de consulta y mapea los tipos de producción detallados en categorías simplificadas:
df_units = pd.read_csv("data/programming_units.csv", sep=";")
unit_tech = df_units.set_index("UP Code")["Production Type"]
TECH_MAP = {
"Onshore wind": "Wind",
"Offshore wind": "Wind",
"Solar PV": "Solar PV",
"Solar thermal": "Solar thermal",
"Hydro UGH": "Hydro",
"Hydro non UGH": "Hydro",
"Turbine pumping": "Hydro",
"Pump consumption": "Hydro",
"Nuclear": "Nuclear",
"Combined cycle GT": "Gas CCGT",
"Natural Gas": "Gas CCGT",
"Natural Gas Cogeneration": "Cogeneration",
"Biogas": "Cogeneration",
"Biomass": "Cogeneration",
"Oil products and coal": "Other",
"Soft coal/Anthracite": "Other",
"Sub-bituminous coal": "Other",
"Fuel": "Other",
"Mining subproducts": "Other",
"Sundry waste": "Cogeneration",
"Household and similar wastes": "Cogeneration",
"Storage": "Other",
"Hybrid renewable-storage": "Other",
"Renewable hybrid": "Solar PV",
"Renewable-renewable-thermal hybrid": "Cogeneration",
"Residual energy": "Cogeneration",
"Geothermal and Ocean": "Other",
}
def classify_technology(unit_code: str) -> str:
prod_type = unit_tech.get(unit_code, "Unknown")
return TECH_MAP.get(prod_type, "Other") 
Mapa de calor horario de curtailment
La información más valiosa: ¿cuándo se produce el curtailment? Un mapa de calor de hora × día revela el patrón intradiario:

El mapa de calor muestra cuándo el operador del sistema impone más curtailment. El curtailment solar se concentra en las horas centrales del día (10:00–16:00), mientras que el eólico se reparte por la noche cuando la demanda es más baja. Con un año completo de datos se verían patrones estacionales claros.
Mix de generación desde el PBF
El PBF (Programa Base de Funcionamiento) en la hoja I90DIA26 muestra la generación programada para cada unidad. Parseémoslo para entender el mix de generación:
all_pbf = []
for xls_path in xls_files:
try:
book = I90Book(xls_path)
sheet = book["I90DIA26"]
df = sheet.df
if not df.empty:
all_pbf.append(df.reset_index())
except Exception:
continue
df_pbf = pd.concat(all_pbf, ignore_index=True) 
Correlación con precios
¿Aumenta el curtailment cuando bajan los precios? Superpongamos los precios del mercado diario de OMIE con los volúmenes de curtailment. Obtendremos el indicador 600 (precio del mercado diario para España) de la API de ESIOS:
prices = client.indicators.get(600)
df_prices = prices.historical(START_DATE, END_DATE, geo_ids=[3]) # España (geo_id=3) Calculemos las medias diarias de curtailment y precios y representémoslas juntas:

El patrón es visible: los días con mayor producción renovable (y por tanto más curtailment) tienden a tener precios más bajos. Con un año completo de datos se observaría una correlación negativa más clara — una alta generación renovable empuja el curtailment al alza y los precios a la baja.
Unidades de programación más afectadas
¿Qué unidades específicas soportan más curtailment? Esto es crucial para inversores que evalúan proyectos renovables:

La unidad con más curtailment en este periodo es FGASNE2 (Solar PV) con 11.470 MWh. A un precio medio capturado de 30 €/MWh, esto representa aproximadamente 344 k€ en ingresos perdidos — en solo cinco semanas.
Conclusiones
Hemos construido un pipeline completo de análisis de datos del mercado eléctrico español usando archivos I90:
- Descarga automatizada con
python-esios— sin necesidad de navegar manualmente por la web de ESIOS - Parseo estructurado con
I90Book— gestiona el formato Excel de múltiples hojas - Análisis con pandas — groupby, pivot y merge para datos de curtailment y generación
- Visualizaciones interactivas con Plotly — mapas de calor, barras apiladas y gráficos de doble eje
Hemos usado cinco semanas para mantener la ejecución manejable, pero el mismo código funciona para cualquier rango de fechas — solo hay que cambiar START_DATE y END_DATE. Con un año completo se verían patrones estacionales en el mapa de calor y rankings más representativos de las unidades más afectadas.
Artículos relacionados
- Automatizar el preprocesamiento de archivos Excel I90 — enfoque paso a paso de preprocesamiento manual
- Construir una base de datos relacional para archivos I90 — almacenar datos I90 en una base de datos normalizada
- Tutorial de la librería python-esios — guía completa de la librería python-esios
- Obtener precios de electricidad de OMIE — datos de precios directamente de OMIE