Source code for trustlens.api

"""
trustlens.api.
==============
Primary entry point for the TrustLens analysis pipeline.

Responsibilities
----------------
* Expose the core `analyze()` and `quick_analyze()` functions.
* Coordinate the translation of user inputs into the internal format via backends.
* Delegate execution to the core analysis pipeline.

Usage
-----
>>> from trustlens import analyze
>>> report = analyze(model, X_val, y_val, y_prob)
>>> report.show()
"""

from __future__ import annotations

import logging
from typing import Any, Optional

import numpy as np

from trustlens.backends.registry import get_resolver
from trustlens.core.pipeline import _run_analysis_pipeline, _run_regression_pipeline
from trustlens.report import TrustReport

logger = logging.getLogger(__name__)


def _detect_task(y_true: np.ndarray, task: str) -> str:
    """Resolve the analysis task type.

    ``task`` may be ``"classification"`` / ``"regression"`` (explicit, honored
    as-is) or ``"auto"``. Auto-detection errs toward ``"classification"`` and
    only returns ``"regression"`` when the target is clearly continuous — a
    float array that is not integer-valued, or has many distinct values — so a
    discrete label set is never mis-routed.
    """
    if task in ("classification", "regression"):
        return task
    if task != "auto":
        raise ValueError(f"Invalid task {task!r}. Use 'auto', 'classification', or 'regression'.")

    y = np.asarray(y_true)
    if y.dtype.kind == "f":
        n_unique = len(np.unique(y))
        is_integer_valued = bool(np.all(np.isfinite(y))) and bool(np.allclose(y, np.round(y)))
        # Integer-valued floats are class labels at ANY cardinality (a 25-class
        # target encoded as float must not be mistaken for regression), and a
        # small distinct-value set is also label-like. Only clearly-continuous
        # floats route to regression.
        if is_integer_valued or n_unique <= 20:
            return "classification"
        return "regression"
    # Non-float dtypes (ints, strings, bools) default to classification.
    return "classification"


def quick_analyze(
    model=None, X=None, y=None, dataset="iris", framework: Optional[str] = None
) -> TrustReport:
    """
    Zero-friction entry point for TrustLens.
    If no model/data provided, auto-loads a basic dataset to demonstrate output.

    Parameters
    ----------
    model : Any, optional
        A trained machine learning model. If None, a demo model is trained.
    X : np.ndarray, optional
        Validation feature matrix.
    y : np.ndarray, optional
        Ground-truth labels.
    dataset : str, default='iris'
        The demo dataset to load if data is not provided ('iris' or 'breast_cancer').
    framework : str, optional
        Explicitly specify the model framework (e.g., 'sklearn').

    Returns
    -------
    TrustReport
        Populated report object with metrics, plots, and narrative summaries.
    """
    if model is None or X is None or y is None:
        logger.info(f"No model/data provided. Auto-loading {dataset} dataset for demo...")
        if dataset == "iris":
            from sklearn.datasets import load_iris
            from sklearn.ensemble import RandomForestClassifier
            from sklearn.model_selection import train_test_split

            data = load_iris()
            X_all, y_all = data.data, data.target
            # Make it binary for simpler demo
            X_all, y_all = X_all[y_all != 2], y_all[y_all != 2]
            X_train, X, y_train, y = train_test_split(X_all, y_all, test_size=0.3, random_state=42)

            model = RandomForestClassifier(n_estimators=10, random_state=42)
            model.fit(X_train, y_train)
        elif dataset == "breast_cancer":
            from sklearn.datasets import load_breast_cancer
            from sklearn.linear_model import LogisticRegression
            from sklearn.model_selection import train_test_split

            data = load_breast_cancer()
            X_all, y_all = data.data, data.target
            X_train, X, y_train, y = train_test_split(X_all, y_all, test_size=0.3, random_state=42)

            model = LogisticRegression(max_iter=1000, random_state=42)
            model.fit(X_train, y_train)
        else:
            raise ValueError("Supported demo datasets: 'iris', 'breast_cancer'")

    print(f"\nTrustLens Analysis: {dataset}")
    print(f"Status: Loading demo model and {dataset} validation data...")

    report = analyze(model=model, X=X, y_true=y, framework=framework, verbose=False)

    report.show()
    report.summary_plot()
    return report


