Self-directed project
Energy and Renewable: Machine Learning for the Power Grid
Junior Machine Learning Engineer · AI / ML · July 2026
Built a leakage-free turbine yield predictor end to end: framed TEY as the target, excluded downstream CO/NOx emissions, trained a LinearRegression baseline and a tuned RandomForest, and validated on a genuine time-based hold-out split. Reported honest held-out performance (MAE 1.53 MWh, RMSE 1.94 MWh, R2 0.98) and documented practical limits plus a monitoring/retraining plan.
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 and kept the 8 legitimate sensor features
- Used an earlier-years train / later-years test split instead of a random shuffle
- Compared LinearRegression against RandomForest and showed a clear held-out improvement
- Checked residuals and explicitly noted the model may not generalize to a different plant
- Leakage-Free Feature Engineering
- Honest Held-Out Evaluation
The work I submitted12 tasks
Problem Frame
{"target":"TEY","taskType":"regression","metric":"mae","intendedUse":"Predict energy output sensor data to help engineers monitor performance","passed":true,"attempt":1}Leakage Flags
{"excluded":["CO","NOX","TEY"],"kept":["AT","AP","AH","AFDP","GTEP","TIT","TAT","CDP"],"passed":true,"attempt":1}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":5983,"ranAt":"2026-07-25T06:49:22.599Z"}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=12, 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.37 MWh lower MAE\n","stderr":"","error":null,"imageCount":0,"durationMs":6046,"ranAt":"2026-07-25T06:51:27.876Z"}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 = 120\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=120 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":5630,"ranAt":"2026-07-25T07:01:18.053Z"}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.115 MWh\nResidual std: 1.583 MWh\n -8.48 to -6.54 MWh | ##\n -6.54 to -4.60 MWh | ##########\n -4.60 to -2.66 MWh | ##################################\n -2.66 to -0.71 MWh | #####################################################################################################################################################################################################################################################################################################################\n -0.71 to 1.23 MWh | #######################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################\n 1.23 to 3.17 MWh | ############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################\n 3.17 to 5.12 MWh | ###########################
…Tune Evaluate
{"nEstimators":120,"maxDepth":12,"heldOutMAE":1.532,"heldOutRMSE":1.937,"heldOutR2":0.9833,"residualMean":1.115,"residualStd":1.583}Residual Read
{"category":"heteroscedastic","sentence":"My hold-out MAE,RSME and R2 show good performance on this plant, but they do not prove the model will work on a different plant","passed":true,"attempt":1}Drift Monitor
{"monitorSignal":"rolling_error","retrainThreshold":4,"exploredDriftC":55,"observedMae":9.52}Model Card
{"predicts":"Predicts turbine energy yield(TEY) in MWh from turbine sensor readings.","features":"Uses AT,AP,AH,AFDP,GTEP,TIT,TAT and CDP.CO and NOx were excluded because they are not needed for predicting TEY.","accuracy":"Held-out MAE=1.53 MWh, RMSE=1.94MWh,R2=0.98 KW h","limits":"trained on one plant only. performance may degrade at high loads or when the temperatures go outside the training range, so retraining and monitoring are required","monitor":"Monitor the input distribution and the rolling error. Retrain the model when inputs fall outside the training range or when the rolling MAE keeps increasing.","hits":{"predicts":true,"features":true,"accuracy":true,"limits":true,"monitor":true},"passed":true,"attempt":1}Teach Back
{"prompt":"Explain your model to the plant manager running tonight’s shift, not to another engineer. In plain words, address four things: what the model predicts, roughly how accurate it is, why you trust that number, and the one situation in which it would let the plant down.","summary":"The model predicts turbine energy yield in MWh. On average it is off by about 1.5 MWh. I trust this result because it was tested on data the model had never seen before. It could become less reliable if future temperature or operating conditions go outside the range it was trained on, So the model should then be monitored and retrained if needed.","wordCount":63,"elementsHit":["predicts","accuracy","trust","failure"],"jargonTerms":[],"passed":true,"attempt":1}