Imbalanced Data and Anomaly Detection: Average Precision, Rebalancing, Alert Budgets and Unseen Failures
Hands-on lab · IDE in your browser

Imbalanced Data and Anomaly Detection: Average Precision, Rebalancing, Alert Budgets and Unseen Failures

Predict wind-turbine gearbox failures when only 5% of days precede one.

Time
55 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit55 min · 5 stepsSession running
3 / 5 steps passingAn alert budget · step 4 of 5
turbines.py▶ Run✓ Check
# ---------- Step 4: a crew that can check one turbine a day ---------- def daily_top_k(df, scores, k=1):    """The alerts a crew that inspects k turbines a day would act on: for each day, the k rows with the highest    score. A DataFrame with columns turbine and day."""    def events_caught(alerts, events, since=SPLIT_DAY, horizon=HORIZON):    """For every event with day >= since: {"turbine", "day", "mode", "caught", "lead_days"}. caught: some alert on    that turbine came 1 to horizon days before the failure; lead_days: days between the earliest such alert and the    failure (0 when missed)."""       
TerminalOutput

The job

Brackwater Wind's crew can inspect one turbine a day, and a gearbox that fails without warning costs a quarter of a million euros. You turn fifteen months of sensor summaries into daily warnings, and make sure a new kind of fault does not slip past them.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    The accuracy trap

    Brackwater Wind replaces a gearbox for about €250,000 when it fails, and for a fraction of that when an inspection catches the damage early.

    You writeconfusion()
  2. 2

    Rebalancing

    The usual advice for imbalance is to rebalance training.

    You writeoversample()
  3. 3

    Honest probabilities

    The maintenance planner reads the model's number as a probability: "0.3 means a 30% chance of failure".

    You writecorrect_prior()
  4. 4

    An alert budget

    The crew can inspect one turbine a day, so the question is not "which rows score above a threshold" but "which turbine do we visit today".

    You writedaily_top_k()
  5. 5

    Failures nobody labelled

    Step 4 missed the cooling faults.

    You writehealthy_signals()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: The accuracy trap

Brackwater Wind replaces a gearbox for about €250,000 when it fails, and for a fraction of that when an inspection catches the damage early. train_days.csv and test_days.csv hold a daily sensor summary for 40 turbines. Each row is labelled fails_within_14d: did this turbine fail within the next two weeks? Only about 5% of rows are, which makes the data imbalanced.

On imbalanced data, accuracy flatters any model that ignores the rare class. Average precision summarises the ranking instead: rank the rows by score, and at every true positive ask what fraction of the rows ranked so far are positive. A random ranking scores the base rate, and a perfect one scores 1.

Do this

1. Write confusion() and average_precision() in turbines.py.

2. Run it. It compares "never fails" with the gradient-boosted model on the test file's 150 days. The model trains on the 300 days before them.

turbines.py, the file you edit126 lines
"""Failure warnings for Brackwater Wind's turbines. You write the functions marked TODO, one step at a time."""
import json

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier

FEATURES = ["vib_rms", "oil_particles", "bearing_temp", "ambient_temp", "power_ratio"]
LABEL = "fails_within_14d"
SPLIT_DAY = 300     # train on days before, test on days from here on
HORIZON = 14        # a warning counts if it comes within 14 days before the failure


def load():
    """(train, test): days before SPLIT_DAY, and the 150 days after."""
    return pd.read_csv("train_days.csv"), pd.read_csv("test_days.csv")


def load_events():
    with open("events.json") as f:
        return json.load(f)


def fit_model(train, sample_weight=None, rows=None):
    """The supervised model: gradient-boosted trees on FEATURES. rows: optional row indices to train on (repeats
    allowed); sample_weight: optional weight per training row."""
    data = train if rows is None else train.iloc[rows]
    return HistGradientBoostingClassifier(random_state=0).fit(data[FEATURES], data[LABEL], sample_weight=sample_weight)


# ---------- Step 1: the accuracy trap ----------

def confusion(y, pred):
    """{"tp", "fp", "fn", "tn"} counts for 0/1 arrays y (truth) and pred."""
    # TODO (Step 1): turn y and pred into boolean arrays and count the four combinations.
    raise NotImplementedError("Step 1: write confusion()")


def average_precision(y, scores):
    """Rank rows by score, highest first; the mean, over the positive rows, of the precision among all rows ranked
    at or above each one. (Assume no tied scores.)"""
    # TODO (Step 1): order y by score (np.argsort(-scores)), precision at every rank =
    # np.cumsum(y) / rank, then the mean of it at the positive rows.
    raise NotImplementedError("Step 1: write average_precision()")


# ---------- Step 2: rebalancing ----------

