python -m venv venvvenv\Scripts\activateIf you get a script execution error, run:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserThen activate the environment.
pip install -r requirements.txtThis step downloads the monthly CSV files for both datasets:
https://dados.ons.org.br/dataset/restricao_coff_eolica_usihttps://dados.ons.org.br/dataset/restricao_coff_eolica_detail
Choose the desired date range and execute the script using the following format:
python src/extract/download_data.py --start YYYY-MM --end YYYY-MMpython src/extract/download_data.py --start 2025-10 --end 2026-03This command downloads all monthly files between October 2025 and March 2026 for both datasets.
Files are saved in:
- data/raw/wind_farm/
- data/raw/spe/
This step consolidates all monthly CSV files into unified datasets.
Execute the following command from the project root:
python src/transform/consolidate_data.pyFiles are saved in:
- data/interim/spe_consolidated.csv
- data/interim/wind_farm_consolidated.csv
This step is responsible for ensuring the reliability and consistency of the consolidated datasets. The pipeline performs data validation, cleansing, and generates a structured data quality report.
Execute the following command from the project root:
python src/transform/data_quality.pyFiles are saved in:
- data/processed/spe_clean.csv
- data/processed/wind_farm_clean.csv
- data/reports/data_quality_report.json
This step filters the dataset to keep only SPEs belonging to Casa dos Ventos and their associated wind farm complexes.
Execute the following command from the project root:
python src/transform/filter_cdv.pyFiles are saved in:
- data/filtered/spe_cdv_filtered.csv
- data/filtered/wind_cdv_filtered.csv
The SPE detail dataset and the Wind Farm dataset were joined using a business key based on the wind complex name.
Although both datasets contain the id_ons column, the field represents different hierarchical entities:
- In the SPE dataset,
id_onsidentifies the individual SPE. - In the Wind Farm dataset,
id_onsidentifies the wind complex.
Because of this semantic mismatch, id_ons was not used as the join key.
After investigating the available columns, the logical relationship between datasets was identified as:
spe.nom_conjuntousina ↔ wind.nom_usina
Example:
Conj. Paulino Neves ↔ CONJ. PAULINO NEVES
To guarantee matching consistency, both fields were normalized by:
- converting text to uppercase
- trimming spaces
- removing duplicated spaces
A LEFT JOIN strategy was used, preserving the SPE dataset as the primary granular layer.
This approach guarantees that:
- all SPE records remain in the final dataset
- Wind Farm information is added whenever a valid match exists
The final dataset therefore keeps the original SPE granularity while enriching records with aggregated Wind Farm attributes.
The Wind Farm dataset contains multiple temporal records for the same wind complex. To avoid a many-to-many merge explosion and excessive memory usage, duplicates were removed before the join using the normalized business key.
The following columns from the Wind Farm dataset were incorporated into the final joined dataset:
nom_subsistemanom_estadoval_geracaoval_disponibilidadeval_geracaoreferenciaval_geracaoreferenciafinalcod_razaorestricao
Operational restriction fields such as:
val_geracaolimitadacod_origemrestricaodsc_restricao
were intentionally excluded from the final dataset to keep the model focused on generation and availability metrics.
Potential merge losses may occur due to:
- naming inconsistencies between datasets
- missing or malformed wind complex names
- unmatched normalized keys
Join quality metrics were calculated to validate merge coverage and identify unmatched records.
Execute the following command from the project root:
python src/transform/join_spe_wind.pyFile is saved in:
- data/processed/cdv_spe_wind_joined.csv
The final consolidated dataset was persisted in Parquet format using partitioning by:
yearmonth
Both columns were extracted from din_instante.
The dataset was partitioned by time (year/month) because the data represents time-series energy generation records.
This strategy improves performance for queries filtered by temporal windows, such as:
- monthly analysis
- yearly reports
- historical comparisons
Execute the following command from the project root:
python src/load/save_parquet.pyThe partitioned Parquet dataset will be saved to:
- data/final_parquet/
Example structure:
data/final_parquet/
├── year=2025/
│ ├── month=10/
│ ├── month=11/
│ └── month=12/
│
└── year=2026/
├── month=1/
├── month=2/
└── month=3/
- Data is sourced from ONS public datasets (wind farms and SPEs) and assumed to be accurate.
- Only SPEs belonging to Casa dos Ventos are included in the final dataset.
id_onscolumns in the two datasets represent different levels of hierarchy, the join uses normalized names (nom_conjuntousina↔nom_usina) to ensure consistency.- A LEFT JOIN preserves SPE granularity while enriching with wind farm attributes.
- Duplicates in wind farm data are removed to prevent many-to-many merge issues.
- Partitioning by
yearandmonthin Parquet files improves query performance for temporal analysis.
A dimensional model following the Star Schema approach was designed for the constrained-off wind generation domain.
The model separates:
- Fact table → generation and operational metrics
- Dimension tables → descriptive business context
The fact table stores the wind generation measurements and operational metrics.
The chosen granularity is:
One record per SPE per timestamp (
din_instante).
This granularity preserves the original detail level from the SPE dataset and allows temporal analysis at the individual wind plant level.
The following metrics were included:
val_ventoverificadoval_geracaoestimadaval_geracaoverificadaval_geracaoval_disponibilidadeval_geracaoreferenciaval_geracaoreferenciafinalcod_razaorestricao
The fact table contains the following foreign keys:
spe_keyconjunto_keytempo_key
Stores descriptive information about individual SPEs.
spe_key(Primary Key)nom_usinaid_onscegprojetonom_modalidadeoperacao
Stores information about wind complexes/conjuntos.
conjunto_key(Primary Key)nom_conjuntousinaid_subsistemanom_subsistemaid_estadonom_estado
Stores temporal attributes extracted from din_instante.
tempo_key(Primary Key)din_instanteanomesdiahora
The dimensional model relationships are:
fact_generation.spe_key→dim_spe.spe_keyfact_generation.conjunto_key→dim_conjunto.conjunto_keyfact_generation.tempo_key→dim_tempo.tempo_key
Execute the following command from the project root:
python src/modeling/build_star_schema.pyThe generated dimensional model tables will be saved to:
- data/warehouse/
Structure:
data/
└── warehouse/
├── dimensions/
│ ├── dim_spe.parquet
│ ├── dim_conjunto.parquet
│ └── dim_tempo.parquet
│
└── facts/
└── fact_generation.parquet
The fact table granularity was defined as:
One record per SPE per timestamp (
din_instante).
This granularity was chosen because the SPE dataset already represents the most detailed operational level available in the source data.
Using this level of detail allows:
- temporal analysis of wind generation
- comparisons between SPEs
- project-level aggregations
- future analytical flexibility without losing information
It also preserves the original business semantics of the ONS detailed dataset.
At the current scope of the project, there is no strong need for Slowly Changing Dimensions.
The descriptive attributes used in the dimensions are relatively stable, such as:
- project
- SPE
- conjunto
- subsystem
- state
However, in a production-grade environment, some dimensions could eventually require:
Especially for attributes that may change historically over time, such as:
- project ownership
- operational classification
- regional organization
SCD Type 2 would preserve historical versions of the records while maintaining analytical consistency over time.
For this project, dimensions were implemented as static snapshots.
A small amount of denormalization was intentionally applied in the dimensional model.
For example:
- subsystem information
- state information
were kept directly inside dim_conjunto.
This decision was made because:
- these attributes have low cardinality
- they rarely change
- it simplifies analytical queries
- it reduces unnecessary joins
The goal was to prioritize simplicity and query performance while maintaining a clean star schema structure.
The dimensional model was designed to:
- preserve the SPE-level analytical granularity
- optimize BI and aggregation queries
- reduce redundancy in the fact table
- simplify analytical exploration
- follow common Data Warehouse best practices
The dimensional model was implemented programmatically using:
- Python
- Pandas
- Parquet
The implementation consumes the consolidated dataset generated in Part 1 and transforms it into a Star Schema structure composed of:
- dimension tables
- fact table
The modeling process uses the following dataset:
data/processed/cdv_spe_wind_joined.csv
dim_spe.parquetdim_conjunto.parquetdim_tempo.parquet
Saved in:
- data/warehouse/dimensions/
fact_generation.parquet
Saved in:
- data/warehouse/facts/
- Star Schema was chosen to optimize analytical queries.
- Fact table granularity is SPE per timestamp to preserve operational detail for temporal analysis.
cod_razaorestricaois included in the fact table to allow restriction-level aggregations.- All IDs and keys are surrogate keys to maintain referential integrity and simplify joins.
In this stage, the original scripts from the first part were refactored into a modular and robust ELT pipeline following software engineering best practices.
The pipeline was designed with clear separation between extraction, loading, and transformation layers, using DuckDB as the analytical processing engine.
The pipeline follows the ELT approach:
Extract → Load → Transform
Raw CSV files are downloaded directly from the public AWS S3 bucket provided by ONS.
The raw files are loaded into DuckDB without prior transformation.
All transformations are executed inside DuckDB using SQL.
src/
│
├── extract/
│ └── extract_data.py
│
├── load/
│ └── load_duckdb.py
│
├── transform/
│ ├── transform_duckdb.py
│ ├── quality_report.py
│ ├── filter_cdv_duckdb.py
│ ├── join_data.py
│ └── export_parquet.py
│
├── utils/
│ └── logger.py
│
└── pipeline.py
Run the pipeline with:
python -m src.pipelineDownloads monthly files for both datasets:
- Wind farm dataset
- SPE detail dataset
Features implemented:
- Retry mechanism for download failures
- Informative logging
- Skip download if file already exists
Loads all raw CSV files into DuckDB tables. The loading process consolidates all monthly files automatically.
Transformations are executed directly inside DuckDB.
A detailed quality report is generated and is saved in:
- data/reports/data_quality_report_pipeline.json
The pipeline filters only Casa dos Ventos SPEs using the mapping file:
spes_casa_dos_ventos.csv
The SPE dataset is related to the wind farm dataset using the logical business key:
nom_conjuntousinanom_usinadin_instante
Join type:
LEFT JOIN
The final dataset is exported in partitioned Parquet format.
Partition strategy:
yearmonth
Output is saved in:
- data/final_parquet_pipeline/
The pipeline can be safely re-executed without duplicating data because:
- Existing files are not downloaded again
- DuckDB tables use
CREATE OR REPLACE - Final parquet export overwrites previous partitions safely
The project uses Python's logging module.
Implemented log levels:
- INFO
- WARNING
- ERROR
Logs are saved to:
- logs/pipeline.log
The pipeline implements:
- Retry for download failures
- Informative error messages
Pipeline parameters are centralized in:
- config/settings.py
In a production environment, this pipeline could be orchestrated with the main goal of automating execution, monitoring, retries, scheduling, and failure notifications.
A possible production architecture would be:
Cloud Scheduler / Airflow
↓
Pipeline Trigger
↓
Extract Layer
↓
DuckDB Load
↓
SQL Transformations
↓
Data Quality Validation
↓
Final Parquet Export
↓
Data Lake / Storage
Using Apache Airflow, the pipeline could be separated into independent tasks:
-
extract_task- Download monthly CSV files
-
load_task- Load raw files into DuckDB
-
transform_task- Execute SQL transformations
-
quality_task- Generate quality report
-
filter_task- Filter Casa dos Ventos assets
-
join_task- Join datasets
-
export_task- Export partitioned parquet files
Task dependencies would guarantee the correct execution order.
The pipeline could run automatically:
- Daily
- Weekly
- Monthly
For the ONS constrained-off datasets, a monthly schedule would likely be sufficient.
The orchestration layer would provide:
- Automatic retries
- Failure alerts
- Execution logs
- Task monitoring
- Dependency management
This improves reliability and operational visibility.
Although the current project uses local DuckDB processing, the architecture could be adapted to cloud environments using:
- Amazon S3
- Google Cloud Storage
- BigQuery
- Databricks
This orchestration strategy was chosen because it provides:
- Modular execution
- Easier maintenance
- Better observability
- Automatic scheduling
- Fault tolerance
- Scalability for larger datasets
- DuckDB was chosen for in-memory analytical processing to efficiently handle the CSV datasets.
- ELT approach (Extract → Load → Transform) ensures raw data is preserved and transformations are reproducible.
cod_razaorestricaois preserved through transformations and joins to enable restriction-level analysis downstream.- All transformations are performed in SQL to leverage DuckDB performance.
- Idempotency is ensured by using
CREATE OR REPLACEand partitioned Parquet exports, allowing safe re-execution. - Logging and error handling are centralized to improve observability and operational monitoring.
- Partitioning by
yearandmonthoptimizes query performance for time-series analytics.
This stage of the project focuses on ensuring the reliability, consistency, and integrity of the data.
An automated validation pipeline was implemented to verify:
- Schema consistency
- Data freshness
- Business rules
- Timestamp continuity
- Data completeness
The validations are executed over the partitioned parquet dataset stored in:
- data/final_parquet_pipeline/
The pipeline generates validation logs and a JSON report containing all validation results.
src/
└── validation/
├── __init__.py
└── validate_pipeline.py
Generated outputs:
logs/
└── validate_pipeline_YYYYMMDD_HHMMSS.log
data/
└── validation_reports/
└── validation_report.json
The pipeline verifies whether:
- All expected columns exist
- Column data types are correct
The pipeline validates whether the latest expected month is present in the dataset.
The following business rules were implemented:
- Power generation cannot be negative
- Wind speed must be between 0 and 40 m/s
- Generation cannot exceed reference generation
- Duplicate timestamps are not allowed
- Projects cannot be null
- Timestamp continuity is verified for each project
The pipeline checks for unexpected timestamp gaps for each project.
The pipeline calculates the percentage of expected timestamps versus received timestamps for each project.
Projects below the configured threshold are automatically flagged.
Configured threshold:
threshold = 95
Run the validation pipeline with:
python -m src.validation.validate_pipelineAfter execution, the pipeline automatically generates:
- data/validation_reports/validation_report.json
This report contains:
- Schema validation results
- Freshness validation
- Business rule violations
- Timestamp continuity analysis
- Completeness percentages
- Threshold alerts
- Python
- Pandas
- PyArrow
- JSON
- Logging
- Validation pipeline focuses on data reliability, consistency, and completeness for analytical use.
- Partitioned Parquet datasets enable efficient validation over time-series data.
- Validations are modular (schema, freshness, business rules, timestamp continuity, completeness) to simplify maintenance.
- Logging and JSON reports improve traceability, observability, and reproducibility.
- The pipeline is idempotent, it can be re-run without affecting the original dataset.
- Design assumes hourly frequency in timestamps, gaps are considered anomalies.
In this stage, the processed and modeled datasets are exposed through a REST API built with FastAPI, enabling external consumption of analytical results.
The API provides endpoints for querying wind generation data, project metadata, and restriction summaries.
The API was built using:
- FastAPI
- Pydantic (validation)
- Pandas (data processing)
- Parquet (data source)
FastAPI was chosen due to:
- Automatic Swagger/OpenAPI documentation
- High performance
- Built-in validation system
To start the API locally:
uvicorn src.api.app:app --reloadAfter starting the service, access:
http://127.0.0.1:8000/docs
FastAPI automatically generates:
- Swagger UI
- OpenAPI schema
- Interactive endpoint testing
src/api/
│
├── app.py
├── routes.py
├── services.py
└── schemas.py
Returns API health status.
Returns available wind generation projects with metadata.
Returns aggregated generation data for a project.
- project_id (required)
- start_date (optional)
- end_date (optional)
- frequency: daily | monthly
Returns a summary of generation restrictions grouped by restriction reason (cod_razaorestricao).
- project_id
- start_date
- end_date
- Swagger UI at
/docs - OpenAPI schema generated automatically
- Path and query parameters validated with Pydantic
- Invalid inputs return HTTP 400
- 404 for missing projects
- 400 for invalid parameters
- Safe handling of null values
The API reads from:
- data/final_parquet_pipeline/
Generated from previous pipeline stages.
- The API exposes only the pre-processed and modeled datasets.
- Null values in cod_razaorestricao can be categorized as "without restriction".
- Aggregations for generation and restrictions are performed at query-time using Pandas.
- Swagger/OpenAPI documentation ensures discoverability and testing without external tools.
- All business logic validation is done in the pipeline stage, the API serves read-only data.
- The design follows a modular structure to allow future extension (new endpoints or filters).
This section documents the architecture, technical decisions, and potential cloud evolution for the project.
- DuckDB: Chosen for fast, local analytical processing with Parquet support, simpler than full-fledged databases for this dataset size;
- Parquet Partitioning: Partitioned by year and month to optimize time-based queries;
- Left Join Strategy: Preserves all SPE records, enriches with wind farm data, avoids data loss;
- Star Schema: Fact table at SPE-timestamp granularity, dimensions for descriptive context, balances query performance and simplicity;
- FastAPI: Provides REST API with automatic Swagger/OpenAPI documentation and easy validation;
- Validation Pipeline: Automated checks for schema, completeness, and business rules to ensure data reliability.
To scale and deploy this solution in production:
-
Compute Layer
- Use GCP Cloud Run or AWS Fargate to host the API and ETL pipeline.
- Containerized Python environment for portability.
-
Storage
- Move final Parquet datasets to Google Cloud Storage (GCS), replacing the local
data/folder.
- Move final Parquet datasets to Google Cloud Storage (GCS), replacing the local
-
Data Warehouse
- Load Parquet into BigQuery for fast analytical queries and aggregation.
-
Orchestration
- Schedule the ETL pipeline with Cloud Scheduler or Airflow in Cloud Composer.
- Monitor logs, retries, and failure notifications automatically.
-
API Layer
- Cloud Run exposes FastAPI endpoints.
- Can connect directly to BigQuery for live queries if the dataset grows beyond local DuckDB feasibility.
-
Monitoring & Alerts
- Use Cloud Monitoring and Cloud Logging to track ETL runs, API health, and data freshness.
- Alert for failures or schema drift.
This approach allows scaling with dataset growth, ensures fault tolerance, and simplifies operational management in the cloud.
