Combine per-detector flag columns into a single outlier decision
Source:R/mt_flag_consensus.R
mt_flag_consensus.RdMany 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
move2object that already has per-detector flag columns from a cascade run or from running the four primitives standalone. Missing flag columns are treated as identicallyFALSE(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-detectorloglr_*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 vectorsf(by_bridge, by_prob, by_speed, by_detour). The general contract is a function of the evidencef(flags, weights), whereflagsis a named logical matrix (one column per detector) andweightsis the aligned competence matrix orNULL. Either must return a single logical vector giving the per-fix outlier decision. Ignored unlessmode = "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 bymode = "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 fourloglr_*columns the primitives emit. A missing column orNAentry 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. Default0(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 viaevidence_C * tanh(loglr / MAD(loglr))before summing. DefaultTRUE; 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_Cto the combined evidence. Default4.- solo_cols
Character vector of detector names (matching
evidence_cols) allowed to flag alone when saturated, undermode = "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
MADis 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 nestedc(outer, inner)form, looked up inmt_track_data(x)and validated by the shared resolver (a missing column errors rather than silently falling back). Because theMADis 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 formode = "competence_count": a detector counts at a fix only when its weight is at leastgate. Default0.5.- k
Integer. Vote threshold for
mode = "competence_count": flag when at leastkcompetent detectors agree. Default2L.- bridge_col, prob_col, speed_col, detour_col
Character. Deprecated convenience aliases that override the corresponding entry of
detectors. Retained for backward compatibility; preferdetectors. If a column is missing fromx, 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 agreegeometric_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 speedkinematic_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
kof them agree. A detector counts at a fix only when its competence weight (supplied viaweight_cols) is at leastgate. With no weights every detector is fully competent everywhere and this reduces to"majority"atk = 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 gradedcombined_evidencebehind 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 themt_clean_trackcascade default."weighted_evidence"Evidence accumulation. Each detector supplies a signed log-likelihood-ratio (via
evidence_cols, e.g. theloglr_*columns the four primitives emit). Withcalibrate = TRUE(default) each detector's log-LR is first made commensurable –evidence_C * tanh(loglr / MAD(loglr)), bounding every detector to \(\pm\)evidence_Cso 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 exceedsevidence_threshold(default 0, the data-driven knee between the homogeneous inlier mass and the conspicuous tail). A missing orNAcontribution is an abstention (no evidence either way), not evidence against. The combined score is returned ascombined_evidence– the single commensurable scalar behind the decision, and the substrate for an optional one-lever sensitivity control (a user threshold on that column). SeeDESIGN_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
customargument.
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)
} # }