Self-directed project
Energy and Renewable: Machine Learning for the Power Grid
Junior Machine Learning Engineer · AI / ML · July 2026
Built a leakage-aware turbine yield predictor end to end: framed TEY as the target, excluded downstream CO/NOx emissions, compared a LinearRegression baseline with a tuned RandomForest, and evaluated on a locked later-period holdout. Reported honest held-out performance (about 1.5 MWh MAE, 1.94 RMSE, R2≈0.983) and documented limitations, drift risks, and a retraining trigger in a concise model card.
Graded against
- Build and validate the model correctly35%
- Optimize and evaluate honestly25%
- Write the Model Card (clarity + honesty)25%
- Explain it to a non-expert15%
Passed · pass mark 60/100
What stood out6
- Correctly excluded CO and NOX as downstream emissions to avoid leakage
- Used an earlier-years train / later-years test split instead of a random shuffle
- Reported held-out MAE, RMSE, and R2 for the tuned model
- Included a concrete monitoring trigger tied to rolling error and input distribution shift
- Leakage-Free Feature Selection
- Time-Based Holdout Evaluation
The work I submitted11 tasks
Problem Frame
{"target":"TEY","taskType":"regression","metric":"mae","intendedUse":"The team will read this number, and will determine how much the difference is","passed":true,"attempt":1}Leakage Flags
{"excluded":["CO","NOX","TEY"],"kept":["AT","AP","AH","AFDP","GTEP","TIT","TAT","CDP"],"passed":true,"attempt":2}Train Validate Baseline
{"checkpointId":"turbine-baseline","code":"import pandas as pd\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.metrics import mean_absolute_error, r2_score\n\ndf = pd.read_csv('gas_turbine.csv')\n\n# The 8 legitimate features. CO and NOX are dropped on purpose: both are\n# measured AFTER the turbine has already made its output, so you would not\n# have them yet at the moment you need to predict TEY.\nfeatures = ['AT', 'AP', 'AH', 'AFDP', 'GTEP', 'TIT', 'TAT', 'CDP'] # fill in the 8th feature\n\ntarget = 'TEY'\n\n# Time-based split: train on 2011-2013, test on 2014-2015.\n# Never shuffle a time-ordered stream at random. That would let the model\n# peek at rows from right next to the ones it gets tested on.\ntrain = df[df['year'] <= 2013]\ntest = df[df['year'] >= 2014] # fill in: which year should testing start at?\n\nX_train, y_train = train[features], train[target]\nX_test, y_test = test[features], test[target]\n\nmodel = LinearRegression()\nmodel.fit(X_train, y_train)\npreds = model.predict(X_test)\n\nmae = mean_absolute_error(y_test, preds)\nr2 = r2_score(y_test, preds)\n\nprint(\"Baseline: Linear Regression\")\nprint(f\"MAE: {mae:.2f} MWh\")\nprint(f\"R2: {r2:.3f}\")\n","stdout":"Baseline: Linear Regression\nMAE: 2.90 MWh\nR2: 0.957\n","stderr":"","error":null,"imageCount":0,"durationMs":3806,"ranAt":"2026-07-25T05:45:42.838Z"}Train Validate Forest
{"checkpointId":"turbine-forest","code":"from sklearn.ensemble import RandomForestRegressor\n\n# Same 8 features, same target, same time-based split as Step 1. Only the\n# model changes: a forest of decision trees instead of one straight line.\nmodel_rf = RandomForestRegressor(n_estimators=120, max_depth=15, random_state=42) # fill in the depth from the brief\nmodel_rf.fit(X_train, y_train)\npreds_rf = model_rf.predict(X_test)\n\nmae_rf = mean_absolute_error(y_test, preds_rf)\nr2_rf = r2_score(y_test, preds_rf)\n\nprint(\"Random Forest (n_estimators=120, max_depth=12)\")\nprint(f\"MAE: {mae_rf:.2f} MWh\")\nprint(f\"R2: {r2_rf:.3f}\")\nprint(f\"Improvement vs baseline: {mae - mae_rf:.2f} MWh lower MAE\")\n","stdout":"Random Forest (n_estimators=120, max_depth=12)\nMAE: 1.53 MWh\nR2: 0.983\nImprovement vs baseline: 1.38 MWh lower MAE\n","stderr":"","error":null,"imageCount":0,"durationMs":4147,"ranAt":"2026-07-25T05:47:11.158Z"}Train Validate
{"baselinePassed":true,"baselineMae":2.9,"baselineR2":0.957,"rfPassed":true,"rfMae":1.53,"rfR2":0.983,"improvementMae":1.37}Tune Evaluate Setup
{"checkpointId":"tune_evaluate_setup","code":"import pandas as pd\nimport numpy as np\nfrom sklearn.ensemble import RandomForestRegressor\nfrom sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n\ndf = pd.read_csv('gas_turbine.csv')\n\nFEATURES = ['AT', 'AP', 'AH', 'AFDP', 'GTEP', 'TIT', 'TAT', 'CDP']\nTARGET = 'TEY'\n\n# Time-based split: train on the earlier years, lock the latest year away as test.\ntrain = df[df['year'] <= 2013]\ntest = df[df['year'] >= 2014]\n\nX_train, y_train = train[FEATURES], train[TARGET]\nX_test, y_test = test[FEATURES], test[TARGET]\n\n# These starter values are deliberately modest. Judge every change ONLY on the\n# held-out numbers printed below, never on how the model looks on training data.\nn_estimators = 125\nmax_depth = 12\n\nmodel = RandomForestRegressor(\n n_estimators=n_estimators,\n max_depth=max_depth,\n random_state=42,\n)\nmodel.fit(X_train, y_train)\npreds = model.predict(X_test)\n\nmae = mean_absolute_error(y_test, preds)\nrmse = mean_squared_error(y_test, preds) ** 0.5\nr2 = r2_score(y_test, preds)\n\nprint(f\"n_estimators={n_estimators} max_depth={max_depth}\")\nprint(f\"Held-out MAE: {mae:.3f} MWh\")\nprint(f\"Held-out RMSE: {rmse:.3f}\")\nprint(f\"Held-out R2: {r2:.4f}\")\n","stdout":"n_estimators=125 max_depth=12\nHeld-out MAE: 1.532 MWh\nHeld-out RMSE: 1.937\nHeld-out R2: 0.9833\n","stderr":"","error":null,"imageCount":0,"durationMs":3510,"ranAt":"2026-07-25T05:51:00.139Z"}Tune Evaluate Residuals
{"checkpointId":"tune_evaluate_residuals","code":"preds = model.predict(X_test)\nresid = y_test.values - preds # true TEY minus predicted TEY, in MWh\n\nprint(f\"Residual mean: {resid.mean():.3f} MWh\")\nprint(f\"Residual std: {resid.std():.3f} MWh\")\n\n# A quick text histogram: how spread out are the misses across the held-out year?\ncounts, edges = np.histogram(resid, bins=8)\nfor c, lo, hi in zip(counts, edges[:-1], edges[1:]):\n print(f\"{lo:6.2f} to {hi:6.2f} MWh | \" + \"#\" * c)\n","stdout":"Residual mean: 1.116 MWh\nResidual std: 1.583 MWh\n -8.49 to -6.54 MWh | ##\n -6.54 to -4.60 MWh | ##########\n -4.60 to -2.65 MWh | ##################################\n -2.65 to -0.70 MWh | ########################################################################################################################################################################################################################################################################################################################\n -0.70 to 1.24 MWh | #########################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################\n 1.24 to 3.19 MWh | ##########################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################\n 3.19 to 5.14 MWh | ########################
…Tune Evaluate
{"nEstimators":125,"maxDepth":12,"heldOutMAE":1.532,"heldOutRMSE":1.937,"heldOutR2":0.9833,"residualMean":1.116,"residualStd":1.583}Residual Read
{"category":"heteroscedastic","sentence":"while the MAE and RMSE shows my overall avg performance, they hide the fact that the model becomes much less reliable when the turbine starts to operate at heavy loads","passed":true,"attempt":1}Drift Monitor
{"monitorSignal":"distribution","retrainThreshold":4,"exploredDriftC":32,"observedMae":1.5}Model Card
{"predicts":"Predicts Turbine Energy Yield (TEY) in Megawatt-hours (MWh).","features":"Includes 8 intake and pre-combustion sensors(AT,AP,AH,AFDP,GTEP,TIT,TAT,CDP). Excludes downstream emissions (CO and NOX) to prevent data leakage, as those readings are unavailable at prediction time.","accuracy":"Achieved MAE of approx 1.5 MWh on unseen held-out data.","limits":"Model accuracy decreases significantly at higher loads. As a tree-based model, it cannot extrapolate beyond its trained input distribution and will fail silently if it encounters extreme out of range sensor readings.","monitor":"We will monitor the input distribution for out of bounds sensor readings and tack the rolling mean absolute error as real telemetry arrives. A retrain will be triggered if the rolling error crosses the 4 MWh limit of if live inputs consistently shift outside the original training data range.","hits":{"predicts":true,"features":true,"accuracy":true,"limits":true,"monitor":true},"passed":true,"attempt":1}