[docs] def analyze( model: Any, X: np.ndarray, y_true: np.ndarray, y_pred: Optional[np.ndarray] = None, y_prob: Optional[np.ndarray] = None, *, framework: Optional[str] = None, embeddings: Optional[np.ndarray] = None, sensitive_features: Optional[dict[str, np.ndarray]] = None, modules: Optional[list[str]] = None, plugins: Optional[list[str]] = None, class_labels: Optional[np.ndarray] = None, task: str = "auto", prediction_intervals: Optional[tuple[np.ndarray, np.ndarray]] = None, predicted_variance: Optional[np.ndarray] = None, confidence_level: float = 0.95, verbose: bool = True, ) -> TrustReport: """ Run a full TrustLens analysis on a trained model. Parameters ---------- model : Any, optional Trained machine learning model. Can be None if ``y_pred`` or ``y_prob`` are provided manually. X : np.ndarray Validation feature matrix, shape (n_samples, n_features). y_true : np.ndarray Ground-truth labels, shape (n_samples,). y_pred : np.ndarray, optional Predicted class labels, shape (n_samples,). If None, TrustLens will automatically resolve predictions via the backend system. y_prob : np.ndarray, optional Predicted class probabilities, shape (n_samples, n_classes). If None, TrustLens will automatically resolve probabilities via the backend system. framework : str, optional Explicitly specify the model framework (e.g., ``'sklearn'``, ``'xgboost'``, ``'lightgbm'``, ``'catboost'``). If None, TrustLens will attempt to auto-detect the framework. embeddings : np.ndarray, optional Latent representations / embeddings for representation analysis, shape (n_samples, embedding_dim). sensitive_features : dict, optional Mapping of feature name → 1-D array for bias/subgroup analysis. modules : list[str], optional Subset of analysis modules to run. plugins : list[str], optional Names of registered plugins to activate. class_labels : np.ndarray, optional Semantic class labels in the order corresponding to probability columns. Useful for raw backends such as ``xgboost.Booster`` that return ordinal probability columns without a ``classes_`` attribute. task : str, default='auto' Analysis task: ``'auto'`` (detect from ``y_true``), ``'classification'``, or ``'regression'``. Regression routes through the regression reliability metrics (error distribution, interval coverage, error-variance correlation) instead of the classification modules. prediction_intervals : tuple(np.ndarray, np.ndarray), optional ``(lower, upper)`` per-sample prediction-interval bounds (regression only). Enables Prediction Interval Coverage (PICP); omitted ⇒ that metric is skipped. predicted_variance : np.ndarray, optional Per-sample predicted variance / uncertainty score (regression only). Enables the error-variance correlation metric; omitted ⇒ skipped. confidence_level : float, default=0.95 Nominal coverage the supplied ``prediction_intervals`` claim (regression). verbose : bool Print progress updates. Default True. Returns ------- TrustReport Populated report object with metrics, plots, and narrative summaries. Examples -------- End-to-end analysis with a RandomForest classifier: >>> from sklearn.datasets import make_classification >>> from sklearn.ensemble import RandomForestClassifier >>> from sklearn.model_selection import train_test_split >>> from trustlens import analyze >>> >>> # Create a synthetic dataset >>> X, y = make_classification( ... n_samples=500, n_features=10, random_state=42 ... ) >>> >>> # Train / test split >>> X_train, X_test, y_train, y_test = train_test_split( ... X, y, test_size=0.3, random_state=42 ... ) >>> >>> # Train a classifier >>> model = RandomForestClassifier(random_state=42) >>> model.fit(X_train, y_train) >>> >>> # Predict probabilities >>> y_prob = model.predict_proba(X_test) >>> >>> # Run TrustLens analysis >>> report = analyze(model, X_test, y_test, y_prob=y_prob) >>> >>> # Display results >>> report.show() """ if len(y_true) < 30: logger.warning("Small dataset (n < 30) detected. Metrics may be unreliable.") # ------------------------------------------------------------------ # 0. Route by task. Regression skips the classification backend (which # resolves class probabilities) and the classification modules. # ------------------------------------------------------------------ task_type = _detect_task(y_true, task) if task_type == "regression": if y_pred is None: if model is None or not hasattr(model, "predict"): raise ValueError( "Regression analysis needs point predictions: pass y_pred=..., " "or a model that exposes .predict(X)." ) y_pred_resolved = np.asarray(model.predict(X)) else: y_pred_resolved = np.asarray(y_pred) return _run_regression_pipeline( model=model, X=X, y_true=np.asarray(y_true), y_pred=y_pred_resolved, prediction_intervals=prediction_intervals, predicted_variance=predicted_variance, confidence_level=confidence_level, framework=framework or ("manual" if y_pred is not None else None), backend_metadata={"task_type": "regression"}, verbose=verbose, ) # ------------------------------------------------------------------ # 1. Resolve predictions via Backend Registry # Short-circuit if both overrides are provided if y_pred is not None and y_prob is not None: framework = "manual" resolver = get_resolver(model, framework=framework) resolved_class_labels = np.asarray(class_labels) if class_labels is not None else None bundle = resolver( model, X, y_pred=y_pred, y_prob=y_prob, class_labels=resolved_class_labels, ) # ------------------------------------------------------------------ # 2. Delegate to Core Pipeline # ------------------------------------------------------------------ return _run_analysis_pipeline( model=model, X=X, y_true=y_true, y_pred=bundle.y_pred, y_prob=bundle.y_prob, framework=bundle.framework, backend_metadata=bundle.metadata, class_labels=bundle.class_labels, embeddings=embeddings, sensitive_features=sensitive_features, modules=modules, plugins=plugins, verbose=verbose, )