Precision-Recall Curve

A Precision-Recall (PR) curve plots precision (positive predictive value) against recall (sensitivity) as the classification threshold is varied. Unlike a ROC curve, the PR curve is insensitive to the class imbalance ratio and focuses exclusively on the performance over the positive class — making it the correct choice for imbalanced datasets such as rare disease detection, fraud detection, or information retrieval.

The area under the PR curve (AUC-PR) summarises classifier performance; a perfect classifier achieves AUC-PR = 1.0, while the no-skill baseline is a horizontal line at the prevalence (positive rate).

Import path: kuva::plot::pr::{PrPlot, PrGroup}


Basic usage

Build one PrGroup per classifier using .with_raw(), which accepts (score, bool) pairs. The curve and AUC-PR are computed internally.

#![allow(unused)]
fn main() {
use kuva::plot::pr::{PrPlot, PrGroup};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

/// Generate deterministic imbalanced predictions.
/// 1 positive per `ratio` negatives; positive scores drawn higher.
fn imbalanced_data(n: usize, ratio: usize, signal: f64) -> Vec<(f64, bool)> {
    let mut data = Vec::with_capacity(n);
    for i in 0..n {
        let is_pos = i % ratio == 0;
        let frac = i as f64 / n as f64;
        let score = if is_pos { signal + (1.0 - signal) * frac } else { (1.0 - signal) * frac };
        data.push((score.clamp(0.0, 1.0), is_pos));
    }
    data
}

let group = PrGroup::new("Classifier A")
    .with_raw(imbalanced_data(300, 10, 0.7));

let pr = PrPlot::new().with_group(group);

let plots = vec![Plot::Pr(pr)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Precision-Recall Curve")
    .with_x_label("Recall")
    .with_y_label("Precision");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("pr.svg", svg).unwrap();
}
Basic precision-recall curve

The dashed grey horizontal line is the no-skill baseline at the dataset prevalence. The AUC-PR appears in the legend. Both can be suppressed — see the API reference.


Optimal F1 threshold marker

.with_optimal_point() marks the threshold that maximises the F1 score (harmonic mean of precision and recall). This is the natural operating point when precision and recall have equal importance.

#![allow(unused)]
fn main() {
use kuva::plot::pr::{PrPlot, PrGroup};
use kuva::render::plots::Plot;
use kuva::render::layout::Layout;
use kuva::render::render::render_multiple;
fn imbalanced_data(n: usize, ratio: usize, signal: f64) -> Vec<(f64, bool)> { vec![] }

let group = PrGroup::new("Classifier A")
    .with_raw(imbalanced_data(300, 10, 0.7))
    .with_optimal_point();

let pr = PrPlot::new().with_group(group);
let plots = vec![Plot::Pr(pr)];
}
PR curve with optimal F1 threshold marker

Multi-model comparison

Add one PrGroup per model. Use .with_legend() on PrPlot to show a legend.

#![allow(unused)]
fn main() {
use kuva::plot::pr::{PrPlot, PrGroup};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

fn imbalanced_data(n: usize, ratio: usize, signal: f64) -> Vec<(f64, bool)> { vec![] }
let g1 = PrGroup::new("Logistic Regression")
    .with_raw(imbalanced_data(300, 10, 0.65));

let g2 = PrGroup::new("Random Forest")
    .with_raw(imbalanced_data(300, 10, 0.80))
    .with_optimal_point();

let g3 = PrGroup::new("Neural Network")
    .with_raw(imbalanced_data(300, 10, 0.88))
    .with_optimal_point();

let pr = PrPlot::new()
    .with_group(g1)
    .with_group(g2)
    .with_group(g3)
    .with_legend("Model");

let plots = vec![Plot::Pr(pr)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Model Comparison — Rare Event Detection")
    .with_x_label("Recall")
    .with_y_label("Precision");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Three-model PR comparison

Pre-computed (recall, precision) points

If you have already computed the PR curve externally (e.g. from Python's sklearn.metrics.precision_recall_curve), pass the points directly. Supply .with_prevalence() to draw the correct no-skill baseline.

#![allow(unused)]
fn main() {
use kuva::plot::pr::{PrPlot, PrGroup};
use kuva::render::plots::Plot;

// (recall, precision) already computed externally; sorted by increasing recall
let points = vec![
    (0.00, 1.00),
    (0.10, 0.91),
    (0.25, 0.84),
    (0.40, 0.76),
    (0.55, 0.67),
    (0.70, 0.55),
    (0.85, 0.42),
    (1.00, 0.10),
];

let group = PrGroup::new("External model")
    .with_points(points)
    .with_prevalence(0.10);  // 10% positive rate

let pr = PrPlot::new().with_group(group);
let plots = vec![Plot::Pr(pr)];
}

Line style

Differentiate models in greyscale or colorblind-safe output using custom stroke styles.

#![allow(unused)]
fn main() {
use kuva::plot::pr::{PrPlot, PrGroup};
use kuva::render::plots::Plot;
fn imbalanced_data(n: usize, ratio: usize, signal: f64) -> Vec<(f64, bool)> { vec![] }

let g1 = PrGroup::new("Model A")
    .with_raw(imbalanced_data(300, 8, 0.75))
    .with_color("black")
    .with_line_width(2.0);

let g2 = PrGroup::new("Model B")
    .with_raw(imbalanced_data(300, 8, 0.60))
    .with_color("black")
    .with_line_width(1.5)
    .with_dasharray("6 3");

let pr = PrPlot::new().with_group(g1).with_group(g2).with_legend("Model");
let plots = vec![Plot::Pr(pr)];
}

PrPlot API reference

PrPlot builders

MethodDefaultDescription
PrPlot::new()Create a new PR curve plot
.with_group(PrGroup)Add one classifier group
.with_groups(iter)Add multiple groups at once
.with_color(css)"steelblue"Fallback color when groups have no explicit color
.with_baseline(bool)trueShow the no-skill prevalence baseline
.with_legend(label)Legend title (shown when groups have labels)

PrGroup builders

MethodDefaultDescription
PrGroup::new(label)Create a group with a display label
.with_raw(iter)Raw (score: f64, label: bool) predictions; computes curve and AUC internally
.with_points(iter)Pre-computed (recall, precision) points; trapezoidal AUC only
.with_prevalence(f)Override prevalence for the no-skill baseline (pre-computed input)
.with_color(css)paletteCurve color
.with_optimal_point()Mark the F1-optimal threshold
.with_auc_label(bool)trueAppend AUC = … to the legend entry
.with_line_width(px)2.0Curve stroke width
.with_dasharray(s)SVG stroke-dasharray string (e.g. "6 3")