--- license: mit library_name: sklearn pipeline_tag: tabular-classification tags: - sklearn - tabular-classification - solar - photovoltaics - fault-detection - energy datasets: - hoodsy/sn-triage-dataset model-index: - name: sn-triage results: - task: type: tabular-classification dataset: name: PVDAQ daily solar fault triage (leave-one-site-out) type: hoodsy/sn-triage-dataset metrics: - type: f1 name: leave-one-site-out macro-F1 value: 0.92 --- # sn-triage A daily fault-triage classifier for solar PV plants: given a 24-number shape summary of one plant-day (performance index vs baseline, clear-sky ratios, plateau/dead runs, temperature tracking, snow trail, seasonality), it labels the day as one of nine classes: `healthy`, `outage`, `weather`, `snow`, `snow_shedding`, `cloud_intermittent`, `clipping`, `curtailment`, `thermal`. The model is a scikit-learn `HistGradientBoostingClassifier` distilled from a rules-plus-referee pipeline over 32 public PVDAQ systems and one SolarNetwork node. It was built to generalize to **plants it has never seen**: every feature is a ratio or a fraction of plant capacity, and missing values (no temperature sensor, short history, no weather feed) are tolerated as `NaN`. It runs day-level, per site, on a single meter stream — sub-metering is not required at inference time. ## Usage `model.joblib` is a self-describing bundle (a plain dict), pickled with joblib under scikit-learn 1.9.0 / Python 3.11: ```python import joblib bundle = joblib.load("model.joblib") model = bundle["model"] # HistGradientBoostingClassifier features = bundle["features"] # ordered list of 24 column names classes = bundle["classes"] # the 9 labels # bundle["trained_at"] = "2026-08-01T03:44:37+00:00" # bundle["version"] = "20260801T034437Z" X = day_frame[features] # one row per day; NaN allowed labels = model.predict(X) proba = model.predict_proba(X) ``` Feature definitions and the training data are in the companion dataset: [`hoodsy/sn-triage-dataset`](https://huggingface.co/datasets/hoodsy/sn-triage-dataset). The 24 features, in bundle order: `pi_rel`, `csr`, `model_csr`, `chop`, `plateau_run_h`, `peak_frac`, `expected_peak_frac`, `dead_run_h`, `morning_ratio`, `midday_ratio`, `tmax_c`, `temp_corr`, `snow_3d`, `snow_7d`, `snow_30d`, `pi_slope_14d`, `pi_drop_share`, `unit_count`, `unit_dist`, `doy_sin`, `doy_cos`, `pi`, `coverage`, `rain_mm`. This is a joblib **pickle** — load it only from this repository or another source you trust. ## Training - 67,987 trainable site-days (nine classes; `data_gap` and `unclassified` days excluded) across 33 sites, of which 6,141 are **gold**: days graded against per-inverter data by a fleet-relative referee. Sample weights = class balance × 5 for gold. - Labels come from an evidence-backed rule classifier, overruled by the referee where sub-metering proved it wrong. The published dataset holds 67,782 of these 67,987 trainable rows: the difference is exactly the 205 trainable days of the one non-PVDAQ training site (a SolarNetwork node excluded from the dataset for licensing reasons). ## Evaluation Faults are multi-day events; all protocols split by event or by site, never by row. - **Leave-one-site-out macro-F1 0.92** (headline): train on 32 sites, score the held-out site, rotate. All classes ≥ 0.86 except curtailment (0.69). - Event-holdout macro-F1 0.959. - On the 1,552 days where the referee overruled the rules, this model — never trained on the site being scored — sides with the referee 77% of the time and with the overruled rule label 0%. - **True held-out-site test**: two Santa Ana, CA systems (PVDAQ 14695/14697) were onboarded after training and scored against the frozen model: 98.4% day-level agreement over 1,710 labeled days, 88.5% on the 208 referee-graded days, zero false alarms across 1,383 healthy days. Both sites were folded into training afterward, and this published model is the retrain; the numbers above describe its frozen predecessor on data that model had never seen. ## Edge deployment This exact bundle (`20260801T034437Z`) also runs at the edge as a SolarQuant plugin (Ecosuite's spec: a container on the plant's SolarNode ingests live datums, closes each local day, computes the same 24 features — Open-Meteo weather when available, clear-sky fallback — and posts a daily fault-class datum). Replaying a 7-month window of the SolarNetwork training node through the containerized plugin at production streaming cadence reproduced the batch pipeline's final labels on **92.4% of 211 days**, including 88/88 outage days (a real 77-day outage) and 44/44 weather days. The residual is the plugin's online trailing-median baseline vs the batch two-pass baseline on marginal healthy/weather days, plus days the batch left `unclassified` (a label this model deliberately does not have — it predicts over them). ## Limitations - **Curtailment is the hard class** (F1 0.69). Its residual confusion is asymmetric partial outage (one inverter of two down) vs curtailment — from a single meter stream both look like a bright, smooth, scaled-down day. Separating them reliably requires sub-metering. - Trained on fixed-tilt systems only; tracker plants are out of scope. - Day-level granularity: it labels days, not intervals, and won't catch sub-daily transients. - Weather features assume Open-Meteo/ERA5-like inputs where used; they are NaN-tolerant but the model performs best with them present. ## Attribution Trained on data derived from NREL's PVDAQ public datasets (DOE Open Energy Data Initiative, CC BY 4.0): > Deline, C., Perry, K., Deceglie, M., Muller, M., Sekulic, W., & > Jordan, D. (2021). _Photovoltaic Data Acquisition (PVDAQ) Public > Datasets_ [Data set]. Open Energy Data Initiative (OEDI). NREL. > https://doi.org/10.25984/1846021 Weather data by [Open-Meteo.com](https://open-meteo.com/) (CC BY 4.0); contains modified Copernicus Climate Change Service information (ERA5). One additional training site (a residential Auckland node) comes from the [SolarNetwork](https://solarnetwork.net/) public API.