Source code for marivo.analysis.frames.association
"""Typed association analysis results."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal
from pydantic import ConfigDict, model_validator
from marivo.analysis.frames.base import BaseFrame, BaseFrameMeta, _display_column_names
from marivo.render import Card
#: The single lag a summary/evidence represents is selected as the lag with the
#: strongest absolute correlation, preferring the closest lag on ties.
SELECTION_RULE_MAX_ABS = "max_abs_correlation_closest_lag"
#: A single-lag (default) correlate call has no exploration: exactly lag 0 is used.
SELECTION_RULE_SINGLE = "single_lag"
class AssociationResultMeta(BaseFrameMeta):
model_config = ConfigDict(extra="forbid")
kind: Literal["association_result"] = "association_result"
source_refs: list[str]
metric_ids: list[str]
semantic_kinds: list[Literal["scalar", "time_series", "segmented", "panel"]]
semantic_models: list[str]
method: Literal["pearson", "spearman", "kendall"]
alignment: dict[str, Any]
lag_policy: dict[str, Any]
aligned_row_count: int
dropped_row_count: int
correlation: float
best_lag: int = 0
selection_rule: str = SELECTION_RULE_SINGLE
@model_validator(mode="before")
@classmethod
def _infer_selection_rule_for_legacy_artifacts(cls, data: Any) -> Any:
"""Default the selection rule from ``lag_policy`` for legacy artifacts.
Artifacts written before ``selection_rule`` was introduced carry only
``lag_policy.mode``; a range artifact would otherwise wrongly reload as
``"single_lag"`` and mislead agents about the summary's provenance.
"""
if not isinstance(data, dict):
return data
if "selection_rule" in data and data["selection_rule"] is not None:
return data
lag_policy = data.get("lag_policy") or {}
if isinstance(lag_policy, dict) and lag_policy.get("mode") == "range":
data["selection_rule"] = SELECTION_RULE_MAX_ABS
return data
@property
def selected_lag_offset(self) -> int:
"""Return the lag the summary/evidence represents (the selected lag).
Derived from ``best_lag`` (the persisted single source of truth), so the
field never needs separate persistence or schema migration.
"""
return self.best_lag
[docs]
@dataclass(repr=False)
class AssociationResult(BaseFrame):
"""Call marivo.help(AssociationResult) for its public consumption contract."""
meta: AssociationResultMeta
def _repr_identity(self) -> str:
return (
f"AssociationResult ref={self.meta.ref} method={self.meta.method} "
f"r={self.meta.correlation:.2f} lag={self.meta.selected_lag_offset} "
f"rows={self.meta.row_count}"
)
def _card(self) -> Card:
columns = _display_column_names(self._df.columns)
metric_ids = ",".join(self.meta.metric_ids)
status_parts = [
f"method={self.meta.method}",
f"r={self.meta.correlation:.2f}",
f"lag={self.meta.selected_lag_offset}",
f"sel={self.meta.selection_rule}",
f"aligned={self.meta.aligned_row_count}",
f"dropped={self.meta.dropped_row_count}",
f"metrics={metric_ids}",
]
evidence = self._evidence_status_token()
if evidence is not None:
status_parts.append(evidence)
card = Card(identity=self._repr_identity(), available=self._AVAILABLE_ENTRIES).status(
" ".join(status_parts)
)
self._append_evidence_sections(card)
return card.lazy_table(
columns=columns,
rows_provider=self._preview_rows_provider,
row_count=len(self._df),
)