# Chronological evaluation and training-set error normalization. # Input preparation and model fitting are described in the case methods. train_dates, val_dates, test_dates = all_dates[:20], all_dates[20:27], all_dates[27:] full = merged.inverter_count.eq(expected_inverters) weather_valid = np.isfinite(merged[["IRRADIATION", "AMBIENT_TEMPERATURE", "MODULE_TEMPERATURE"]].to_numpy()).all(axis=1) daylight = merged.IRRADIATION.gt(0.01) clean = merged.loc[full & weather_valid & daylight].copy().sort_values("DATE_TIME") clean["date"] = clean.DATE_TIME.dt.date train = clean.loc[clean.date.isin(train_dates)].copy() val = clean.loc[clean.date.isin(val_dates)].copy() test = clean.loc[clean.date.isin(test_dates)].copy() assert train.DATE_TIME.max() < val.DATE_TIME.min() < test.DATE_TIME.min() assert not set(train.date) & set(test.date) if min(len(train), len(val), len(test)) < 30: raise ValueError("Insufficient retained observations in a time partition") xtrain, xval = features(train), features(val) ytrain, yval = train.AC_POWER.to_numpy(), val.AC_POWER.to_numpy() scale = float(np.quantile(ytrain, 0.95)) def metrics(y, pred, scale): return {"mae_reported_kw": float(np.mean(np.abs(y-pred))), "rmse_reported_kw": float(np.sqrt(np.mean((y-pred)**2))), "nmae_percent_train_p95": float(100*np.mean(np.abs(y-pred))/scale)}