def oversample(y, seed=0):
    """Row indices for a balanced training set: every row once, plus positive rows drawn with replacement
    (np.random.default_rng(seed).choice) until there are as many positives as negatives."""
    raise NotImplementedError("oversample() arrives in Step 2")


def balanced_weights(y):
    """A weight per row so both classes carry equal total weight: n / (2 * count of the row's class)."""
    raise NotImplementedError("balanced_weights() arrives in Step 2")


# ---------- Step 3: honest probabilities ----------

def correct_prior(p, trained_rate, true_rate):
    """Undo the base-rate shift of training on a set with positive rate trained_rate: multiply the odds p / (1 - p)
    by (true_rate / (1 - true_rate)) / (trained_rate / (1 - trained_rate)) and turn them back into probabilities."""
    raise NotImplementedError("correct_prior() arrives in Step 3")


def reliability(y, p, bins=(0, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0)):
    """A DataFrame with one row per probability bin that holds rows: "bin" (a label like "0.05-0.1"), "n",
    "predicted" (mean p) and "actual" (positive rate). A value equal to an upper edge belongs to that bin; p = 0
    to the first."""
    raise NotImplementedError("reliability() arrives in Step 3")


# ---------- Step 4: a crew that can check one turbine a day ----------

def daily_top_k(df, scores, k=1):
    """The alerts a crew that inspects k turbines a day would act on: for each day, the k rows with the highest
    score. A DataFrame with columns turbine and day."""
    raise NotImplementedError("daily_top_k() arrives in Step 4")


def events_caught(alerts, events, since=SPLIT_DAY, horizon=HORIZON):
    """For every event with day >= since: {"turbine", "day", "mode", "caught", "lead_days"}. caught: some alert on
    that turbine came 1 to horizon days before the failure; lead_days: days between the earliest such alert and the
    failure (0 when missed)."""
    raise NotImplementedError("events_caught() arrives in Step 4")


# ---------- Step 5: failures nobody labelled ----------

def healthy_signals(df, coef):
    """What a healthy turbine keeps steady: vib_rms, oil_particles, the bearing temperature minus what ambient
    explains (np.polyval(coef, ambient_temp)), and power_ratio. A (rows, 4) array."""
    raise NotImplementedError("healthy_signals() arrives in Step 5")


def turbine_baselines(df, coef):
    """Each turbine's own normal: the median of every column of healthy_signals() over its rows. A DataFrame indexed
    by turbine with columns 0-3."""
    raise NotImplementedError("turbine_baselines() arrives in Step 5")


def centred(df, coef, baselines):
    """healthy_signals(df, coef) minus the baseline of each row's own turbine."""
    raise NotImplementedError("centred() arrives in Step 5")


class RobustZ:
    """Distance from normal in robust units: median and 1.4826 x median absolute deviation per column, fitted on
    healthy rows. score() is the largest |z| over the columns of each row."""

    def fit(self, X):
        raise NotImplementedError("RobustZ.fit() arrives in Step 5")

    def score(self, X):
        raise NotImplementedError("RobustZ.score() arrives in Step 5")


def anomaly_detector(train):
    """(coef, baselines, detector), all from the training rows NOT followed by a failure: coef =
    np.polyfit(ambient_temp, bearing_temp, 1), baselines = turbine_baselines(), and a RobustZ fitted on centred()."""
    raise NotImplementedError("anomaly_detector() arrives in Step 5")


Z_ALERT = 5.0
Provided for you:events.jsontest_days.csvtrain_days.csvtry_it.py

Frequently asked questions

Why is accuracy a bad metric for imbalanced data?

When 95% of rows are negative, predicting 'negative' for everything scores 95% accuracy while catching nothing. Average precision and precision-recall curves summarise how well the rare class is ranked instead.

Does oversampling improve an imbalanced classifier?

Often it changes little in the ranking, which average precision measures, but it inflates predicted probabilities because the model learned a higher base rate. Correct the probabilities afterwards, and never oversample before splitting train and test, or duplicates leak across.

When should you use anomaly detection instead of a classifier?

When the failures you need to catch are not in your labelled history. An anomaly detector learns what normal looks like and flags large deviations, so it can catch new failure modes; comparing each machine with its own baseline keeps normal differences between machines from hiding them.

Rare events: imbalanced classification and anomaly detection

When the event you care about is rare, standard metrics and standard fixes both mislead. This lab measures what actually helps. You compute average precision from scratch, compare class weights and oversampling (and the leak of oversampling before a split), restore honest probabilities with a prior correction and a reliability table, evaluate daily top-k alerts by failures caught, and build a robust z-score anomaly detector with per-machine baselines.