Skip to contents

Many outlier-detection workflows produce per-detector flags (flagged_by_bridge, flagged_by_prob, flagged_by_speed, flagged_by_detour) and then ask a separate question: given the agreement structure across detectors, which fixes are outliers? mt_flag_consensus() is the canonical home for that decision.

Usage

mt_flag_consensus(
  x,
  mode = c("evidence_corroborated", "class_aware", "strict", "majority", "speed_trusted",
    "competence_count", "weighted_evidence", "any", "custom"),
  custom = NULL,
  detectors = c(bridge = "flagged_by_bridge", prob = "flagged_by_prob", speed =
    "flagged_by_speed", detour = "flagged_by_detour"),
  weight_cols = NULL,
  evidence_cols = c(bridge = "loglr_bridge", prob = "loglr_prob", speed = "loglr_speed",
    detour = "loglr_detour"),
  evidence_threshold = 0,
  calibrate = TRUE,
  evidence_C = 4,
  solo_cols = "detour",
  pool_by = NULL,
  gate = 0.5,
  k = 2L,
  bridge_col = NULL,
  prob_col = NULL,
  speed_col = NULL,
  detour_col = NULL
)

Arguments

x

A move2 object that already has per-detector flag columns from a cascade run or from running the four primitives standalone. Missing flag columns are treated as identically FALSE (no flags from that detector).

mode

Character. Which consensus rule to apply. One of "evidence_corroborated" (default), "class_aware", "strict", "majority", "speed_trusted", "competence_count", "weighted_evidence", "any", or "custom". See Consensus modes below. The two evidence modes need the per-detector loglr_* columns (evidence_cols); the others need only the flag columns.

custom

Function used when mode = "custom". Two contracts are accepted. The legacy contract is a function of four logical vectors f(by_bridge, by_prob, by_speed, by_detour). The general contract is a function of the evidence f(flags, weights), where flags is a named logical matrix (one column per detector) and weights is the aligned competence matrix or NULL. Either must return a single logical vector giving the per-fix outlier decision. Ignored unless mode = "custom".

detectors

Named character vector mapping detector names to the flag columns that hold their decisions. Defaults to the four standard cascade detectors. Extend it to bring additional detectors into the decision.

weight_cols

Optional named character vector mapping detector names to columns holding per-fix competence weights in [0, 1]. Used by mode = "competence_count" (and available to a custom function). NULL (default) means every detector is fully competent everywhere, reproducing the unweighted behaviour.

evidence_cols

Named character vector mapping detector names to columns holding per-fix signed log-likelihood-ratios. Used by the "evidence_corroborated" and "weighted_evidence" modes. Defaults to the four loglr_* columns the primitives emit. A missing column or NA entry is treated as an abstention (zero contribution); if none of the columns are present the evidence modes error (run the detectors first, or use a flag-based mode).

evidence_threshold

Numeric. Cut for mode = "weighted_evidence": a fix is flagged when the combined evidence exceeds this value. Default 0 (the data-driven knee between the homogeneous inlier mass and the conspicuous tail; raise for a more conservative cut, lower for a more aggressive one).

calibrate

Logical. For mode = "weighted_evidence", make the per-detector log-LRs commensurable via evidence_C * tanh(loglr / MAD(loglr)) before summing. Default TRUE; this is what keeps a high-magnitude detector from dominating the combined evidence.

evidence_C

Numeric. Saturation ceiling for the calibration: each detector contributes at most \(\pm\)evidence_C to the combined evidence. Default 4.

solo_cols

Character vector of detector names (matching evidence_cols) allowed to flag alone when saturated, under mode = "evidence_corroborated". Default "detour" – the high-specificity geometric detector (an out-and-back is hard to produce by real movement). Other detectors must be corroborated.

pool_by

Optional grouping key for the evidence calibration – the commensurability MAD is computed within each group, the same way the detectors pool their thresholds. Accepts the same forms as the detector primitives: a single column name, or the nested c(outer, inner) form, looked up in mt_track_data(x) and validated by the shared resolver (a missing column errors rather than silently falling back). Because the MAD is a distribution-scale estimate, the calibration is pooled on the outer (distribution-source) level of a nested key; the inner union-unit role does not apply here. NULL (default) groups per track (mt_track_id) so a multi-track object is never calibrated across tracks. Set it to the same column(s) you pooled the detectors on.

gate

Numeric in [0, 1]. Competence threshold for mode = "competence_count": a detector counts at a fix only when its weight is at least gate. Default 0.5.

k

Integer. Vote threshold for mode = "competence_count": flag when at least k competent detectors agree. Default 2L.

bridge_col, prob_col, speed_col, detour_col

Character. Deprecated convenience aliases that override the corresponding entry of detectors. Retained for backward compatibility; prefer detectors. If a column is missing from x, that detector is treated as silent.

Value

The input x with an updated is_outlier logical column. Other per-fix columns are passed through unchanged. If is_outlier already existed on input, it is replaced. For the evidence modes ("evidence_corroborated", the default, and "weighted_evidence") a numeric combined_evidence column is also added.

Details

The function is used internally by mt_clean_track() each iteration, but is also exported so users running their own cascades (or running the four primitives standalone) can apply the same consensus rules to their own per-detector flag columns.

