campus-weather / code /evaluate.py
citysyntaxlab's picture
Upload code/evaluate.py with huggingface_hub
8a03c28 verified
Raw
History Blame Contribute Delete
16.6 kB
"""
All 5 evaluations for Campus Weather VAE.
Run: python evaluate.py
"""
import os, sys, json
sys.path.insert(0, os.path.dirname(__file__))
import numpy as np
import torch
from torch.utils.data import DataLoader
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
from sklearn.cluster import KMeans
from sklearn.linear_model import Ridge
from sklearn.decomposition import PCA
from model import WeatherVAE, get_config
from train import load_nus40, FlatDataset, VAR_NAMES, VAR_UNITS, load_trained, train_model
SAVE = '/app/campus_weather/results'
DATA = '/app/data_tmp/imputed'
DEVICE = 'cpu'
def eval_reconstruction(model, data, t_split):
"""Basic reconstruction quality on test set."""
test = data[t_split:]
x = torch.from_numpy(test.reshape(-1, 6))
with torch.no_grad():
out = model(x)
pred, true = out['x_hat'].numpy(), x.numpy()
results = {}
for v, (name, unit) in enumerate(zip(VAR_NAMES, VAR_UNITS)):
results[name] = {
'MAE': float(mean_absolute_error(true[:, v], pred[:, v])),
'RMSE': float(np.sqrt(mean_squared_error(true[:, v], pred[:, v]))),
'R2': float(r2_score(true[:, v], pred[:, v])),
}
return results
def eval1_spatial_interpolation(data, coords):
"""
Hold out 5 geographically spread stations. Train on 35.
Reconstruct held-out stations using embeddings from neighbouring stations.
"""
print("\n" + "="*60)
print("EVAL 1: Spatial Interpolation (hold-out 5 stations)")
print("="*60)
N = data.shape[1]
# Pick 5 spread across campus (south, central, north, east, west)
holdout_idx = [4, 12, 20, 30, 37] # WS05, WS13, WS21, WS31, WS38
train_idx = [i for i in range(N) if i not in holdout_idx]
print(f"Hold-out stations: {[i+1 for i in holdout_idx]}")
print(f"Training stations: {len(train_idx)}")
# Train model on 35 stations
T = data.shape[0]
t_tr = int(T * 0.7)
data_35 = data[:, train_idx, :]
cfg = get_config('base')
model_35 = WeatherVAE(**cfg)
mean = torch.tensor(data_35[:t_tr].mean(axis=(0,1)), dtype=torch.float32)
std = torch.tensor(data_35[:t_tr].std(axis=(0,1)), dtype=torch.float32)
model_35.set_normalisation(mean, std)
# Quick train (50 epochs — enough to converge on this simple model)
import torch.optim as optim
opt = optim.AdamW(model_35.parameters(), lr=5e-4, weight_decay=0.01)
tr_ds = FlatDataset(data_35[:t_tr])
tr_ld = DataLoader(tr_ds, batch_size=256, shuffle=True, drop_last=True)
model_35.train()
for ep in range(50):
for batch in tr_ld:
out = model_35(batch)
out['loss'].backward()
torch.nn.utils.clip_grad_norm_(model_35.parameters(), 1.0)
opt.step(); opt.zero_grad()
model_35.eval()
# For each held-out station, reconstruct using k nearest training stations
from sklearn.neighbors import NearestNeighbors
train_coords = coords[train_idx]
holdout_coords = coords[holdout_idx]
nn_model = NearestNeighbors(n_neighbors=5).fit(train_coords)
_, nn_idx = nn_model.kneighbors(holdout_coords)
t_test = int(T * 0.85)
test_data = data[t_test:]
results = {}
for hi, ho_station in enumerate(holdout_idx):
# Get embeddings of 5 nearest training stations
neighbour_stations = [train_idx[j] for j in nn_idx[hi]]
neighbour_data = test_data[:, neighbour_stations, :] # (T_test, 5, 6)
# Average neighbour embeddings as proxy for held-out station
with torch.no_grad():
n_flat = torch.from_numpy(neighbour_data.reshape(-1, 6))
n_emb = model_35.get_embedding(n_flat).numpy()
n_emb = n_emb.reshape(test_data.shape[0], 5, -1).mean(axis=1) # (T_test, d)
# Decode averaged embedding
pred = model_35.decode(torch.from_numpy(n_emb)).numpy()
true = test_data[:, ho_station, :]
station_results = {}
for v, name in enumerate(VAR_NAMES):
station_results[name] = {
'MAE': float(mean_absolute_error(true[:, v], pred[:, v])),
'R2': float(r2_score(true[:, v], pred[:, v])),
}
results[f'WS{ho_station+1:02d}'] = station_results
print(f" WS{ho_station+1:02d}: AirTemp MAE={station_results['AirTemp']['MAE']:.3f}°C, "
f"R²={station_results['AirTemp']['R2']:.4f}")
# Average across held-out stations
avg = {}
for name in VAR_NAMES:
maes = [results[s][name]['MAE'] for s in results]
r2s = [results[s][name]['R2'] for s in results]
avg[name] = {'MAE': float(np.mean(maes)), 'R2': float(np.mean(r2s))}
results['average'] = avg
print(f"\n Average: AirTemp MAE={avg['AirTemp']['MAE']:.3f}°C R²={avg['AirTemp']['R2']:.4f} | "
f"RelHum MAE={avg['RelHum']['MAE']:.3f}% R²={avg['RelHum']['R2']:.4f}")
return results
def eval2_temporal_forecasting(embeddings, data):
"""
Forecast T+1/6/24 using linear regression on embeddings.
Compare vs persistence and climatology baselines.
"""
print("\n" + "="*60)
print("EVAL 2: Temporal Forecasting (embedding vs baselines)")
print("="*60)
T, N, V = data.shape
t_tr, t_te = int(T * 0.7), int(T * 0.85)
hours = np.arange(T) % 24
# Compute hourly climatology from training data
climatology = np.zeros((24, N, V))
for h in range(24):
mask = hours[:t_tr] == h
climatology[h] = data[:t_tr][mask].mean(axis=0)
results = {}
for horizon in [1, 6, 24]:
# Build train/test pairs: embedding at t → weather at t+horizon
tr_X, tr_Y = [], []
for t in range(0, t_tr - horizon):
tr_X.append(embeddings[t].reshape(N, -1)) # (N, d)
tr_Y.append(data[t + horizon]) # (N, V)
tr_X = np.array(tr_X).reshape(-1, embeddings.shape[-1]) # (samples*N, d)
tr_Y = np.array(tr_Y).reshape(-1, V)
te_X, te_Y = [], []
te_persist, te_clim = [], []
for t in range(t_te, T - horizon):
te_X.append(embeddings[t].reshape(N, -1))
te_Y.append(data[t + horizon])
te_persist.append(data[t]) # persistence: weather at t
te_clim.append(climatology[(t + horizon) % 24])
te_X = np.array(te_X).reshape(-1, embeddings.shape[-1])
te_Y = np.array(te_Y).reshape(-1, V)
te_persist = np.array(te_persist).reshape(-1, V)
te_clim = np.array(te_clim).reshape(-1, V)
horizon_results = {}
for v, name in enumerate(VAR_NAMES):
reg = Ridge(alpha=1.0)
reg.fit(tr_X, tr_Y[:, v])
pred = reg.predict(te_X)
mae_emb = mean_absolute_error(te_Y[:, v], pred)
mae_persist = mean_absolute_error(te_Y[:, v], te_persist[:, v])
mae_clim = mean_absolute_error(te_Y[:, v], te_clim[:, v])
horizon_results[name] = {
'MAE_embedding': float(mae_emb),
'MAE_persistence': float(mae_persist),
'MAE_climatology': float(mae_clim),
}
results[f'T+{horizon}'] = horizon_results
print(f"\n T+{horizon}h:")
print(f" {'Variable':>12s} {'Embedding':>10s} {'Persist':>10s} {'Climat':>10s} {'Skill':>8s}")
for name in ['AirTemp', 'RelHum', 'GlobalRad', 'WindSpeed']:
r = horizon_results[name]
skill = 1 - r['MAE_embedding'] / r['MAE_persistence']
print(f" {name:>12s} {r['MAE_embedding']:>10.3f} {r['MAE_persistence']:>10.3f} "
f"{r['MAE_climatology']:>10.3f} {skill:>7.1%}")
return results
def eval3_clustering(embeddings, coords):
"""
Unsupervised microclimate zone discovery from station embeddings.
"""
print("\n" + "="*60)
print("EVAL 3: Microclimate Clustering")
print("="*60)
# Average embedding per station across all time
station_emb = embeddings.mean(axis=0) # (N, d)
# Try K=3,4,5 clusters
results = {}
for k in [3, 4, 5]:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(station_emb)
inertia = km.inertia_
# Silhouette score
from sklearn.metrics import silhouette_score
sil = silhouette_score(station_emb, labels) if k < len(station_emb) else 0
results[f'K={k}'] = {
'labels': labels.tolist(),
'inertia': float(inertia),
'silhouette': float(sil),
}
print(f" K={k}: silhouette={sil:.3f}")
for c in range(k):
stations = [i+1 for i in range(len(labels)) if labels[i] == c]
lat_mean = coords[labels == c, 0].mean()
lng_mean = coords[labels == c, 1].mean()
print(f" Cluster {c}: {len(stations)} stations, "
f"centroid=({lat_mean:.4f}, {lng_mean:.4f}), stations={stations[:8]}...")
# PCA for visualisation
pca = PCA(n_components=2)
station_2d = pca.fit_transform(station_emb)
results['pca_2d'] = station_2d.tolist()
results['explained_var'] = pca.explained_variance_ratio_.tolist()
return results
def eval4_anomaly_detection(model, data, embeddings):
"""
Anomaly detection via reconstruction error.
High error = unusual weather the model hasn't learned.
"""
print("\n" + "="*60)
print("EVAL 4: Anomaly Detection")
print("="*60)
T, N, V = data.shape
# Compute per-timestep reconstruction error
recon_errors = np.zeros((T, N))
with torch.no_grad():
for t in range(0, T, 500):
chunk = data[t:t+500] # (chunk, N, V)
ct, cn = chunk.shape[0], chunk.shape[1]
x = torch.from_numpy(chunk.reshape(-1, V))
out = model(x)
err = (out['x_hat'].numpy() - x.numpy()) ** 2
err = err.mean(axis=1) # mean across variables
recon_errors[t:t+ct] = err.reshape(ct, cn)
# Per-station mean error
station_mean_err = recon_errors.mean(axis=0)
# Find top anomalous hours (campus-wide)
campus_err = recon_errors.mean(axis=1) # (T,)
top_anomalies_idx = np.argsort(campus_err)[-20:][::-1]
# Threshold at 95th percentile
threshold = np.percentile(campus_err, 95)
anomaly_mask = campus_err > threshold
n_anomalies = anomaly_mask.sum()
# Check if anomalies correlate with rainfall
rain = data[:, :, 5] # GlobalRad as proxy (low = cloudy/rainy)
campus_rain = rain.mean(axis=1)
anomaly_rain = campus_rain[anomaly_mask].mean()
normal_rain = campus_rain[~anomaly_mask].mean()
hours_of_day = np.arange(T) % 24
anomaly_hours = hours_of_day[anomaly_mask]
hour_dist = np.bincount(anomaly_hours.astype(int), minlength=24)
results = {
'station_mean_error': station_mean_err.tolist(),
'threshold_95': float(threshold),
'n_anomalies': int(n_anomalies),
'anomaly_rate': float(n_anomalies / T),
'mean_globalrad_anomaly': float(anomaly_rain),
'mean_globalrad_normal': float(normal_rain),
'anomaly_hour_distribution': hour_dist.tolist(),
'top_20_indices': top_anomalies_idx.tolist(),
'campus_error_timeseries': campus_err.tolist(),
}
print(f" Threshold (95th pct): {threshold:.4f}")
print(f" Anomalous hours: {n_anomalies}/{T} ({n_anomalies/T*100:.1f}%)")
print(f" Mean GlobalRad during anomalies: {anomaly_rain:.1f} vs normal: {normal_rain:.1f} W/m²")
print(f" Peak anomaly hours: {np.argsort(hour_dist)[-3:][::-1].tolist()}")
return results
def eval5_future_prediction(embeddings, data):
"""
Multi-step rolling forecast. At each test hour, predict next 24 hours.
Compare vs persistence and climatology.
"""
print("\n" + "="*60)
print("EVAL 5: Rolling 24h Future Prediction")
print("="*60)
T, N, V = data.shape
t_tr, t_te = int(T * 0.7), int(T * 0.85)
hours = np.arange(T) % 24
# Climatology
climatology = np.zeros((24, N, V))
for h in range(24):
mask = hours[:t_tr] == h
climatology[h] = data[:t_tr][mask].mean(axis=0)
# Train one Ridge per (horizon, variable) on training data
models = {} # (horizon, var) → Ridge
for horizon in range(1, 25):
tr_X = embeddings[:t_tr - horizon].reshape(-1, embeddings.shape[-1])
tr_Y = data[horizon:t_tr].reshape(-1, V)
for v in range(V):
reg = Ridge(alpha=1.0)
reg.fit(tr_X, tr_Y[:, v])
models[(horizon, v)] = reg
# Rolling forecast on test set
n_test = T - t_te - 24
all_pred = np.zeros((n_test, 24, N, V))
all_true = np.zeros((n_test, 24, N, V))
all_persist = np.zeros((n_test, 24, N, V))
all_clim = np.zeros((n_test, 24, N, V))
for i, t in enumerate(range(t_te, t_te + n_test)):
emb_t = embeddings[t].reshape(-1, embeddings.shape[-1]) # (N, d)
for h in range(24):
for v in range(V):
all_pred[i, h, :, v] = models[(h + 1, v)].predict(emb_t)
all_true[i, h] = data[t + h + 1]
all_persist[i, h] = data[t] # persistence: current hour
all_clim[i, h] = climatology[(t + h + 1) % 24]
# Compute MAE per horizon
results = {'per_hour': {}}
for h in range(24):
hr_results = {}
for v, name in enumerate(VAR_NAMES):
mae_emb = mean_absolute_error(all_true[:, h, :, v].flatten(), all_pred[:, h, :, v].flatten())
mae_per = mean_absolute_error(all_true[:, h, :, v].flatten(), all_persist[:, h, :, v].flatten())
mae_clm = mean_absolute_error(all_true[:, h, :, v].flatten(), all_clim[:, h, :, v].flatten())
hr_results[name] = {
'MAE_embedding': float(mae_emb),
'MAE_persistence': float(mae_per),
'MAE_climatology': float(mae_clm),
}
results['per_hour'][f'h+{h+1}'] = hr_results
# Summary at key horizons
for h_show in [0, 5, 11, 23]:
hr = results['per_hour'][f'h+{h_show+1}']
print(f"\n h+{h_show+1}:")
for name in ['AirTemp', 'RelHum', 'GlobalRad']:
r = hr[name]
skill = 1 - r['MAE_embedding'] / r['MAE_persistence']
print(f" {name:>12s}: Emb={r['MAE_embedding']:.3f} Pers={r['MAE_persistence']:.3f} "
f"Clim={r['MAE_climatology']:.3f} Skill={skill:.1%}")
return results
def run_all():
"""Run all 5 evaluations."""
os.makedirs(SAVE, exist_ok=True)
# Load model and data
model, data, coords, embeddings, ckpt = load_trained(f'{SAVE}/checkpoints')
T = data.shape[0]
t_te = int(T * 0.85)
all_results = {}
# Reconstruction baseline
print("="*60)
print("BASELINE: Reconstruction Quality")
print("="*60)
recon = eval_reconstruction(model, data, t_te)
all_results['reconstruction'] = recon
for name in VAR_NAMES:
r = recon[name]
print(f" {name:>12s}: MAE={r['MAE']:.4f} RMSE={r['RMSE']:.4f} R²={r['R2']:.4f}")
# Eval 1
all_results['spatial_interpolation'] = eval1_spatial_interpolation(data, coords)
# Eval 2
all_results['temporal_forecasting'] = eval2_temporal_forecasting(embeddings, data)
# Eval 3
all_results['clustering'] = eval3_clustering(embeddings, coords)
# Eval 4
all_results['anomaly_detection'] = eval4_anomaly_detection(model, data, embeddings)
# Eval 5
all_results['future_prediction'] = eval5_future_prediction(embeddings, data)
# Save
# Remove large arrays for JSON serialisation
save_results = {}
for k, v in all_results.items():
if k == 'anomaly_detection':
v2 = {kk: vv for kk, vv in v.items() if kk != 'campus_error_timeseries'}
save_results[k] = v2
else:
save_results[k] = v
with open(f'{SAVE}/all_results.json', 'w') as f:
json.dump(save_results, f, indent=2, default=str)
# Save anomaly timeseries separately (large)
np.save(f'{SAVE}/anomaly_errors.npy', np.array(all_results['anomaly_detection']['campus_error_timeseries']))
print(f"\n{'='*60}")
print(f"All results saved to {SAVE}/")
return all_results
if __name__ == '__main__':
run_all()