Skip to content

Metrics API Reference

Evaluation

evaluate_detections

evaluate_detections(
    predictions: DataFrame | LazyFrame,
    ground_truth: DataFrame | LazyFrame,
    *,
    geometry: str | Sequence[str] = "bbox",
    box_format: BoxFormat | None = None,
    image_id: str = COL_IMAGE_ID,
    class_id: str | None = COL_CLASS_ID,
    score: str = "score",
    iou_thresholds: Literal["coco"]
    | float
    | Sequence[float] = "coco",
    interpolation: APInterpolation | None = None,
    max_detections: int | None | Literal["coco"] = "coco",
    images: Iterable[str]
    | Series
    | DataFrame
    | LazyFrame
    | None = None,
    weight: str | None = None,
    group: str | None = None,
    fp_rates: Sequence[float] | None = None,
    extrapolate: Extrapolate = "none",
) -> DetectionReport

Evaluate object detections (boxes, polygons or instance masks).

The one-call form of :func:match_detections plus the summary metrics, COCO-style by default::

report = evaluate_detections(preds, gts, box_format="xyxy")
report.summary            # map, map_50, map_75, mar
report.per_class          # AP per class
report.ci("map")          # bootstrap interval
report.matches(0.5)       # each prediction's TP/FP, for error analysis

With the defaults (iou_thresholds="coco") matching is redone at each IoU threshold 0.50:0.05:0.95, AP is COCO's 101-point interpolation, each image keeps its 100 highest-scoring predictions per class, and a class without ground truth is left out of the means — COCO's evaluation, except that it has no area ranges and no crowd/ignore regions (and ties between equal scores form one PR point rather than following input order).

Parameters:

Name Type Description Default
predictions DataFrame | LazyFrame

One row per predicted object (image_id, geometry, score, class_id).

required
ground_truth DataFrame | LazyFrame

One row per ground-truth object.

required
geometry str | Sequence[str]

The geometry column (or four coordinate columns); see :func:match_detections for the accepted forms.

'bbox'
box_format BoxFormat | None

"xyxy", "xywh" or "cxcywh" for boxes given as four numbers.

None
image_id str

The image identifier column.

COL_IMAGE_ID
class_id str | None

The class column, or None for one class.

COL_CLASS_ID
score str

The prediction confidence column.

'score'
iou_thresholds Literal['coco'] | float | Sequence[float]

"coco", one threshold (e.g. 0.5, Pascal VOC) or several.

'coco'
interpolation APInterpolation | None

AP interpolation; None is "101_point" for "coco" and "all_points" otherwise.

None
max_detections int | None | Literal['coco']

Per (image, class) cap on predictions; "coco" is 100 with iou_thresholds="coco" and no cap otherwise.

'coco'
images Iterable[str] | Series | DataFrame | LazyFrame | None

The image population (see :func:match_detections).

None
weight str | None

A per-image weight column of the images frame.

None
group str | None

A per-image subgroup column of the images frame.

None
fp_rates Sequence[float] | None

FROC operating points to summarise (FROC_RATES is the radiology set), or None.

None
extrapolate Extrapolate

How FROC curves are read past their last point.

'none'

Returns:

Name Type Description
A DetectionReport

class:DetectionReport.

evaluate_heatmaps

evaluate_heatmaps(
    data: DataFrame | LazyFrame,
    *,
    heatmap: str,
    gt: str,
    image_id: str | None = None,
    class_id: str | None = None,
    iou_threshold: float | Sequence[float] = 0.5,
    fp_rates: Sequence[float] = FROC_RATES,
    extrapolate: Extrapolate = "none",
    interpolation: APInterpolation = "all_points",
    weight: str | None = None,
    group: str | None = None,
    **matcher_options: Any,
) -> DetectionReport

Evaluate dense predictions (heatmaps, probability maps) against masks.

One row per image: each heatmap is thresholded into candidate regions, each region scored from the heatmap, and the regions matched to the ground-truth mask's regions by :class:ContourMatcher (any of its options can be passed through, e.g. match_by="coverage", extraction_threshold=0.3, score_reduction="mean"). The report summarises FROC — sensitivity at each of fp_rates and their mean, the CPM — beside AP::

report = evaluate_heatmaps(df, heatmap="prob", gt="mask")
report.summary              # map, mar, sensitivity@…, cpm
report.ci("cpm")

Parameters:

Name Type Description Default
data DataFrame | LazyFrame

One row per image.

required
heatmap str

The prediction column (any format a polars-cv source reads).

required
gt str

The ground-truth mask (or contour) column.

required
image_id str | None

An image identifier column (default: the row index).

None
class_id str | None

A class column (one row per image and class).

None
iou_threshold float | Sequence[float]

The matching threshold, or several.

0.5
fp_rates Sequence[float]

FROC operating points (false positives per image).

FROC_RATES
extrapolate Extrapolate