Detector set

The decision is taken over an extensible set of detectors named by the detectors argument (a named character vector mapping a detector name to the flag column that holds its decision). The default names the four standard cascade detectors. Adding a detector is adding an entry to detectors; the built-in modes that count votes (majority, any, the consensus clause of class_aware) generalise automatically, while the named class rules (geometric_spike, etc.) reference detectors by name and ignore columns they do not mention.

Consensus modes

"class_aware"

The default prior to v0.4.0 (round-4 audit, 2026-05-11); the current default is "evidence_corroborated" (below). A fix is flagged if ANY of the following class rules fires:

  • consensus: at least 3 detectors agree

  • geometric_spike: bridge AND detour (symmetric out-and-back is geometrically impossible regardless of state; the autodiff framework is blind to it, so kinematic confirmation would defeat detection)

  • state_anomaly: (bridge OR detour) AND speed

  • kinematic_confluence: (bridge OR detour) AND prob

Each detector contributes where its structural strengths apply, rather than being a symmetric voter. The class taxonomy used downstream (error_class) shares this structure.

"strict"

(bridge OR detour) AND (prob OR speed). At least one geometric AND one kinematic confirmer. Highest precision; on clean synthetic ground truth (CPF tracks) achieves zero false positives.

"majority"

Flag iff at least 2 detectors agree. Tolerates one silent detector; slightly higher recall than "strict", slightly lower precision.

"speed_trusted"

speed OR ((bridge OR detour) AND prob). Trusts the speed detector alone because in its "auto" mode it is already dip-test-validated. Catches extreme-speed transitions that strict misses, at the cost of flagging both endpoints of fast steps.

"competence_count"

Count only the detectors that are locally competent and flag when at least k of them agree. A detector counts at a fix only when its competence weight (supplied via weight_cols) is at least gate. With no weights every detector is fully competent everywhere and this reduces to "majority" at k = 2. This is the mode that lets a detector's vote be silenced exactly where it is structurally blind (e.g. raw speed in the low-speed floor zone).

"evidence_corroborated"

Default. The evidence accumulation of "weighted_evidence" (below), but a fix is flagged only when the combined evidence is positive and either at least two detectors corroborate (their log-LRs are positive) or a high-specificity detector (solo_cols, default the detour detector) is saturated – its calibrated evidence at least 2 MADs beyond its own boundary, i.e. overwhelming within its own distribution. This keeps the lone-strong-detector catches (e.g. a conspicuous out-and-back that only the detour detector sees) while requiring corroboration for detectors that over-react to legitimate sharp turns or behavioural-state changes (bridge, prob). It is the default on principled grounds – unsupervised, exposes the graded combined_evidence behind each call, and extends to new detectors – and on current evidence is not more accurate than "class_aware": the two differ by less than the realisation noise, which is not a demonstrated equivalence. Matches the mt_clean_track cascade default.

"weighted_evidence"

Evidence accumulation. Each detector supplies a signed log-likelihood-ratio (via evidence_cols, e.g. the loglr_* columns the four primitives emit). With calibrate = TRUE (default) each detector's log-LR is first made commensurableevidence_C * tanh(loglr / MAD(loglr)), bounding every detector to \(\pm\)evidence_C so that no detector's native magnitude (e.g. the bridge's \(\eta^2/2\), which can reach millions) dominates – then summed into a combined evidence score. A fix is flagged when that score exceeds evidence_threshold (default 0, the data-driven knee between the homogeneous inlier mass and the conspicuous tail). A missing or NA contribution is an abstention (no evidence either way), not evidence against. The combined score is returned as combined_evidence – the single commensurable scalar behind the decision, and the substrate for an optional one-lever sensitivity control (a user threshold on that column). See DESIGN_evidence_accumulation.md.

"any"

Union of all detectors. Maximum recall, lowest precision. Rarely the right choice for production pipelines.

"custom"

Apply a user-supplied function. See the custom argument.

Composing with custom cascades

If you have built your own cascade that does not use one of the four standard detectors (or uses them under different column names), point detectors at your column names. Detectors with no corresponding column are silently treated as FALSE, so a three-detector cascade (e.g. without detour) gives the expected reduced-form rules.

For consensus rules outside the documented set, use mode = "custom". Inside the function you can use base-R logical operations, rowSums(flags) for vote counts, etc.

See also

mt_clean_track (which uses this function internally each iteration); mt_flag_outliers_bridge, mt_flag_outliers, mt_flag_speed_cap, mt_flag_outliers_detour (the four primitives that emit the per-detector flag columns this function consumes).

Examples

if (FALSE) { # \dontrun{
# Default mode (evidence_corroborated) on a cleaned cascade output
cleaned <- mt_clean_track(track)
cleaned <- mt_flag_consensus(cleaned)   # same as mt_clean_track default

# More conservative: require a geometric AND a kinematic confirmer
cleaned <- mt_flag_consensus(cleaned, mode = "strict")

# User-defined: bridge alone (geometric impossibility only)
cleaned <- mt_flag_consensus(cleaned, mode = "custom",
                             custom = function(b, p, s, d) b)
} # }