Projects / Research

Wind energy site suitability

I worked on a five-student team in the AI4ALL Ignite fellowship that predicts whether a US wind-resource grid cell already has a wind turbine, as a proxy for site suitability. I built the labeled dataset by joining two public sources, then ran the feature engineering and XGBoost tuning. The train/test split leaks duplicate cells, so I do not report headline scores and treat the project mainly as a lesson in dataset construction.

Problem

Wind developers need to know which locations are worth studying. NREL’s Wind Toolkit (WTK) describes the wind resource on a grid of about 127,000 US cells, but it does not say where turbines exist. The US Wind Turbine Database (USWTDB) lists turbines, but only where they already stand.

There is no ground truth for “suitable”. We used “a turbine exists here today” as the label and asked whether a model can recover it from site attributes. That tests whether the attributes carry signal about siting. It does not prove a site is a good place for a new turbine.

My role

The team (Team CRANBerry) had five students, with two mentors. The repo’s commit history shows three contributors. Commits under my account cover the spatial join and labeled dataset, the logistic regression baseline, the random forest, the feature-engineering study, the XGBoost tuning, and the model-comparison charts. A teammate built the Streamlit app and a third contributed the repo setup and app deployment. I did not write the app.

How it works

USWTDB turbines ──┐
 (lat/lon, one    │ nearest-neighbour join,
  row per turbine)├─ EPSG:3857, 25 km cap ──► turbine -> nearest WTK cell
WTK grid cells ───┘                                   │
 (wind_speed, capacity,                               ▼
  capacity_factor,                  left-merge onto all cells on site_id,
  usable-area fraction, State)      unmatched cells filled with isTurbine=False
                                                      │
                                                      ▼
              features: 4 base + State one-hot + 2 interactions (56 columns)
                                                      │
                          80/20 random split ─► XGBoost (GridSearchCV, ROC-AUC)
                                                      │
                                                      ▼
                                              Streamlit demo

Each turbine is assigned to its nearest WTK cell, with turbines more than 25 km from any cell left unmatched. The cell table is left-joined to those assignments. Cells with no turbine get isTurbine = False.

The baseline models use four WTK attributes: wind speed, capacity factor, potential capacity and usable-area fraction. The final model adds the cell’s US state (one-hot) and two products: wind speed times capacity factor, and capacity times usable-area fraction.

Key decisions

Nearest-cell labeling with a 25 km cap. The alternative was a tight radius around each cell. A turbine’s coordinates rarely land exactly on a grid point, so I snapped each turbine to its nearest cell and capped the distance so offshore or out-of-coverage turbines would not attach to a distant cell. The cap was a judgment call. The notebook says to tune it after inspecting a distance histogram, and I did not find that analysis.

Choosing features by cross-validation on the training split. I built 12 feature sets and scored each with default XGBoost under 5-fold CV. Numeric versions beat binned versions, and the two interaction features added little. Adding one-hot State moved the score the most, and a 7-region grouping did not match it, so I kept State. Latitude and longitude were dropped from the final feature set. Those comparisons used random row splits over the duplicated table described under Limitations, so I read them as a rough ordering, not as measurements.

Class weight and tuning metric. The training rows are about 38% positive. I set scale_pos_weight to the negative-to-positive ratio (1.62) and tuned a 54-point grid (depth, subsample, column sample, and two learning-rate/tree-count pairs) on ROC-AUC with 5-fold CV. I chose ROC-AUC because it does not depend on a decision threshold.

What was hard

Turning two unrelated databases into one labeled table was the real work. The two sources use different identifiers and different coordinate precision, and the WTK covers offshore cells with no state. About 7.5% of training cells have state “Unknown”. A shapefile fallback in the exploration notebook recovered only 1,549 of 11,408 missing states, so State is weak offshore.

The second difficulty was evaluation: the join left duplicate rows that the train/test split did not account for. The Results and Limitations sections cover it.

Results

The saved notebooks report ROC-AUC 0.8473 for the initial four-feature XGBoost model and 0.9545 for the tuned 56-feature model. Both scores use a random row split over a table with duplicated grid cells. About 97% of positive test rows have a copy of the same cell in training, so these scores are leaked and must not be read as valid estimates of performance on unseen sites.

What I can stand behind is the dataset work and the lesson from it: a merge that yields one row per turbine, followed by a row-level split, puts the same cell on both sides of the test. The counts behind that are under Limitations.

Limitations and next steps

The training table has 190,860 rows but only 126,692 unique grid cells. The merge produced one row per turbine rather than one per cell, so the 72,649 positive rows cover just 8,481 cells. Negative cells appear once each.

A random row split therefore puts copies of the same positive cell, with identical features, on both sides. Simulating a random 80/20 split on that table, about 97% of positive test rows have their cell in training. Any score from that split is optimistic, and the 38% positive rate is an artifact of the duplication, not the real base rate (about 6.7% of cells).

Next step: deduplicate to one row per cell and split by cell, or better by spatial block, since neighbouring cells are correlated. Then report ROC-AUC and PR-AUC on that holdout. I also have not checked whether State acts mainly as a proxy for where wind farms have been built, or looked at turbine age and vintage.