How the FROC curve is read past its last point ("flat" is LUNA16's convention).

'none'
interpolation APInterpolation

AP interpolation.

'all_points'
weight str | None

A per-image weight column of data.

None
group str | None

A per-image subgroup column of data.

None
**matcher_options Any

:class:ContourMatcher options.

{}

Returns:

Name Type Description
A DetectionReport

class:DetectionReport.

evaluate_segmentation

evaluate_segmentation(
    data: DataFrame | LazyFrame,
    *,
    pred: str,
    target: str,
    image_id: str | None = None,
    threshold: float = 0.5,
    frame: Literal["image"] | None = "image",
    sample_step: float | None = 1.0,
    spacing: tuple[float, float] | None = None,
) -> SegmentationReport

Evaluate semantic segmentation masks: Dice, IoU, ASSD, HD, HD95.

::

report = evaluate_segmentation(df, pred="pred_mask", target="gt_mask")
report.summary          # mean of each measure, with undefined counts
report.per_image        # one row per image
report.ci("dice")

Parameters:

Name Type Description Default
data DataFrame | LazyFrame

One row per image.

required
pred str

The predicted mask column.

required
target str

The reference mask column.

required
image_id str | None

An image identifier column (default: the row index).

None
threshold float

Foreground threshold for both masks.

0.5
frame Literal['image'] | None

"image" leaves boundary on the image edge unmeasured.

'image'
sample_step float | None

Boundary sample spacing (see :func:segmentation_measures).

1.0
spacing tuple[float, float] | None

(row, column) pixel spacing for physical distances.

None

Returns:

Name Type Description
A SegmentationReport

class:SegmentationReport.

segmentation_measures

segmentation_measures(
    pred: str | Expr,
    target: str | Expr,
    *,
    threshold: float = 0.5,
    frame: Literal["image"] | None = "image",
    sample_step: float | None = 1.0,
    spacing: tuple[float, float] | None = None,
) -> pl.Expr

Overlap and boundary agreement of two masks, one struct per row.

Parameters:

Name Type Description Default
pred str | Expr

The predicted mask column (any format a polars-cv source reads: encoded image bytes, a nested List/Array, a VIEW blob).

required
target str | Expr

The reference mask column, the same size.

required
threshold float

Pixels above it are foreground (0.5 suits 0/1 and 0/255 masks and probability maps alike).

0.5
frame Literal['image'] | None

"image" leaves boundary on the image edge unmeasured; None measures all of it.

'image'
sample_step float | None

Spacing of the boundary samples along each edge, in output units (pixels, or spacing units); None samples the outline vertices only.

1.0
spacing tuple[float, float] | None

(row, column) pixel spacing (e.g. mm): boundary distances in those units. Overlap is unitless either way.

None

Returns:

Type Description
Expr

``Struct{dice, iou, assd, hd, hd95, mean_pred_to_target,

Expr

mean_target_to_pred}`` (Float64). Two empty masks agree perfectly

Expr

(Dice and IoU 1); one empty mask has Dice and IoU 0. Boundary

Expr

distances are null whenever either mask is empty (no boundary to

Expr

measure to) — as MONAI's are NaN.

DetectionReport dataclass

A detection evaluation: summary, per-class and per-threshold tables.

Every number comes from a :class:Statistic on :attr:table (or one threshold's view of it); :meth:ci bootstraps the same statistics.

Attributes:

Name Type Description
table DetectionTable

The matched :class:DetectionTable (a sweep when several IoU thresholds were matched).

interpolation APInterpolation

The AP interpolation of every AP in the report.

fp_rates tuple[float, ...] | None

The FROC operating points summarised (sensitivity at each, and their mean, the CPM), or None for no FROC summary.

extrapolate Extrapolate

How FROC curves are read past their last point.

summary cached property

summary: DataFrame

[metric, value]: mAP (COCO: classes without ground truth left out), mAP at 0.5 / 0.75 when swept, mean recall (mar) and, with fp_rates, FROC sensitivities and their mean (cpm).

per_class cached property

per_class: DataFrame

[class_id, ap, ap_50, ap_75, recall, n_gts, n_preds]: AP and recall averaged over the matched thresholds (ap_50/ap_75 when those were matched).

per_threshold cached property

per_threshold: DataFrame

[iou_threshold, class_id, ap, recall, n_gts, n_preds], one row per matched threshold and class. ap/recall are null for a class without ground truth (undefined, as COCO's -1).

ci

ci(
    metric: str | Sequence[str] | None = None,
    *,
    by: str | list[str] | None = None,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
    sample_col: str | None = None,
    strata: str | list[str] | None = None,
    weight_rtol: float | None = None,
    weight_scheme: WeightScheme = "reestimate",
) -> pl.DataFrame

Percentile bootstrap intervals for summary numbers.

Each is :func:bootstrap_ci of the statistic behind the number: images are resampled (stratified by whether they hold ground truth), and the classes and IoU thresholds a number averages over are drawn together, so an mAP interval reflects one resample per replicate.

Parameters:

Name Type Description Default
metric str | Sequence[str] | None

A summary name, several, or None for all of them.

None
by str | list[str] | None

Optional metadata column(s) to report per group (e.g. "group_id"); each group is resampled within itself.

None
n_bootstrap int

Replicates.

1000
confidence float

Confidence level in (0, 1).

0.95
seed int | None

Optional seed (None is a fixed default: reproducible).

None
sample_col str | None

Optional entity column (e.g. a patient id) to resample instead of images.

None
strata str | list[str] | None

Optional metadata column(s) crossed into the weight cells.

None
weight_rtol float | None

Relative tolerance within which weights share a cell.

None
weight_scheme WeightScheme

What the weights are, and so how the draw treats them: "reestimate", "stratified" or "fixed" (see :func:~polars_cv.metrics.froc_auc_ci_lazy).

'reestimate'

Returns:

Type Description
DataFrame

[*by, metric, value, ci_lower, ci_upper].

matches

matches(iou_threshold: float | None = None) -> pl.DataFrame

Every prediction with its outcome at one threshold, for error analysis.

For a report from :func:evaluate_detections, the prediction input's rows with is_tp, iou and matched_gt_row (the matched ground truth's row in the ground-truth input). Otherwise the detections table.

pr_curve

pr_curve(
    class_id: str | None = None,
    iou_threshold: float | None = None,
) -> PrecisionRecallResult

The precision-recall curve of one class at one threshold.

froc

froc(
    iou_threshold: float | None = None,
    *,
    group_by: str | list[str] | None = None,
) -> pl.DataFrame

The FROC curve at one threshold (default: the lowest matched).

confusion

confusion(
    score_threshold: float,
    *,
    class_id: str | None = None,
    iou_threshold: float | None = None,
) -> ConfusionResult

TP/FP/FN of the detections scoring at least score_threshold.

at

at(iou_threshold: float | None = None) -> DetectionTable

The table at one IoU threshold (default: the lowest matched).

SegmentationReport dataclass

Per-image measures of a segmentation evaluation and their summary.

Attributes:

Name Type Description
per_image DataFrame

[image_id, dice, iou, assd, hd, hd95, …], one row per image.

summary cached property

summary: DataFrame

[metric, value, n, n_undefined]: the mean of each measure over the images where it is defined (boundary distances are undefined when a mask is empty), and how many images that left out.

ci

ci(
    metric: str | Sequence[str] | None = None,
    *,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
) -> pl.DataFrame

Percentile bootstrap intervals of the summary means over images.

Images are drawn by the same seeded hash resampler as the detection intervals (:func:~polars_cv.metrics.bootstrap_ci), so a seed means the same thing in both.

Returns:

Type Description
DataFrame

[metric, value, ci_lower, ci_upper].

ReportMetric dataclass

A reported number: the statistic and the table view it reads.

match_detections

match_detections(
    predictions: DataFrame | LazyFrame,
    ground_truth: DataFrame | LazyFrame,
    *,
    geometry: Geometry = "bbox",
    box_format: BoxFormat | None = None,
    image_id: str = COL_IMAGE_ID,
    class_id: str | None = COL_CLASS_ID,
    score: str = "score",
    iou_thresholds: float | Sequence[float] = 0.5,
    max_detections: int | None = None,
    images: Iterable[str]
    | Series
    | DataFrame
    | LazyFrame
    | None = None,
    weight: str | None = None,
    group: str | None = None,
    carry: Carry | None = None,
) -> DetectionTable

Match object-level predictions to ground truth: the general entry point.

Takes the long tables detections usually come in — one row per object — and returns the :class:DetectionTable every metric, statistic and confidence interval reads. The geometry decides the matcher:

  • boxes — a BBOX_SCHEMA column, or four numbers per object (a List/Array column, or a tuple of four column names) in box_format: matched by box IoU (:class:BBoxMatcher);
  • polygons — a CONTOUR_SCHEMA column: matched by region IoU (:class:ContourMatcher);
  • instance masks — one binary mask per object (encoded image bytes or a 2-D List/Array): its outline is extracted and matched as a polygon. A mask must hold exactly one connected region; a mask with several (or none) fails the query, naming its image — split it, or pass polygons.

Matching is greedy in descending confidence, each prediction taking the unmatched ground truth it overlaps most at or above the threshold — the COCO rule. A sequence of iou_thresholds matches once per threshold ([0.5, 0.55, …, 0.95] for COCO), so a detection that loses its ground truth at one threshold can claim another at a higher one.

Parameters:

Name Type Description Default
predictions DataFrame | LazyFrame

One row per predicted object (image_id, geometry, score, and class_id unless class_id=None).

required
ground_truth DataFrame | LazyFrame

One row per ground-truth object.

required
geometry Geometry

The geometry column (same name in both tables), or a tuple of four coordinate columns.

'bbox'
box_format BoxFormat | None

"xyxy", "xywh" or "cxcywh": required for boxes given as four numbers, refused otherwise.

None
image_id str

The image identifier column.

COL_IMAGE_ID
class_id str | None

The class column, or None for one class.

COL_CLASS_ID
score str

The prediction confidence column.

'score'
iou_thresholds float | Sequence[float]

One IoU threshold, or several to match at each.

0.5
max_detections int | None

Keep each (image, class)'s max_detections highest-scoring predictions (COCO: 100).

None
images Iterable[str] | Series | DataFrame | LazyFrame | None

The image population: ids, or a frame with an image_id column (and the weight / group / carry columns when named). With any of those, the frame must hold one row per image; this is not checked (a repeated image_id would repeat that image's objects).

None
weight str | None

A per-image weight column of the images frame.

None
group str | None

A per-image subgroup column of the images frame (e.g. a scanner or site), for grouped and stratified evaluation.

None
carry Carry | None

Further per-image columns of the images frame to copy into image_metadata: names, or expressions over it named by their output name (see :meth:BBoxMatcher.match). Each is then a grouping key like group.

None

Returns:

Name Type Description
The DetectionTable

class:DetectionTable. Its det_idx is the prediction's

DetectionTable

position in its (image, class) list; :func:group_objects maps it

DetectionTable

back to the input row.

group_objects

group_objects(
    predictions: DataFrame | LazyFrame,
    ground_truth: DataFrame | LazyFrame,
    *,
    geometry: str,
    image_id: str = COL_IMAGE_ID,
    class_id: str | None = COL_CLASS_ID,
    score: str = "score",
    images: Iterable[str]
    | Series
    | DataFrame
    | LazyFrame
    | None = None,
    max_detections: int | None = None,
    image_columns: Sequence[str] = (),
) -> pl.LazyFrame

Long object tables → one row per (image, class) with object lists.

Parameters:

Name Type Description Default
predictions DataFrame | LazyFrame

One row per predicted object: image_id, geometry, score and (unless class_id=None) class_id columns.

required
ground_truth DataFrame | LazyFrame

One row per ground-truth object: image_id, geometry and class_id.

required
geometry str

The geometry column, the same name in both tables.

required
image_id str

The image identifier column.

COL_IMAGE_ID
class_id str | None

The class column, or None for one class.

COL_CLASS_ID
score str

The prediction confidence column.

'score'
images Iterable[str] | Series | DataFrame | LazyFrame | None

The image population: ids (or a frame with an image_id column). Images absent from both tables are evaluated as empty (their predictions none, their truth none). None uses the images in either table.

None
max_detections int | None

Keep only each (image, class)'s highest-scoring predictions — COCO's maxDets (100).

None
image_columns Sequence[str]

Columns of the images frame to copy onto each of the image's rows (null for an image it does not list). The frame must hold one row per image; this is not checked.

()

Returns:

Type Description
LazyFrame

A lazy frame with image_id (String), class_id (String),

LazyFrame

pred / gt (lists of geometry), pred_score (List[Float64],

LazyFrame

aligned with pred, highest first) and pred_row / gt_row

LazyFrame

(each object's row index in its input table). Every image of the

LazyFrame

population is crossed with every class of either table.

Statistics

COCO_IOU_THRESHOLDS (0.50:0.05:0.95, COCO's IoU sweep) and FROC_RATES (0.125 to 8 false positives per image, the standard FROC operating points) are the defaults the evaluation functions use; both are importable from polars_cv.metrics.

Statistic

Bases: ABC

A metric as a grouped lazy reduction of a :class:DetectionTable.

Subclasses implement :meth:_by_group; callers use :meth:by_group (any grouping, lazily) or :meth:value (the ungrouped scalar).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

AP dataclass

Bases: Statistic

Weighted average precision.

Parameters:

Name Type Description Default
interpolation APInterpolation

"all_points" (the envelope integrated as a step function, Pascal VOC 2010+), "11_point" (VOC 2007) or "101_point" (COCO).

'all_points'

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

Recall dataclass

Bases: Statistic

Weighted recall over every detection: the highest recall reached.

Averaged over classes and IoU thresholds, this is COCO's average recall (AR) at the detections kept (max_detections).

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

PrecisionAt dataclass

Bases: _AtScore

Weighted precision of the detections scoring at least score_threshold (1.0 when none do).

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

RecallAt dataclass

Bases: _AtScore

Weighted recall of the detections scoring at least score_threshold.

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

F1At dataclass

Bases: _AtScore

Weighted F1 of the detections scoring at least score_threshold.

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

FROCSensitivity dataclass

Bases: Statistic

FROC sensitivity at fp_per_image false positives per image.

Null where the curve does not reach that rate and extrapolate="none".

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

CPM dataclass

Bases: Statistic

Competition performance metric: the mean FROC sensitivity over fp_rates (LUNA16 / ANODE09; by default the seven rates 1/8 … 8).

Null where the curve does not reach a rate and extrapolate="none"; LUNA16 reads the curve with extrapolate="flat".

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

FROCAUC dataclass

Bases: Statistic

FROC AUC (see :func:~polars_cv.metrics.froc_auc for the arguments).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

LROCAUC dataclass

Bases: Statistic

LROC AUC (see :func:~polars_cv.metrics.lroc_auc for the arguments).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

LROCSensitivity dataclass

Bases: Statistic

LROC sensitivity at a false-positive fraction fpf.

empty_value property

empty_value: float

The value of a group with no detections (a bootstrap draw of none).

require_both_classes property

require_both_classes: bool

Whether a group needs negative images too (two-class rank statistics).

by_group

by_group(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The statistic per group, as a lazy [*keys, name] frame.

Parameters:

Name Type Description Default
table DetectionTable

The detection table.

required
keys str | Sequence[str] | None

Grouping column(s) on the detections or the metadata. None gives one row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

support

support(
    table: DetectionTable,
    keys: list[str],
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The groups where the statistic is defined, as [*keys].

Detection statistics are defined where there is ground truth to find: a group with no weighted ground-truth mass has no recall (COCO scores such a class -1 and leaves it out of the mean).

value

value(
    table: DetectionTable,
    *,
    weight_agg: WeightAgg = "first",
) -> float

The ungrouped statistic as a Python float (one collect).

A table with no row for the statistic (no detections) gives :attr:empty_value; a null (an operating point the curve does not reach) is returned as nan.

MeanOver dataclass

Bases: Statistic

The mean of statistic over facets — mAP is MeanOver(AP()).

The inner statistic is computed per [*keys, *over] and averaged within keys, over the facets where it is defined (:meth:Statistic.support): a facet with ground truth but no detections counts as the inner statistic's empty value (AP 0). A facet with no ground truth is handled by undefined:

  • "exclude" leaves it out of the mean, as COCO does (an absent class is not a class the model failed on);
  • "zero" averages it in as 0 (what :func:~polars_cv.metrics.mean_average_precision has always done).

Parameters:

Name Type Description Default
statistic Statistic

The statistic to average.

required
over tuple[str, ...]

The facet columns; class_id and iou_threshold by default (whichever the table has).

(COL_CLASS_ID, 'iou_threshold')
undefined Literal['exclude', 'zero']

The policy for facets without ground truth (required: the two conventions give different numbers).

required
label str | None

The output column; "mean_<inner name>" by default.

None

per_facet

per_facet(
    table: DetectionTable,
    keys: str | Sequence[str] | None = None,
    *,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

The inner statistic per [*keys, *facets] over the averaging grid.

One row per facet the mean averages over: undefined="exclude" keeps only facets with ground truth; "zero" keeps every facet of the metadata, scoring those without ground truth 0.

mean_ap

mean_ap(
    interpolation: APInterpolation = "all_points",
    *,
    undefined: Literal["exclude", "zero"] = "exclude",
) -> MeanOver

mAP: :class:AP averaged over classes and IoU thresholds.

bootstrap_ci

bootstrap_ci(
    table: DetectionTable,
    statistic: Statistic,
    *,
    group_by: str | list[str] | None = None,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
    sample_col: str | None = None,
    strata: str | list[str] | None = None,
    weight_rtol: float | None = None,
    weight_scheme: WeightScheme = "reestimate",
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

Lazy, group-aware percentile bootstrap CI for any :class:Statistic.

The one CI engine: froc_auc_ci_lazy, lroc_auc_ci_lazy and average_precision_ci_lazy are this with FROCAUC, LROCAUC and AP. The point estimate is statistic.by_group(table, group_by); each replicate is the same statistic on a resample of the table keyed by bootstrap_id. Resampling, stratification (gt_label and the weight_scheme's weight cells), entity-level draws and the degenerate-group rules are those of :func:froc_auc_ci_lazy (see it for the details).

Images are the resampling unit and group_by partitions the draw, so keys a statistic averages over (a :class:MeanOver facet such as class_id or iou_threshold) stay paired: a drawn image brings its rows for every class and threshold into the same replicate. Pass them as group_by instead to get one independently-resampled interval per facet.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
statistic Statistic

The statistic to bootstrap, e.g. AP("101_point") or mean_ap().

required
group_by str | list[str] | None

Optional grouping column(s). None yields one row.

None
n_bootstrap int

Number of bootstrap replicates.

1000
confidence float

Confidence level in (0, 1).

0.95
seed int | None

Optional RNG seed (None → deterministic constant).

None
sample_col str | None

Optional entity column to resample at the entity level.

None
strata str | list[str] | None

Optional metadata column(s) crossed into the weight cells.

None
weight_rtol float | None

Relative tolerance within which weights share a cell (None: 1e-6).

None
weight_scheme WeightScheme

What the weights are — "reestimate", "stratified" or "fixed" (see :func:froc_auc_ci_lazy).

'reestimate'
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
LazyFrame

LazyFrame with [*group_by, <statistic.name>, ci_lower, ci_upper].

Core Types

DetectionTable dataclass

Canonical intermediate representation consumed by all metric functions.

Wraps two aligned lazy frames:

  • detections — one row per detection with image_id, class_id, score, is_tp, gt_idx, iou, det_idx.
  • image_metadata — one row per (image, class) with n_gts, weight, gt_label, and optionally group_id.

When the same image_id (and class_id, when present) appears more than once in image_metadata (e.g. one rendered image owned by two cases), FROC/LROC resolve the per-key weight to a single value so detections are not fan-out-multiplied. Equal weights on the duplicates are the common case; disagreeing weights are resolved by the metric's weight_agg policy ("first" default, or "min"/"max"/"mean"/"sum") rather than raising — supplying consistent weights is the caller's responsibility. Prefer a composite key in image_id when each ownership should be a distinct evaluation unit.

Use :meth:from_matched to construct with schema validation.

iou_thresholds property

iou_thresholds: tuple[float, ...]

The IoU thresholds matched: several for a sweep, one for a single matching at a known threshold, none when unknown (pre-matched, or a per-row threshold).

from_matched classmethod

from_matched(
    detections: LazyFrame | DataFrame,
    image_meta: LazyFrame | DataFrame,
    *,
    matching_iou_threshold: float | None = None,
    has_iou: bool = True,
) -> DetectionTable

Construct a DetectionTable with planning-time schema validation.

Parameters:

Name Type Description Default
detections LazyFrame | DataFrame

Per-detection rows. Must contain columns image_id, class_id, score, is_tp, gt_idx, iou, det_idx.

required
image_meta LazyFrame | DataFrame

Per-image metadata. Must contain image_id, class_id, n_gts, weight, gt_label.

required
matching_iou_threshold float | None

The IoU threshold used by the matcher. Stored so that :meth:at_iou_threshold can warn when the caller tries to lower the threshold below the matching level (which has no effect).

None
has_iou bool

Whether the iou column holds real overlaps; False when the source had none (iou is then null), so :meth:at_iou_threshold refuses the table.

True

Returns:

Type Description
DetectionTable

Validated DetectionTable instance.

Raises:

Type Description
ValueError

If required columns are missing.

with_group

with_group(group_col: str) -> DetectionTable

Return a copy with group_id set from an existing metadata column.

Parameters:

Name Type Description Default
group_col str

Column in image_metadata to use as the group.

required

Returns:

Type Description
DetectionTable

New DetectionTable with group_id populated.

filter_class

filter_class(class_id: str) -> DetectionTable

Return a copy filtered to a single class.

Parameters:

Name Type Description Default
class_id str

The class to retain.

required

Returns:

Type Description
DetectionTable

Filtered DetectionTable.

class_ids

class_ids() -> list[str]

Return distinct class IDs present in the detections.

Note: triggers a partial collect on the metadata frame.

at_iou_threshold

at_iou_threshold(iou_threshold: float) -> DetectionTable

Return the table evaluated at one IoU threshold.

On a sweep (a table matched at several thresholds) a matched threshold is selected exactly: its own matching, as the matcher produced it at that threshold. Any other threshold — and every threshold of a single matching — re-thresholds a matching instead: the stored iou is compared against iou_threshold to set is_tp, without matching again (on a sweep, the matching at the highest threshold below it). Re-thresholding is not re-matching: a detection that lost its ground truth at the matching threshold to a higher-scoring one is not given it back, which COCO's per-threshold matching would do.

.. warning::

Re-thresholding only works reliably when *raising* the threshold
above the original matching IoU. Lowering it has no effect because
detections that were unmatched at the original threshold have no
stored ``gt_idx``/``iou`` to re-evaluate.

Parameters:

Name Type Description Default
iou_threshold float

New IoU threshold to apply.

required

Returns:

Type Description
DetectionTable

DetectionTable with updated is_tp.

Raises:

Type Description
ValueError

If the table has no IoU values (a pre-matched table built without iou_col).

frames

frames(
    keys: Sequence[str] = (),
) -> tuple[pl.LazyFrame, pl.LazyFrame]

(detections, image_metadata) for an evaluation grouped by keys.

The frames every metric reads. A sweep holds one matching per IoU threshold, so an evaluation that does not keep the thresholds apart would count each ground truth once per threshold: it is refused here, once, for every metric.

Raises:

Type Description
ValueError

A sweep read without iou_threshold among keys.

stack classmethod

stack(
    tables: dict[float, DetectionTable],
) -> DetectionTable

One sweep table from single matchings of the same data, keyed by their IoU threshold. A single entry is returned as it is.

filter_images

filter_images(
    images: Expr | Iterable[str] | Series,
) -> DetectionTable

Return a copy restricted to a subset of images.

For stratified evaluation: the same metrics over the images of one device, scan type or size bucket. The stored matcher settings (the matching IoU threshold, whether iou holds overlaps) are kept, so the subset is evaluated exactly as the whole table would be.

Parameters:

Name Type Description Default
images Expr | Iterable[str] | Series

Either the ids of the images to keep (an iterable of image_id values), or a predicate over image_metadata (e.g. pl.col("group_id") == "device_a"). Detections follow the (image_id, class_id) metadata rows that are kept.

required

Returns:

Type Description
DetectionTable

Filtered DetectionTable.

Raises:

Type Description
TypeError

For a bare string, which would otherwise be read as a sequence of one-character ids.

to_per_image

to_per_image() -> pl.LazyFrame

One row per image (per (image_id, class_id)) with its detections summarised.

The image metadata (gt_label, weight, n_gts …) plus:

  • max_score: the highest detection score (null without detections);
  • top_is_tp: whether the highest-scoring detection is a true positive. Tied top scores go to the earliest detection (lowest det_idx), the order the matcher ranked them in;
  • best_tp_score: the highest true-positive score (null without one).

LROC's per-image commitment (best_tp / top_scoring) reads these. Every aggregate is native to the streaming engine; no per-image list is built.

collect

collect(
    engine: EngineType = "auto",
) -> tuple[pl.DataFrame, pl.DataFrame]

Materialize both frames in one pass.

Uses pl.collect_all so that a shared upstream subplan (e.g. the matcher's cached contour-extraction/correspond graph, feeding both the detections and the image metadata) executes once across both frames rather than once per frame. Common-subplan elimination is what makes the decode-once, collect-once path hold end to end.

Parameters:

Name Type Description Default
engine EngineType

Polars' engine. "auto" (the default) is polars' own choice: the streaming engine, unless pl.Config.set_engine_affinity says otherwise.

'auto'

Returns:

Type Description
tuple[DataFrame, DataFrame]

Tuple of (detections_df, image_meta_df).

MetricResult dataclass

Base class for all detection metric results.

Subclasses (e.g. PrecisionRecallResult) add metric-specific convenience methods with pre-bound column names. This base carries the PR-curve auc (with optional partial-AUC range and correction), computed through the lazy _auc_expr integral authority — never a second, eager implementation. FROC/LROC curve interpolation does not go through here — those helpers build on the lazy _auc_expr.interpolate_curve_lazy authority directly (see froc_sensitivity_at_fp / froc_summary_table).

Attributes:

Name Type Description
curve DataFrame

DataFrame containing the computed metric curve.

metadata dict[str, Any]

Arbitrary metadata about the computation.

auc

auc(
    *,
    x_col: str,
    y_col: str,
    x_range: tuple[float, float] | None = None,
    correction: CorrectionMethod = None,
) -> float

Compute (partial) AUC under the curve.

Routes through the lazy _auc_expr authority: :func:collapse_curve reduces the curve to strictly-increasing x with the upper-envelope y (a curve carries many rows tied at one x — a run that changes only y leaves x fixed — and collapsing each tie to its max y is deterministic and the ROC/FROC convention), then :func:trapz_auc / :func:partial_auc integrates it in one streaming collect.

Parameters:

Name Type Description Default
x_col str

Column name for the x-axis values.

required
y_col str

Column name for the y-axis values.

required
x_range tuple[float, float] | None

Optional (lo, hi) bounds for partial AUC.

None
correction CorrectionMethod

Optional correction for partial AUC. None returns the raw area. "normalize" divides by the x-range width (mean y-value).

None

Returns:

Type Description
float

Area under the curve (or partial area).

PrecisionRecallResult dataclass

Bases: MetricResult

Precision-Recall curve result.

Attributes:

Name Type Description
curve DataFrame

DataFrame with score, precision, recall, the raw cum_tp/cum_fp counts and the cum_weighted_tp/ cum_weighted_fp masses precision and recall are computed from.

total_gts int

Total ground-truth count for this class.

weighted_gts float

Weighted ground-truth mass Σ n_gts · w — the recall denominator.

class_id str

Class this curve was computed for.

auc

auc(
    *,
    method: Literal[
        "all_points", "11_point", "101_point", "trapezoidal"
    ] = "all_points",
) -> float

Compute Average Precision (AUC of the PR curve).

Parameters:

Name Type Description Default
method Literal['all_points', '11_point', '101_point', 'trapezoidal']

Computation method. "all_points" (default) integrates the monotonically decreasing precision envelope as a step function over every recall the curve reaches (Pascal VOC 2010+). "11_point" / "101_point" average the envelope on a fixed recall grid (Pascal VOC 2007 / COCO). "trapezoidal" computes raw trapezoidal AUC without the monotone-envelope correction. The global envelope is not applied, but points sharing one recall value (a run of false positives leaves recall unchanged) still collapse to the highest precision among them — those points span zero width, so the only question they pose is which precision the trapezoid leaving them uses, and "an arbitrary one" is not an answer.

'all_points'

Returns:

Type Description
float

Average Precision value.

precision_at

precision_at(threshold: float) -> float

Precision at a given score threshold.

Parameters:

Name Type Description Default
threshold float

Score threshold.

required

Returns:

Type Description
float

Precision value.

recall_at

recall_at(threshold: float) -> float

Recall at a given score threshold.

Parameters:

Name Type Description Default
threshold float

Score threshold.

required

Returns:

Type Description
float

Recall value.

ConfusionResult dataclass

True/false positive and false negative counts at a score threshold.

Detection problems have no true negatives (the background is unbounded), so only tp, fp and fn are reported — as raw counts of the table's detections and ground truths, and as weighted masses (each detection or ground truth carrying its (image[, class]) weight). The derived :attr:precision / :attr:recall / :attr:f1 read the weighted masses, so they agree with :func:precision_at_threshold and friends; under unit weights the two sets of numbers are equal. Call :meth:to_dict for the legacy count mapping.

Attributes:

Name Type Description
tp int

True positives (matched detections at or above the threshold).

fp int

False positives (unmatched detections at or above the threshold).

fn int

False negatives (ground truths with no matching detection).

weighted_tp float

Σ w over the true positives.

weighted_fp float

Σ w over the false positives.

weighted_fn float

max(Σ w · n_gts − weighted_tp, 0).

precision property

precision: float

Weighted tp / (tp + fp); 0.0 when nothing is predicted.

recall property

recall: float

Weighted tp / (tp + fn); 0.0 with no ground-truth mass.

f1 property

f1: float

Harmonic mean of :attr:precision and :attr:recall.

to_dict

to_dict() -> dict[str, int]

Return the raw counts as a {"tp", "fp", "fn"} mapping.

Matchers

Matcher

Bases: Protocol

Protocol for detection matchers.

A matcher takes a raw data frame with prediction and ground-truth columns and produces a canonical :class:DetectionTable.

match

match(
    data: LazyFrame | DataFrame,
    *,
    pred_col: str,
    gt_col: str,
    score_col: str | None = None,
    class_col: str | None = None,
    image_id_col: str | None = None,
    weight_col: str | None = None,
    group_col: str | None = None,
    carry: Carry | None = None,
) -> DetectionTable

Produce a canonical DetectionTable from raw predictions and GTs.

Parameters:

Name Type Description Default
data LazyFrame | DataFrame

Input frame with one image/sample per row.

required
pred_col str

Prediction column name (format depends on matcher).

required
gt_col str

Ground-truth column name (format depends on matcher).

required
score_col str | None

Optional confidence score column.

None
class_col str | None

Optional class label column.

None
image_id_col str | None

Optional image identifier column.

None
weight_col str | None

Optional sample weight column.

None
group_col str | None

Optional grouping column.

None
carry Carry | None

Columns to copy into image_metadata, one value per row: names of data's columns, or expressions over them (named by their output name). They are computed in the matcher's own pass over data, so a per-image measure (a pixel Dice of the same heatmap) needs no second reader of the image rows. Refused: a missing column, an unnamed or repeated name, and a name image_metadata defines itself.

None

Returns:

Type Description
DetectionTable

A validated DetectionTable.

ContourMatcher

Match detections from heatmaps (or contours) and GT masks (or contours).

It extracts contours from both predictions and GT masks, scores predictions against the heatmap, and runs greedy IoU matching via .contour.correspond(). Predictions given as contours with their own scores skip the extraction and scoring (see :meth:match).

iou_threshold, extraction_threshold, min_contour_area, min_contour_area_fraction and coverage_tolerance may each be a Polars expression, read per row — e.g. a physical tolerance, coverage_tolerance=5.0 / pl.col("spacing_mm"). A literal is checked here; an expression is checked per row when the query runs.

Parameters:

Name Type Description Default
iou_threshold FloatOrExpr | Sequence[float]

IoU threshold for TP matching (literal or per-row expression), or a sequence of literal thresholds to match once per threshold (a sweep; see :class:BBoxMatcher).

0.5
extraction_threshold FloatOrExpr

Threshold for contour extraction from heatmaps.

0.1
min_contour_area FloatOrExpr

Minimum polygon area of an extracted prediction contour. Contours are traced along pixel edges, so a region's area is its pixel count (for a region with holes, including the hole pixels). The default 0.0 keeps every region.

0.0
auto_resize bool

Whether to resize heatmaps to mask shapes automatically. Ground truth given as contours has no mask to take a size from, so it needs auto_resize=False (the heatmap already in the contours' coordinates).

True
gt_min_contour_area float

Minimum polygon area of a ground-truth contour, independent of min_contour_area. The smallest region, one pixel, has area 1, so the default 1.0 keeps every region.

1.0
match_by str

"iou" (default) pairs by overlap (.contour.correspond). "coverage" pairs by the fraction of each GT contour's boundary inside a prediction or within coverage_tolerance of it (.contour.correspond_by_coverage) — the rule for line-shaped GT (polylines), whose IoU with any region is 0. iou_threshold is then the minimum coverage, and the table's iou column holds coverage.

'iou'
coverage_tolerance FloatOrExpr | None

Pixels a GT sample may lie from a prediction and still count as covered; required with match_by="coverage" and refused otherwise. For a physical tolerance, divide by the pixel spacing.

None
duplicates str

"false_positive" (default) counts a prediction that hits only an already-matched GT as a false positive; "ignore" drops it (the LUNA16/CAMELYON convention).

'false_positive'
score_reduction str

How a detection is scored from the heatmap pixels in its region (label_reduce's reduction): "max" (default), "mean" or "sum". A heatmap that saturates scores most detections at its peak, so they tie under "max".

'max'
score_region_mode str

Which pixels are a detection's region (label_reduce's region_mode): "interior" (default), "boundary" or "bbox".

'interior'
min_contour_area_fraction FloatOrExpr | None

Minimum area of an extracted prediction contour as a fraction of the image's pixel count (extract_contours(min_area_fraction=...)); None (default) applies none.

None

match

match(
    data: LazyFrame | DataFrame,
    *,
    pred_col: str | LazyPipelineExpr,
    gt_col: str | LazyPipelineExpr,
    score_col: str | None = None,
    class_col: str | None = None,
    image_id_col: str | None = None,
    weight_col: str | None = None,
    group_col: str | None = None,
    carry: Carry | None = None,
) -> DetectionTable

Produce a DetectionTable from heatmap (or contour) + mask data.

pred_col / gt_col accept either a column name (any format a polars-cv source supports: nested List[List[...]], VIEW Binary blob, or fixed-size Array[...] — the format is auto-detected) or a pre-decoded LazyPipelineExpr. Passing a pre-decoded expr lets the matcher extend the caller's own decoded buffer instead of decoding a column itself, so a segmentation graph and the contour extraction can share one decode (collapsed by CSE) and stream from a single collect.

pred_col may instead name a contour or contour-set column — for a model that emits masks or polygons with its own per-object scores. Those contours are the detections as they are (no extraction, no heatmap scoring), each scored by score_col; auto_resize must then be False (the contours are in the GT's coordinates).

Parameters:

Name Type Description Default
data LazyFrame | DataFrame

Input frame with one image/sample per row.

required
pred_col str | LazyPipelineExpr

Prediction heatmap column name, or a pre-decoded LazyPipelineExpr producing the heatmap buffer.

required
gt_col str | LazyPipelineExpr

Ground-truth mask column name, or a pre-decoded LazyPipelineExpr producing the mask buffer.

required
score_col str | None

Each prediction contour's score, for contour predictions: List[float] aligned with a contour-set pred_col (a count mismatch fails the query), or a float for a single-contour one. Every contour is kept, a 0.0 score included; a null score fails the query. Refused for heatmap predictions, whose scores come from the heatmap (score_reduction).

None
class_col str | None

Optional class label column for multi-class metrics.

None
image_id_col str | None

Optional image identifier column (defaults to row index).

None
weight_col str | None

Optional sample weight column.

None
group_col str | None

Optional grouping column.

None
carry Carry | None

Columns to copy into image_metadata, one value per row: names of data's columns, or expressions over them (named by their output name). They are computed in the matcher's own pass over data, so a per-image measure (a pixel Dice of the same heatmap) needs no second reader of the image rows. Refused: a missing column, an unnamed or repeated name, and a name image_metadata defines itself.

None

Returns:

Type Description
DetectionTable

Validated DetectionTable.

BBoxMatcher

Match detections from bounding-box lists via IoU matching.

Expects prediction and ground-truth columns as List[Struct{x, y, width, height}] (i.e. List[BBOX_SCHEMA]). Scores should be provided as a separate List[Float64] column aligned with the prediction bboxes.

Matching calls .bbox.correspond(), supplying a confidence order: the IoU of two boxes is computed analytically and the greedy assignment is the one .contour.correspond() uses.

Multi-class data is one row per (image, class); each detection keeps its own row's class.

Parameters:

Name Type Description Default
iou_threshold float | Sequence[float]

IoU threshold for TP matching, or a sequence of them to match once per threshold (COCO's [0.5, 0.55, …, 0.95]): the table then carries an iou_threshold column, and a detection that lost its ground truth at one threshold can claim it at another (see :attr:DetectionTable.iou_thresholds).

0.5

match

match(
    data: LazyFrame | DataFrame,
    *,
    pred_col: str,
    gt_col: str,
    score_col: str | None = None,
    class_col: str | None = None,
    image_id_col: str | None = None,
    weight_col: str | None = None,
    group_col: str | None = None,
    carry: Carry | None = None,
) -> DetectionTable

Produce a DetectionTable from bbox prediction/GT lists.

Parameters:

Name Type Description Default
data LazyFrame | DataFrame

Input frame with one image/sample per row.

required
pred_col str

Prediction bboxes column (List[BBOX_SCHEMA]).

required
gt_col str

Ground-truth bboxes column (List[BBOX_SCHEMA]).

required
score_col str | None

Per-prediction score column (List[Float64]). Required for bbox matching.

None
class_col str | None

Optional class label column.

None
image_id_col str | None

Optional image identifier column.

None
weight_col str | None

Optional sample weight column.

None
group_col str | None

Optional grouping column.

None
carry Carry | None

Columns to copy into image_metadata, one value per row: names of data's columns, or expressions over them (named by their output name). They are computed in the matcher's own pass over data, so a per-image measure (a pixel Dice of the same heatmap) needs no second reader of the image rows. Refused: a missing column, an unnamed or repeated name, and a name image_metadata defines itself.

None

Returns:

Type Description
DetectionTable

Validated DetectionTable.

Raises:

Type Description
ValueError

If score_col is not provided.

PreMatchedAdapter

Adapter for data that already has per-detection TP/FP assignments.

Expects a flat table where each row is one detection with at minimum:

  • score (float) — confidence score
  • is_tp (bool) — whether this detection is a true positive

Plus per-image metadata either via image_meta or derived from the detection rows (n_gts column / defaults).

.. warning::

Without ``image_meta``, the adapter derives the image population
by grouping the detection frame. Images with no detections are
then missing from ``image_metadata``, which silently deletes the
negative population and inflates recall / FP-per-image. Prefer
passing an explicit ``image_meta`` that covers the full evaluation
population whenever any image may carry zero detections.

match

match(
    data: LazyFrame | DataFrame,
    *,
    pred_col: str = "score",
    gt_col: str = "is_tp",
    score_col: str | None = None,
    class_col: str | None = None,
    image_id_col: str | None = None,
    weight_col: str | None = None,
    group_col: str | None = None,
    n_gts_col: str | None = None,
    gt_label_col: str | None = None,
    iou_col: str | None = None,
    det_idx_col: str | None = None,
    image_meta: LazyFrame | DataFrame | None = None,
    carry: Carry | None = None,
) -> DetectionTable

Wrap pre-matched data into a DetectionTable.

Parameters:

Name Type Description Default
data LazyFrame | DataFrame

Input frame with one row per detection.

required
pred_col str

Column with confidence scores (aliased to score).

'score'
gt_col str

Column with TP flag (aliased to is_tp).

'is_tp'
score_col str | None

Alias for pred_col (takes precedence if both set).

None
class_col str | None

Class label column. Required when the data carries a class_id column or image_meta does: omitting it keys every detection by the default class, which matches no classed metadata row.

None
image_id_col str | None

Image identifier column (required, or row index used).

None
weight_col str | None

Optional sample weight column.

None
group_col str | None

Optional grouping column.

None
n_gts_col str | None

Column with per-image GT count.

None
gt_label_col str | None

Column with per-image positive/negative label.

None
iou_col str | None

Optional column with per-detection IoU values. Without it the table's iou is null and :meth:DetectionTable.at_iou_threshold (so IoU-swept mAP) is refused rather than compared against a placeholder.

None
det_idx_col str | None

Optional column with detection index within image.

None
image_meta LazyFrame | DataFrame | None

Optional per-image (or per-image-class) frame that defines the evaluation population. Must use the canonical column names image_id and n_gts; class_id, weight, gt_label and group_id are filled with defaults when absent. When provided this is the sole source of image_metadata — images with zero detections are retained — so it may not be combined with the per-image column arguments below. When omitted, metadata is derived from the detection rows and a UserWarning is emitted.

None
carry Carry | None

Columns to copy into image_metadata: names, or expressions named by their output name, over the frame that defines it — image_meta when given, else the detection rows (each image's first value). Refused: a missing column, an unnamed or repeated name, and a name image_metadata defines itself.

None

Returns:

Type Description
DetectionTable

Validated DetectionTable.

Raises:

Type Description
ValueError

If image_meta is combined with n_gts_col, weight_col, gt_label_col or group_col; if a named column is missing from data; or if the detections' and the population's class_id keys cannot agree (see class_col).

Metric Functions

precision_recall_curve

precision_recall_curve(
    table: DetectionTable,
    *,
    class_id: str | None = None,
    weight_agg: WeightAgg = "first",
) -> PrecisionRecallResult

Compute a weighted precision-recall curve from a DetectionTable.

Detections are bucketed by distinct confidence score and the buckets accumulated in descending score order — one PR point per distinct score, after the whole tied block, so the curve does not depend on the input row order among equal scores. Precision and recall are weighted (see the module docstring).

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
class_id str | None

Restrict to a specific class. None uses all detections.

None
weight_agg WeightAgg

Duplicate-weight resolution policy (see :func:~polars_cv.metrics._weights.resolve_key_weights).

'first'

Returns:

Type Description
PrecisionRecallResult

PrecisionRecallResult with the PR curve.

average_precision

average_precision(
    table: DetectionTable,
    *,
    class_id: str | None = None,
    interpolation: APInterpolation = "all_points",
    weight_agg: WeightAgg = "first",
) -> float

Compute weighted Average Precision for a single class.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
class_id str | None

Restrict to a specific class.

None
interpolation APInterpolation

"all_points" (step-integrated envelope, VOC 2010+), "11_point" (VOC 2007) or "101_point" (COCO).

'all_points'
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
float

AP value in [0, 1].

mean_average_precision

mean_average_precision(
    table: DetectionTable,
    *,
    iou_thresholds: list[float] | None = None,
    interpolation: APInterpolation = "all_points",
    weight_agg: WeightAgg = "first",
) -> float

Compute weighted Mean Average Precision across classes and IoU thresholds.

The mean of :class:~polars_cv.metrics.AP over every (threshold, class) cell of the metadata — MeanOver(AP(interpolation), undefined="zero"): a class without ground truth averages in as 0 (COCO leaves it out instead; see :func:~polars_cv.metrics.mean_ap).

On a table matched at several thresholds (a matcher given a sequence of iou_threshold), each matched threshold is evaluated on its own matching, as COCO does. Any other threshold — and every threshold of a single matching — is evaluated by re-thresholding the stored iou (:meth:DetectionTable.at_iou_threshold), which does not re-match.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
iou_thresholds list[float] | None

IoU thresholds to average over. Defaults to the table's matched thresholds on a sweep, else [0.5] (Pascal VOC).

None
interpolation APInterpolation

AP interpolation method.

'all_points'
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
float

mAP value in [0, 1].

precision_at_threshold

precision_at_threshold(
    table: DetectionTable,
    threshold: float,
    *,
    class_id: str | None = None,
    weight_agg: WeightAgg = "first",
) -> float

Weighted precision at a given score threshold.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
threshold float

Score threshold.

required
class_id str | None

Optional class filter.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
float

Precision value; 1.0 when no weighted detection is at or above the

float

threshold.

recall_at_threshold

recall_at_threshold(
    table: DetectionTable,
    threshold: float,
    *,
    class_id: str | None = None,
    weight_agg: WeightAgg = "first",
) -> float

Weighted recall at a given score threshold.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
threshold float

Score threshold.

required
class_id str | None

Optional class filter.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
float

Recall value; 0.0 when there is no weighted ground-truth mass.

f1_at_threshold

f1_at_threshold(
    table: DetectionTable,
    threshold: float,
    *,
    class_id: str | None = None,
    weight_agg: WeightAgg = "first",
) -> float

Weighted F1 score at a given score threshold.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
threshold float

Score threshold.

required
class_id str | None

Optional class filter.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
float

F1 value in [0, 1].

froc_auc

froc_auc(
    table: DetectionTable,
    *,
    method: Literal[
        "trapezoidal", "mann_whitney"
    ] = "trapezoidal",
    fp_range: tuple[float, float] | None = None,
    correction: CorrectionMethod = "normalize",
    level: Literal["detection", "image"] = "detection",
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
    extrapolate: Extrapolate = "none",
) -> pl.LazyFrame

Compute FROC AUC as a lazy, group-aware frame — one row per group.

The single authority for the FROC integral: the reusable expressions in :mod:polars_cv.metrics._auc_expr. A scalar is froc_auc(table, fp_range=(0.0, 8.0)).collect().item().

The trapezoidal method requires an explicit fp_range. FROC's x-axis is false positives per image — unbounded, with a model-dependent observed maximum — so an all-range area is not comparable across models and there is no reasonable default window. With the default correction="normalize" the result is the mean sensitivity over fp_range (bounded to [0, 1] when the curve is); pass correction=None for the raw partial area.

A curve stops at the highest FP/image any threshold reaches (detections from a thresholded mask exist at one operating point). Past that point the sensitivity was not observed, so by default an fp_range reaching beyond it gives a null AUC — as :func:froc_summary_table gives a null sensitivity there; extrapolate="flat" extends the last sensitivity.

Both families are weighted by image_metadata.weight: the trapezoidal path through the weighted curve, and Mann-Whitney through a weighted rank-sum (collapse_scores + mann_whitney_auc). Unit weights recover the unweighted statistic.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
method Literal['trapezoidal', 'mann_whitney']

"trapezoidal" integrates the curve over fp_range; "mann_whitney" computes a range-free rank statistic.

'trapezoidal'
fp_range tuple[float, float] | None

(lo, hi) FP-per-image window to integrate (required for method="trapezoidal"; rejected for method="mann_whitney").

None
correction CorrectionMethod

Partial-AUC correction (trapezoidal only). "normalize" (default) divides by hi - lo, giving mean sensitivity over the window; None returns the raw partial area.

'normalize'
level Literal['detection', 'image']

Mann-Whitney granularity — "detection" (P(TP > FP), the default) or "image" (P(positive-image score > negative-image)).

'detection'
group_by str | list[str] | None

Optional grouping column(s). None yields a single row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy (see :func:resolve_key_weights).

'first'
extrapolate Extrapolate

How the curve is read past its observed FP/image range: "none" (default) gives null, "flat" extends its endpoints (the LUNA16 convention). See :func:froc_operating_range.

'none'

Returns:

Type Description
LazyFrame

A LazyFrame with [*group_by, auc]; auc is null where

LazyFrame

fp_range leaves the group's curve and extrapolate="none".

Raises:

Type Description
ValueError

If method="trapezoidal" and fp_range is None, or if fp_range is passed with method="mann_whitney".

froc_curve_lazy

froc_curve_lazy(
    table: DetectionTable,
    *,
    group_by: str | list[str] | None = None,
    thresholds: list[float] | None = None,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

Build the FROC curve as a lazy, group-aware frame.

Every cumulative sum restarts per group (:func:grouped_scan) and the weighted denominators are per-group aggregations rather than eager .item() Python floats. With group_by=None the whole table is one group; with a grouping column each group is computed as if on its filtered sub-table (so froc_curve_lazy(group_by="class_id") per class equals froc_curve_lazy on table.filter_class(cid)).

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table produced by a matcher.

required
group_by str | list[str] | None

Optional column(s) partitioning the curve. May name a column on the detections (e.g. class_id) or one only on the metadata (e.g. group_id); metadata-only keys are joined onto detections by image_id.

None
thresholds list[float] | None

Optional explicit score thresholds to keep.

None
weight_agg WeightAgg

How to resolve duplicate metadata weights for one (image[, class]) key (see :func:resolve_key_weights). "first" (default) keeps whichever row survives unique; "min" / "max" / "mean" / "sum" are order-independent. Supplying consistent weights is the caller's responsibility — conflicting weights resolve per this policy rather than raising.

'first'

Returns:

Type Description
LazyFrame

A LazyFrame with ``[*group_by, threshold, tp, fp, fn, total_gts,

LazyFrame

fp_per_image, sensitivity](no group columns whengroup_by`` is

LazyFrame

None).

froc_sensitivity_at_fp

froc_sensitivity_at_fp(
    table: DetectionTable,
    fp_per_image: float,
    *,
    group_by: str | list[str] | None = None,
    thresholds: list[float] | None = None,
    weight_agg: WeightAgg = "first",
    extrapolate: Extrapolate = "none",
) -> pl.LazyFrame

Interpolate FROC sensitivity at a requested FP/image rate, lazily.

Builds the curve via :func:froc_curve_lazy and interpolates it with the shared lazy geometry (upper-envelope) — nothing is collected here; the caller owns the collect.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
fp_per_image float

Target false-positive-per-image rate.

required
group_by str | list[str] | None

Optional grouping column(s); None reads one curve.

None
thresholds list[float] | None

Optional explicit score thresholds to keep.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'
extrapolate Extrapolate

How the curve is read past its observed FP/image range: "none" (default) gives null, "flat" extends its endpoints (the LUNA16 convention). See :func:froc_operating_range.

'none'

Returns:

Type Description
LazyFrame

A LazyFrame [*group_by, fp_per_image, sensitivity], one row per

LazyFrame

group; sensitivity is null when fp_per_image is outside the

LazyFrame

observed range of the curve and extrapolate="none".

LazyFrame

A scalar is froc_sensitivity_at_fp(t, fp).collect().item(0, "sensitivity").

froc_summary_table

froc_summary_table(
    table: DetectionTable,
    fp_rates: list[float] | None = None,
    *,
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
    extrapolate: Extrapolate = "none",
) -> pl.LazyFrame

Sensitivity at standard FP/image operating points, lazily.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
fp_rates list[float] | None

Operating points. Defaults to the standard radiology set.

None
group_by str | list[str] | None

Optional grouping column(s); None reads one curve.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'
extrapolate Extrapolate

How the curve is read past its observed FP/image range: "none" (default) gives null, "flat" extends its endpoints (the LUNA16 convention). See :func:froc_operating_range.

'none'

Returns:

Type Description
LazyFrame

A LazyFrame with [*group_by, fp_per_image, sensitivity], one row

LazyFrame

per group and operating point (sensitivity null off the curve unless

LazyFrame

extrapolate="flat"). The caller collects.

froc_operating_range

froc_operating_range(
    table: DetectionTable,
    *,
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

How far each FROC curve reaches, lazily — one row per group.

A curve's last operating point is the highest FP/image any threshold reaches; beyond it :func:froc_auc and :func:froc_summary_table report null unless asked to extrapolate. Report this alongside them so a truncated curve is visible.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
group_by str | list[str] | None

Optional grouping column(s). None yields a single row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'

Returns:

Type Description
LazyFrame

A LazyFrame with [*group_by, max_fp_per_image, max_sensitivity].

lroc_auc

lroc_auc(
    table: DetectionTable,
    *,
    variant: Literal["best_tp", "top_scoring"] = "best_tp",
    method: Literal[
        "trapezoidal", "mann_whitney"
    ] = "trapezoidal",
    fpf_range: tuple[float, float] | None = None,
    correction: CorrectionMethod = "normalize",
    level: Literal["detection", "image"] = "image",
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
    extrapolate: Extrapolate = "none",
) -> pl.LazyFrame

Compute LROC AUC as a lazy, group-aware frame — one row per group.

The single authority for the LROC integral. A scalar is lroc_auc(table).collect().item().

LROC's x-axis is the false-positive fraction, bounded to [0, 1], so the trapezoidal method integrates the full [0, 1] domain by default (the standard LROC figure of merit) and no range need be given — unlike :func:froc_auc, whose unbounded FP/image axis has no reasonable default. With the default correction="normalize" the result is the mean sensitivity over the window; over the full [0, 1] domain that equals the raw area, so the classic LROC AUC is preserved. Pass correction=None for the raw partial area, or an explicit fpf_range for a narrower window.

Both families are weighted by image_metadata.weight: the trapezoidal path through the weighted curve, and Mann-Whitney through a weighted rank-sum (collapse_scores + mann_whitney_auc). Unit weights recover the unweighted statistic.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
variant Literal['best_tp', 'top_scoring']

"best_tp" or "top_scoring" (curve/image-MW only).

'best_tp'
method Literal['trapezoidal', 'mann_whitney']

"trapezoidal" integrates the curve; "mann_whitney" is the rank statistic.

'trapezoidal'
fpf_range tuple[float, float] | None

(lo, hi) FPF window (trapezoidal only). Defaults to the full (0.0, 1.0) domain; rejected for method="mann_whitney".

None
correction CorrectionMethod

Partial-AUC correction (trapezoidal only). "normalize" (default) divides by hi - lo (mean sensitivity); None returns the raw partial area.

'normalize'
level Literal['detection', 'image']

Mann-Whitney granularity — "image" (default) or "detection".

'image'
group_by str | list[str] | None

Optional grouping column(s). None yields a single row.

None
weight_agg WeightAgg

Duplicate-weight resolution policy (see :func:resolve_key_weights).

'first'
extrapolate Extrapolate

How the curve is read outside its observed FPF range: "none" (default, null) or "flat". The LROC curve always spans [0, 1] (it carries the (1, max sensitivity) endpoint), so this only matters for a window reaching outside it.

'none'

Returns:

Type Description
LazyFrame

A LazyFrame with [*group_by, auc].

Raises:

Type Description
ValueError

If fpf_range is passed with method="mann_whitney".

lroc_curve_lazy

lroc_curve_lazy(
    table: DetectionTable,
    *,
    variant: Literal["best_tp", "top_scoring"] = "best_tp",
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
) -> pl.LazyFrame

Build the LROC curve as a lazy, group-aware frame.

Cumulative sums restart per group (:func:grouped_scan) and the weighted denominators are per-group aggregations.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table produced by a matcher.

required
variant Literal['best_tp', 'top_scoring']

"best_tp" or "top_scoring".

'best_tp'
group_by str | list[str] | None

Optional per-image column(s) partitioning the curve (e.g. group_id).

None
weight_agg WeightAgg

How to resolve duplicate metadata weights for one (image[, class]) key (see :func:resolve_key_weights); the per-image weight is resolved to one value per key so a repeated metadata row cannot make the weighted curve order-dependent.

'first'

Returns:

Type Description
LazyFrame

A LazyFrame with [*group_by, threshold, fpf, sensitivity].

lroc_sensitivity_at_fpf

lroc_sensitivity_at_fpf(
    table: DetectionTable,
    fpf: float,
    *,
    variant: Literal["best_tp", "top_scoring"] = "best_tp",
    group_by: str | list[str] | None = None,
    weight_agg: WeightAgg = "first",
    extrapolate: Extrapolate = "none",
) -> pl.LazyFrame

Interpolate LROC sensitivity at a requested false-positive fraction, lazily.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
fpf float

Target false-positive fraction.

required
variant Literal['best_tp', 'top_scoring']

"best_tp" or "top_scoring".

'best_tp'
group_by str | list[str] | None

Optional grouping column(s); None reads one curve.

None
weight_agg WeightAgg

Duplicate-weight resolution policy.

'first'
extrapolate Extrapolate

How the curve is read outside its observed FPF range: "none" (default, null) or "flat". The LROC curve always spans [0, 1] (it carries the (1, max sensitivity) endpoint), so this only matters for a window reaching outside it.

'none'

Returns:

Type Description
LazyFrame

A LazyFrame [*group_by, fpf, sensitivity], one row per group;

LazyFrame

sensitivity is null when fpf is outside the observed range of

LazyFrame

the curve. The caller collects.

confusion_at_threshold

confusion_at_threshold(
    table: DetectionTable,
    threshold: float,
    *,
    class_id: str | None = None,
    weight_agg: WeightAgg = "first",
) -> ConfusionResult

Compute TP, FP, FN counts and weighted masses at a score threshold.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
threshold float

Score threshold — detections with score >= threshold are considered active.

required
class_id str | None

Optional class filter.

None
weight_agg WeightAgg

Duplicate-weight resolution policy (see :func:~polars_cv.metrics._weights.resolve_key_weights).

'first'

Returns:

Name Type Description
A ConfusionResult

class:ConfusionResult.

Bootstrap confidence intervals (lazy, group-aware)

froc_auc_ci_lazy

froc_auc_ci_lazy(
    table: DetectionTable,
    *,
    group_by: str | list[str] | None = None,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
    method: Literal[
        "trapezoidal", "mann_whitney"
    ] = "trapezoidal",
    fp_range: tuple[float, float] | None = None,
    correction: CorrectionMethod = "normalize",
    level: Literal["detection", "image"] = "detection",
    sample_col: str | None = None,
    extrapolate: Extrapolate = "none",
    strata: str | list[str] | None = None,
    weight_rtol: float | None = None,
    weight_scheme: WeightScheme = "reestimate",
) -> pl.LazyFrame

Lazy, group-aware bootstrap confidence interval for FROC AUC.

Returns a LazyFrame with columns [*group_by, auc, ci_lower, ci_upper] — one row per group (a single row when group_by is None). The auc column is the deterministic point estimate (froc_auc); only the bounds are bootstrapped, and they are seed-reproducible. Nothing is collected here — the caller owns the collect.

Each group resamples within itself (its own images, to its own size), stratified by gt_label. A degenerate group (no positive targets) keeps its point estimate but yields null ci_lower/ci_upper instead of raising. So does a group any of whose replicates is undefined — a resampled curve that stops short of fp_range under extrapolate="none": its AUC was not observed, and is never scored as an empty draw's 0.0 — or degenerate, a draw with no positive image (or, for Mann-Whitney, no negative one), which only a draw that does not fix the gt_label counts can make (a cell-stratified or sample_col draw).

Weighted tables. weight_scheme says what the weights are:

  • "reestimate" (default) — weights estimated from the sample: an importance weight p / q̂ over target distributions (conditional ones included), post-stratification, raking over crossed cells. Units (images, or sample_col entities) with the same weight form a weight cell, and each (group, cell) is redrawn to its own size. The weighted statistics are weight-scale-invariant ratios, so a weight that is a function of its cell's count is then exactly the weight re-estimated inside every replicate — computed from the weights given, with no target distributions needed. gt_label is not crossed with the cells: each cell's positive count is random when the weights were estimated over all images, and fixing it narrows the interval (in simulation, an SE about 10% low and a 95% interval covering 0.90-0.93). Targets conditioned on gt_label give each label its own weights, so their cells already carry the label. A group with a single cell (unit weights) keeps the gt_label stratum, so its draw is the unweighted one.
  • "stratified" — each cell's positive count was fixed by the study design: the draw keeps every (group, gt_label, cell) count.
  • "fixed" — known weights (design weights, a continuous weight): carried unchanged on the gt_label-stratified draw, with no weight cells, so strata and weight_rtol raise.

Weights computed per group or globally are both exact (draws never cross a group). Weights within weight_rtol of each other share a cell. Under sample_col the draw redraws entities, so a replicate's image mix can still wander (entities differ in size): each image's weight is then rescaled by (n_c/N) / (n*_c/N*) — its cell's full-sample over replicate share of the group's images — which is the p / q̂ weight re-estimated on the replicate (exactly 1 for image-level draws). With weight cells, a group with a cell holding a single unit nulls its bounds: that cell has no bootstrap variance, and a continuous weight (every unit its own cell) would otherwise report a zero-width interval — use "fixed" for one. A cell of zero weights (images outside the target) is exempt.

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
group_by str | list[str] | None

Optional grouping column(s). None yields one ungrouped row.

None
n_bootstrap int

Number of bootstrap replicates.

1000
confidence float

Confidence level in (0, 1).

0.95
seed int | None

Optional RNG seed. None maps to a fixed constant, so the bounds are deterministic even without an explicit seed.

None
method Literal['trapezoidal', 'mann_whitney']

"trapezoidal" or "mann_whitney".

'trapezoidal'
fp_range tuple[float, float] | None

(lo, hi) FP-per-image window (required for method="trapezoidal" — froc_auc raises without it).

None
correction CorrectionMethod

Partial-AUC correction (trapezoidal only); "normalize" (default) gives mean sensitivity over fp_range.

'normalize'
level Literal['detection', 'image']

Mann-Whitney granularity — "detection" or "image".

'detection'
sample_col str | None

Optional entity column (e.g. "case_id") to resample at the entity level within each group, expanding to images.

None
extrapolate Extrapolate

Off-curve policy passed to :func:froc_auc for the point estimate and every replicate ("none" or "flat").

'none'
strata str | list[str] | None

Optional metadata column(s) crossed into the weight cells — the columns the weights were computed over (e.g. "vendor"). Needed only when two cells share a weight (several at 1.0).

None
weight_rtol float | None

Relative tolerance within which two weights share a cell (sorted distinct weights split where consecutive values differ by more than weight_rtol times the larger). None is 1e-6, which absorbs arithmetic noise; 0.0 compares exactly.

None
weight_scheme WeightScheme

"reestimate", "stratified" or "fixed": what the weights are, and so how the draw treats them (see above).

'reestimate'

Returns:

Type Description
LazyFrame

LazyFrame with [*group_by, auc, ci_lower, ci_upper].

lroc_auc_ci_lazy

lroc_auc_ci_lazy(
    table: DetectionTable,
    *,
    group_by: str | list[str] | None = None,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
    variant: Literal["best_tp", "top_scoring"] = "best_tp",
    method: Literal[
        "trapezoidal", "mann_whitney"
    ] = "trapezoidal",
    fpf_range: tuple[float, float] | None = None,
    correction: CorrectionMethod = "normalize",
    level: Literal["detection", "image"] = "image",
    sample_col: str | None = None,
    extrapolate: Extrapolate = "none",
    strata: str | list[str] | None = None,
    weight_rtol: float | None = None,
    weight_scheme: WeightScheme = "reestimate",
) -> pl.LazyFrame

Lazy, group-aware bootstrap confidence interval for LROC AUC.

The LROC counterpart of :func:froc_auc_ci_lazy; see it for the shared behavior, weight_scheme included. Returns [*group_by, auc, ci_lower, ci_upper].

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
group_by str | list[str] | None

Optional grouping column(s). None yields one ungrouped row.

None
n_bootstrap int

Number of bootstrap replicates.

1000
confidence float

Confidence level in (0, 1).

0.95
seed int | None

Optional RNG seed (None → deterministic constant).

None
variant Literal['best_tp', 'top_scoring']

"best_tp" or "top_scoring".

'best_tp'
method Literal['trapezoidal', 'mann_whitney']

"trapezoidal" or "mann_whitney".

'trapezoidal'
fpf_range tuple[float, float] | None

(lo, hi) FPF window (trapezoidal only); defaults to the full (0.0, 1.0) LROC domain.

None
correction CorrectionMethod

Partial-AUC correction (trapezoidal only); "normalize" (default) gives mean sensitivity over the window.

'normalize'
level Literal['detection', 'image']

Mann-Whitney granularity — "image" or "detection".

'image'
sample_col str | None

Optional entity column to resample at the entity level.

None
extrapolate Extrapolate

Off-curve policy passed to :func:lroc_auc.

'none'
strata str | list[str] | None

Optional metadata column(s) crossed into the weight cells (see :func:froc_auc_ci_lazy).

None
weight_rtol float | None

Relative tolerance within which weights share a cell (None: 1e-6).

None
weight_scheme WeightScheme

What the weights are — "reestimate", "stratified" or "fixed" (see :func:froc_auc_ci_lazy).

'reestimate'

Returns:

Type Description
LazyFrame

LazyFrame with [*group_by, auc, ci_lower, ci_upper].

average_precision_ci_lazy

average_precision_ci_lazy(
    table: DetectionTable,
    *,
    group_by: str | list[str] | None = None,
    n_bootstrap: int = 1000,
    confidence: float = 0.95,
    seed: int | None = None,
    class_id: str | None = None,
    sample_col: str | None = None,
    strata: str | list[str] | None = None,
    weight_rtol: float | None = None,
    weight_scheme: WeightScheme = "reestimate",
) -> pl.LazyFrame

Lazy, group-aware bootstrap confidence interval for weighted all-points AP.

Returns [*group_by, ap, ci_lower, ci_upper]. The ap column is the deterministic point estimate (the same all-points estimator as :func:~polars_cv.metrics.average_precision); only the bounds are bootstrapped. Nothing is collected here. The AP is weighted by image_metadata.weight and its resample follows weight_scheme, as for :func:froc_auc_ci_lazy (see it for the rules).

Parameters:

Name Type Description Default
table DetectionTable

Canonical detection table.

required
group_by str | list[str] | None

Optional grouping column(s). None yields one ungrouped row.

None
n_bootstrap int

Number of bootstrap replicates.

1000
confidence float

Confidence level in (0, 1).

0.95
seed int | None

Optional RNG seed (None → deterministic constant).

None
class_id str | None

Optional class filter applied before sampling and scoring.

None
sample_col str | None

Optional entity column to resample at the entity level.

None
strata str | list[str] | None

Optional metadata column(s) crossed into the weight cells.

None
weight_rtol float | None

Relative tolerance within which weights share a cell (None: 1e-6).

None
weight_scheme WeightScheme

What the weights are — "reestimate", "stratified" or "fixed" (see :func:froc_auc_ci_lazy).

'reestimate'

Returns:

Type Description
LazyFrame

LazyFrame with [*group_by, ap, ci_lower, ci_upper].