Skip to content

Comparative Method Selection

When multiple fdars methods are plausible candidates for the same dataset — for example, choosing between standard FPCA and elastic FPCA, or comparing GCV-based smoothers — the advisor's comparative selection surface ranks them by a shared diagnostic metric and identifies the fdars-determined winner before any LLM call.

Comparative selection flow: N candidates through per-candidate build_diagnostics, fdars deterministic sort to winner, LLM narration-only

Method

compare_methods accepts N labeled candidate inputs, builds build_diagnostics for each one, and sorts the resulting diagnostics by a shared metric. The ranking is fully deterministic and fdars-authoritative: the winner is fixed by the sort before the LLM is ever invoked. The LLM only narrates the already-computed ranking — it cannot change the winner.

Fdars-authoritative winner invariant (COMPARE-01): The same inputs always produce the same winner. The sort is stable so ties are broken by candidate insertion order. Two calls on the same inputs return an equal result["winner"].

Incommensurability guard (COMPARE-03): If candidates span more than one task family (e.g. mixing clustering and regression results), or if the ranking metric is absent from any candidate's diagnostics, compare_methods raises ValueError before producing any ranking. No candidate is silently dropped or compared on a different metric.

LLM narration path (run_llm=True): Each candidate's diagnostics are sent to the LLM in separate labeled blocks — never flat-merged into a single dict. This preserves per-candidate provenance so the LLM can attribute evidence correctly (COMPARE-02). Union grounding is checked once against all candidates' diagnostics combined, preventing fabrication while allowing cross-candidate narration.

Worked example

The fence below creates two synthetic smoothing candidates (different n_basis values) and ranks them offline without an LLM call. No ANTHROPIC_API_KEY is needed — the fence runs fully offline in the docs build.

import numpy as np
from fdars.advisor import build_diagnostics, compare_methods

# Small synthetic smoothing results — two candidate B-spline fits
rng = np.random.default_rng(0)
t = np.linspace(0, 1, 60)

# Candidate A: lower GCV (better fit)
diag_a = {
    "method": "smoothing",
    "lambda_values": [0.01, 0.1, 1.0],
    "gcv_curve": [0.12, 0.09, 0.14],
    "edf": [18.0, 12.0, 8.0],
    "gcv_aic_approx": None,
    "gcv_bic_approx": None,
    "optimal_lambda": 0.1,
    "optimal_gcv": 0.09,
    "optimal_edf": 12.0,
}

# Candidate B: higher GCV (worse fit)
diag_b = {
    "method": "smoothing",
    "lambda_values": [0.01, 0.1, 1.0],
    "gcv_curve": [0.18, 0.15, 0.22],
    "edf": [20.0, 15.0, 10.0],
    "gcv_aic_approx": None,
    "gcv_bic_approx": None,
    "optimal_lambda": 0.1,
    "optimal_gcv": 0.15,
    "optimal_edf": 15.0,
}

result = compare_methods(
    {"n_basis_10": diag_a, "n_basis_15": diag_b},
    metric="optimal_gcv",  # lower is better
    run_llm=False,
)

print(f"Task family:  {result['method']}")
print(f"Metric:       {result['metric']}")
print(f"Winner:       {result['winner']}")
for rank, entry in enumerate(result["ranking"], 1):
    print(f"  Rank {rank}: {entry['label']}  optimal_gcv={entry['metric_value']:.4f}")
print("FDARS_FENCE_OK")
Task family: smoothing Metric: optimal_gcv Winner: n_basis_10 Rank 1: n_basis_10 optimal_gcv=0.0900 Rank 2: n_basis_15 optimal_gcv=0.1500 FDARS_FENCE_OK

Functions

compare_methods

compare_methods(candidates, method=None, *, metric=None, domain_context="",
                model="claude-opus-4-8", provider=None, run_llm=True,
                argvals=None, **kwargs) -> dict

Rank candidate fdars methods by a shared diagnostic metric. The winner is determined by a deterministic fdars-computed sort — the LLM never chooses the winner.

Candidate input forms:

Form Example
dict mapping label → value {"cand_a": diag_a, "cand_b": diag_b}
List of (label, value) tuples [("cand_a", diag_a), ("cand_b", diag_b)]
List of spec dicts with "label" key [{"label": "a", "value": diag_a}, ...]

Each value is either a pre-built diagnostics dict (has a "method" key — passed through unchanged) or a raw fdars result dict (no "method" key — passed to build_diagnostics(value, method, ...)). When passing raw result dicts, the method parameter is required.

Parameters

Parameter Type Default Description
candidates dict or list — Labeled candidate inputs (see forms above)
method str or None None Task family (required when any candidate is a raw result dict without a "method" key)
metric str or None None Ranking metric key. When omitted the per-family default is used (see table below)
domain_context str "" Free-text domain description forwarded to LLM narration (run_llm=True only)
model str "claude-opus-4-8" LLM model identifier (run_llm=True only)
provider str or Provider or None None LLM provider (run_llm=True only)
run_llm bool True When False, return the raw deterministic ranking dict offline; no LLM call
argvals array_like None Forwarded to build_diagnostics for raw result dicts (e.g. clustering/alignment aspects require argvals for distance metrics)

Returns

{
    "method": "<task family str>",
    "metric": "<metric key str>",
    "ranking": [
        {"label": <str>, "method": <str>,
         "metric_value": <float>, "diagnostics": <dict>},
        ...  # ordered best -> worst
    ],
    "winner": "<best label str>",
    # "advice": Advice  (only present when run_llm=True)
}

Per-family default metrics

Task family Default metric Direction
"clustering" mean_amplitude_separation higher is better
"smoothing" optimal_gcv lower is better
"basis" optimal_edf lower is better
"regression_cv" min_cv_error lower is better
"regression" r_squared higher is better
"scoring" functional_mse lower is better
"fpca" cumulative_variance_explained higher is better

Raises

  • ValueError — empty candidates, unknown or missing metric, mixed task families, or metric absent from any candidate (COMPARE-03).
  • GroundingViolationError — when run_llm=True and the LLM narration cites a value absent from all candidates' diagnostics.

Caveats

All candidates must share the same task family. Comparing, for example, a clustering result against a regression result raises ValueError before any ranking. The guard is fail-closed: no partial ranking is returned.

The LLM narrates; fdars ranks. When run_llm=True the LLM receives the fdars-computed ranking and winner, then narrates them. It cannot override the winner declared in result["winner"].

argvals is required for distance-based metrics. For "clustering" or "alignment" aspects, mean_amplitude_separation (and related distance keys) are None when argvals is not supplied. Passing argvals to compare_methods forwards it to each build_diagnostics call automatically.

cumulative_variance_explained (fpca) is list-valued. The ranker extracts the last element (total cumulative variance) as the scalar for comparison. Ensure the FPCA result includes enough components for a meaningful last-element value.