Analysis Workflow
This page is the deeper reference for the agent-guided journey introduced in First agent-guided analysis.
Give the agent the question, not the procedure
Section titled “Give the agent the question, not the procedure”Tell the agent what you want to understand, along with any metric, comparison, scope, or focus that is already known. The agent chooses the typed operators, keeps the work in an analysis session, and records the evidence.
For example:
Analyze why completed-order revenue fell last quarter compared with the same quarter a year earlier. Start with regional differences and report the main conclusion, supporting evidence, and limitations.
You do not need to choose operators, request show() calls, or plan checkpoints.
Join the investigation when meaning could change
Section titled “Join the investigation when meaning could change”The agent should continue independently while the next step stays within the confirmed metric and question. It should pause when it needs to change the metric, population, comparison, or a business assumption in a way that could materially change the conclusion.
Confirm or correct that choice in business terms. If the semantic definition
itself is missing or wrong, the affected typed branch stops. The agent may use
terminal md.raw_sql(...) with explicit temporary inferred semantics, but it
must not change the semantic layer during analysis and must request approval for
the smallest durable fix at closeout.
Review the handoff
Section titled “Review the handoff”At the end, check that the conclusion answers the question, the important claims have visible evidence, and the stated limitations do not make the conclusion unsuitable for the intended decision. The technical mechanics are documented below for readers who need to inspect or integrate the runtime.
Operator and artifact reference
Section titled “Operator and artifact reference”Every Marivo analysis starts from governed semantic inputs and runs as a
write-run-read loop: an agent writes an intent (an operator call plus its
fields), runs it, and reads a typed result before deciding the next step.
Metric analysis starts with session.observe(...); Event journey analysis
starts with session.events.match(...); replay-based Lifecycle analysis starts
with session.lifecycle.replay(...).
The runtime pieces are:
- A session holds the guiding question, the semantic catalog, and the persisted results of every step.
- An operator call is one deterministic analysis step —
observe,events.match,compare,attribute,discover.<objective>, and the other core operators — with its parameters. The parameters are the analysis specification. - An artifact is the typed result of an operator. Artifacts are the boundary between steps: each operator consumes specific artifact families and produces another.
import marivoimport marivo.analysis as mvimport marivo.semantic as msWhen analysis needs a newly authored or changed object, close out that change
first: md.inspect(...) → explicit scope → inspection.sample(...) once →
snapshot projections → one Python declaration → current entry →
catalog.verify(entry) → catalog.preview(entry, using=snapshot) → zero-query
catalog.readiness(refs=[entry]). The equivalent exact-ref form remains valid.
Routine analysis of unchanged objects does not repeat this authoring evidence
flow. Uncommon formats and semantic judgments remain agent-owned.
Open a session
Section titled “Open a session”session = mv.session.get_or_create(name="revenue-investigation", question="Why did Q4 drop?")catalog = session.catalogrevenue = catalog.metrics.get("sales.revenue")region = catalog.dimensions.get("sales.orders.region")
revenue.details().show() # definition, candidate axes, and measure lineageregion.details().show()get_or_create is idempotent: it attaches to an existing session of that name or
creates one, and sets it current. The narrow session API is get_or_create,
current(), list(), recent(), inspect(name), and delete(name).
repr(session) points to session.show(). Call it for a bounded state card
with the question, read/write status, report timezone, timestamps, and the
registered direct operators, namespaces, and catalog/job/frame inspection
paths; session.render() returns the same text without writing stdout.
Use catalog.domains.show() and scoped navigation to discover entries, then inspect
every metric, dimension, and time dimension used by the analysis with
.details().show(). The details output is where authored ai_context, metric
composition, effective entities, candidate axes, and measure lineage appear.
Candidate axes are static discovery; observe remains the authority for
relationship and fanout validity. .contract() is reserved for mechanical
verify/preview/readiness continuations, while .render() returns the same bounded
card that .show() prints. If a required object is missing or disputed, stop the
affected typed branch before the first observe; terminal raw SQL may continue
provisionally, while semantic authoring waits for closeout approval.
MetricFrame.show() makes these persisted execution facts visible: the
observation window, grain, axes and slices; aggregation, additivity, and
reaggregation status; and any fold strategy, status axis, sample interval, or
snapshot identity keys. Sampled folds include expected-slot coverage.
Unsampled snapshot selection explicitly reports that expected-sample coverage
is not applicable. A recovered frame renders the same facts without re-querying.
Match an Event journey
Section titled “Match an Event journey”Event analysis starts from typed participant roles and an ordered EventPattern. Each step gets a stable snake-case key; the role carries its Event and subject Entity, and the subject Entity’s primary key supplies identity.
cart_created = catalog.events.get("commerce.cart_created")checkout_started = catalog.events.get("commerce.checkout_started")payment_succeeded = catalog.events.get("commerce.payment_succeeded")
cart_user = ms.participant_role(event=cart_created.ref, name="user")checkout_user = ms.participant_role(event=checkout_started.ref, name="user")payment_user = ms.participant_role(event=payment_succeeded.ref, name="user")
cart_step = mv.step(participant=cart_user, key="cart")checkout_step = mv.step(participant=checkout_user, key="checkout")payment_step = mv.step(participant=payment_user, key="payment")
pattern = mv.sequence(cart_step, checkout_step, payment_step)journeys = session.events.match( pattern=pattern, cohort_window=mv.TimeScope(start=start, end=end), completion_through=followup_end, matching=mv.first_per_subject(),)The cohort window is half-open: only first-step occurrences in [start, end)
anchor journeys. completion_through is inclusive follow-up and must be at
least end. An optional ready SubjectSet further restricts the subject
membership; it does not replace or infer the cohort window.
Use an explicit repeated-attempt policy when every first-step occurrence should
anchor an attempt. exclusive assigns one completion to at most one open
attempt; shared permits one completion to close multiple eligible attempts:
exclusive_attempts = session.events.match( pattern=pattern, cohort_window=mv.TimeScope(start=start, end=end), completion_through=followup_end, matching=mv.every_start(completion_assignment="exclusive"),)shared_attempts = session.events.match( pattern=pattern, cohort_window=mv.TimeScope(start=start, end=end), completion_through=followup_end, matching=mv.every_start(completion_assignment="shared"),)EventFrame[journey] is a dense long table with one row for every
journey × pattern step. A missing later step remains present with null event,
time, and elapsed values. It is incomplete only when every required Event is
known complete through the follow-up bound; otherwise it is
coverage_censored.
A backend may provide authoritative Event completeness through
marivo_event_watermark(request). Marivo does not infer that watermark from
the maximum observed Event time, query time, or an SLA. When authoritative
coverage is unavailable, the caller may attach an exact, bounded assumption:
coverage = mv.declared_complete_through( inputs=(cart_created, checkout_started, payment_succeeded), through=followup_end, rationale="Warehouse reconciliation completed through followup_end.",)journeys = session.events.match( pattern=pattern, cohort_window=mv.TimeScope(start=start, end=end), completion_through=followup_end, matching=mv.first_per_subject(), completeness=(coverage,),)Reduce Event journeys and compose a typed cohort
Section titled “Reduce Event journeys and compose a typed cohort”A first-per-subject journey has three Phase 2 continuations. Funnel reduction uses persisted occurrence assignment and performs no Event query. With axes, only governed subject-Dimension enrichment at the first-step occurrence time may query a datasource:
funnel = session.events.funnel( journeys, axes=[acquisition_channel], analysis_purpose="Measure checkout conversion by entry channel.",)
elapsed = session.events.time_to_event( journeys, start_step=checkout_step, end_step=payment_step, analysis_purpose="Measure checkout-to-payment elapsed time.",)
dropouts = session.select_subjects( journeys, selection=mv.dropped_before(step=payment_step),)EventFrame[funnel] keeps additive counts, censoring-aware rates, and grouped
reconciliation evidence. EventFrame[time_to_event] keeps the exact source
start/end assignment and never rematches an alternative completion.
SubjectSet exposes only its governed subject_identity tuples. Raw identities
do not appear in cards, jobs, evidence, structured errors, or consumer metadata.
Only a ready SubjectSet can scope observe or a later events.match:
dropout_revenue = session.observe(revenue, cohort=dropouts)return_journeys = session.events.match( pattern=return_pattern, cohort=dropouts, cohort_window=mv.TimeScope(start=start, end=end), completion_through=followup_end, matching=mv.first_per_subject(),)Subjects whose loss remains coverage_censored are excluded from selection and
counted in SubjectSet metadata. Such a SubjectSet remains readable and
recoverable, but cohort admission raises event_coverage_unknown. Funnel and
dropped_before require first_per_subject; time-to-event also accepts
repeated-attempt journeys because it preserves one row per assigned attempt.
Observed watermarks take precedence over declarations. The artifact and its
quality/evidence records retain each input’s
observed_watermark | declared_complete | unknown basis and the aggregate
observed_watermark | declared_complete | mixed | unknown basis.
Replay a normative lifecycle
Section titled “Replay a normative lifecycle”Lifecycle analysis applies one exact current StateModel to its modeled Event
stream. The model owns states and legal transitions; the analysis call owns the
window, explicit seed, optional cohort, and any completeness declaration.
Inspect marivo.help("analysis.lifecycle.replay") before first use rather than
copying a cached signature.
order_model = catalog.state_models.get("sales.order_lifecycle")
history = session.lifecycle.replay( order_model, window=mv.TimeScope(start=start, end=end), seed=mv.from_inception(), analysis_purpose="Reconstruct governed order state history.",)The replay window is timezone-aware and half-open. mv.from_inception() is an
explicit required choice: replay starts each subject at its first qualifying
modeled inception, evaluates all modeled occurrences before window.end, and
emits only intervals overlapping the requested window. A StateModel without an
inception can be loaded for future projection seeding but is not ready for this
replay path.
Each distinct trigger Event is queried at most once. Event predicates, participant-role resolution, identity validation, deterministic ordering, and completeness use the same Event core as journey matching. Completeness declarations may cover only Events consumed by the selected StateModel, and an authoritative provider watermark takes precedence. Events outside the model are neither queried nor violations.
Legal triggers change state. A modeled trigger that is illegal from the current state leaves the state unchanged and is retained in the replay’s private violation trace. Same-time cross-Event occurrences fail when their order would change the resulting state or violation outcome.
LifecycleFrame[history] contains ordered, non-overlapping state intervals.
Its interval status is completed, right_censored, or
coverage_censored. Read history.show() for the bounded current state and
history.contract() for mechanically valid continuations.
Reduce Lifecycle history and select a state cohort
Section titled “Reduce Lifecycle history and select a state cohort”Lifecycle reducers consume the committed history without querying Event sources or replaying the StateModel:
weekly_state = session.lifecycle.distribution( history, at=(week_1_end, week_2_end), axes=[account_region],)transition_counts = session.lifecycle.transitions(history)dwell = session.lifecycle.dwell(history)violations = session.lifecycle.violations(history)Distribution is dense over every requested instant and declared model state. Governed subject axes resolve at each instant, and grouped counts reconcile to the ungrouped state population. Transitions includes every distinct modeled state pair, even when its count is zero. Dwell includes every declared state; duration statistics use only completed clipped intervals while censored intervals remain explicit counts. Violations is a typed copy of the committed trace, not a policy-breach, quality-failure, or causal label.
Select subjects through an exact typed model-state handle, never a bare state string:
paid_state = ms.model_state(model=order_model.ref, name="paid")paid_at_end = session.select_subjects( history, selection=mv.in_state(paid_state, as_of=end),)Subjects whose state is unknown or coverage-censored at the instant are
excluded and retained as censoring metadata. The resulting SubjectSet remains
readable, but only a ready set can scope a later observe, events.match, or
lifecycle.replay. Use focused marivo.help("analysis.in_state") and the
source artifact contract for the exact current admission rules.
How an intent is specified
Section titled “How an intent is specified”Operators take exact current entries/refs, closed expressions, and a few shared value objects. You will reuse these across almost every intent:
| Value | Shape | Meaning |
|---|---|---|
| metric input | catalog.metrics.get("sales.revenue") | Exact current MetricEntry or Ref[metric], or a closed RuntimeMetricExpr for observe; stale/cross-catalog entries, generic refs, and strings are rejected. |
| dimension input | catalog.dimensions.get("sales.orders.region") | Exact current dimension/time-dimension entry or matching ref (see Semantic refs). Used for dimensions, slice_by keys, and axes. |
time_scope | {"start": "2026-10-01", "end": "2027-01-01"} | Half-open time range — start inclusive, end exclusive. |
grain | "day" | "week" | "month" | "quarter" | "year" | "hour" | … | Time bucket size. Present ⇒ time series or panel. |
dimensions | [region, country] | Segment axes. In v1 all must resolve to the metric’s entity. |
slice_by | {region: "US"} or {amount: {"op": ">", "value": 100}} | Pre-aggregation row filter (see ops below). |
AlignmentPolicy | mv.window_bucket() | How two windows are paired for compare / correlate. |
Treat a supplied time_scope.end literally. Marivo does not include that
bound, and an agent should not advance it to reinterpret the request as an
inclusive range. For example, end="2026-06-30" excludes June 30; use
end="2026-07-01" only when the intended range includes all of June.
The result’s semantic kind follows from grain and dimensions: scalar
(neither), time_series (grain only), segmented (dimensions only), or panel
(both).
slice_by predicate operators
Section titled “slice_by predicate operators”Keys are catalog dimensions; values are a scalar (==), a list (in), or a
structured {"op": ..., "value": ...} form:
| Form | Meaning |
|---|---|
"US" | == (equality) |
["US", "CA"] | in (membership) |
{"op": "!=", "value": "US"} | not equal |
{"op": ">", "value": 100} (>=, <, <= likewise) | numeric comparison |
{"op": "between", "value": ["2026-07-01", "2026-09-30"]} | inclusive range (exactly two values) |
Core operators
Section titled “Core operators”observe → MetricFrame
Section titled “observe → MetricFrame”The starting point for any analysis: materialize a metric over a time range and/or segments.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
metrics | `MetricEntry | Ref[metric] | RuntimeMetricExpr`, or non-empty sequence | Yes |
time_scope | dict | No | None | Half-open {"start", "end"} window. |
grain | grain | No | None | Time bucket; present ⇒ time series or panel. |
dimensions | entry/ref sequence | No | None | Exact current dimension/time-dimension entries or refs. |
slice_by | entry/ref-keyed mapping | No | None | Global pre-aggregation row filter. |
time_dimension | time-dimension entry/ref | No | entity default | Pick the time axis when the entity declares several. |
expect_shape | shape | No | None | Guard; raises before backend work if the predicted shape differs. |
Current entries are also accepted for Session.attribute(axes=...), discovery
search-space/peer axes, and transform slice/rollup axes. Every qualifying call
normalizes entries to canonical refs before planning, persistence, evidence, or
replay; catalog.require(ref) remains the strict ref-only path for identity
recovered from configuration, logs, or persisted state.
current = session.observe( revenue, time_scope={"start": "2026-10-01", "end": "2027-01-01"}, grain="month", dimensions=[region],)When a temporal request has no candidate time dimension, receives an ordinary dimension as its time axis, has ambiguous candidates, resolves different implicit axes across metric roots, or requests an incompatible encoding/grain, Marivo fails before backend execution whenever compiled catalog facts are sufficient. The structured repair exposes exact current candidates without guessing a time axis; a retry appears only when the replacement is mechanically unique.
Pass a sequence through metrics= to observe several same-scope metrics in one
frame. metrics also accepts a single metric, while the single-metric
positional form remains valid. The former metric= keyword is not an alias.
Marivo requires temporal roots to resolve the same exact time-dimension ref, fuses
same-datasource metrics into one query, and outer-joins compatible
cross-datasource metrics on that shared time axis. Use frame.metric(id) to
project an arity-1 frame for drill-down on a single metric.
Every public read of a single-metric MetricFrame uses the metric short name for
its value column, including show(), columns, contract(), indexing, and
to_pandas(). The name stays stable after transforms. When it collides with an
axis column, Marivo uses the qualified metric id instead and appends a
deterministic #N suffix if that name is also occupied. Multi-metric frames
continue to use one column per metric, so frames can be merged without
arity-specific renames. Read frame.value_columns for the exact public name(s).
Every artifact card also prints output_columns, the exact ordered names
returned by .columns and to_pandas(). The mechanical contract exposes the
same tuple as contract.output_columns, derived directly from
contract.artifact_schema.columns. For Metric, Event, and Lifecycle artifacts,
contract.semantic_inputs records the retained semantic role/path, any bound
output column, the exact session.catalog.<collection>.get("<path>")
reacquisition call, and its focused catalog help target. The card renders the
same acquisition calls, so agents do not need to infer a catalog collection
from a result column.
Typed transforms still use the internal canonical value column. For example,
frame.transform.filter(predicate=lambda data: data["value"] > 0) remains valid;
afterward, read the result with filtered[filtered.value_columns[0]] or
filtered.to_pandas().
report = session.observe( metrics=[revenue, catalog.metrics.get("sales.total_orders")], time_scope={"start": "2026-10-01", "end": "2027-01-01"}, grain="month",)revenue_only = report.metric("sales.revenue")When a downstream capability requires one metric, a multi-metric
frame.contract() exposes one executable full-id frame.metric(...)
projection for every carried root. It does not choose one.
When the catalog does not contain the required analysis-only caliber, build a
closed expression and still materialize it through session.observe(...):
failed = mv.runtime_metric.aggregate( failed_requests_measure, agg="sum", slice_by={state: "failed"}, label="Runtime failed requests",)weighted_latency = mv.runtime_metric.weighted_mean( latency_measure, request_count_measure, label="Runtime weighted latency",)failure_rate = mv.runtime_metric.ratio( failed, requests, zero_division="null", label="Runtime failure rate",)frame = session.observe(failure_rate, time_scope=window, grain="day")Runtime expressions may recursively contain runtime expressions and catalog
Ref[metric] children. runtime_metric.slice(..., by=...) is branch-local;
observe(..., slice_by=...) is global. Both catalog and runtime roots lower to
one canonical graph (maximum depth 10, maximum 256 submitted pre-CSE
occurrences) and one model/source domain. Labels affect presentation only, not
value identity or catalog authority. Every runtime_metric constructor,
including nested expressions, requires a non-empty label. The label becomes
the stable public value-column handle when the expression is materialized as an
observed root. Downstream operators use persisted frame state rather than
catalog/runtime origin.
runtime_metric.weighted_mean(value, weight) performs row-level multiplication
and paired aggregation automatically. Both measures must have the same entity
and physical row grain, and the weight must be additive; no precomputed
SUM(value * weight) measure is required.
Every observed root and mixed forest persists a recursive component graph.
Use frame.components() to inspect node roles, evaluator contracts, quality,
coverage references, and governed leaf lineage. A separate component_ref
exists only when the root also supports numerical decomposition.
Observed additivity, aggregation, and status-time semantics participate in the
artifact identity for single, derived, cumulative, and multi-metric frames.
After an upgrade, re-running observe therefore bypasses legacy cached frames
that do not carry the current semantic gate.
compare → DeltaFrame
Section titled “compare → DeltaFrame”Quantify change between two observe results (current minus baseline). The frames
must share semantic kind and persisted comparable value semantics; their catalog
and runtime identities may differ.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
current | MetricFrame | Yes | — | Current-period frame. |
baseline | MetricFrame | Yes | — | Baseline-period frame. |
alignment | AlignmentPolicy | No | window_bucket | How buckets/segments are paired. |
baseline = session.observe( revenue, time_scope={"start": "2025-10-01", "end": "2026-01-01"}, grain="month", dimensions=[region],)delta = session.compare(current, baseline)compare pairs buckets with window_bucket by default. Pass alignment= to
override — mv.dow_aligned(), mv.holiday_aligned(), or
mv.holiday_and_dow_aligned(); the calendar-backed kinds also take
calendar=mv.CalendarRef(...).
The delta carries additivity, aggregation, and status-time semantics only when both source frames have the same three values and additivity is known. Missing or mismatched source semantics remain unknown, causing attribution to fail closed instead of inheriting one side.
Funnel comparison and loss-rate attribution
Section titled “Funnel comparison and loss-rate attribution”Two compatible EventFrame[funnel] artifacts use the same compare entry
point, but accept no alignment= argument. Marivo aligns exact persisted
PatternStep identity plus the complete axis tuple; one-sided tuples zero-fill
additive counts while absent-side rates remain null.
funnel_delta = session.compare(current_funnel, baseline_funnel)payment_loss = mv.funnel_loss_rate(step=payment_step)drivers = session.attribute( funnel_delta, axes=[acquisition_channel], target=payment_loss,)The attribution input must be an ungrouped funnel delta. It re-aggregates the
persisted journey assignments over governed cohort-entry Dimensions and never
rematches Events. Each driver emits additive loss and denominator_mix
components whose sum reconciles to the target loss-rate delta within 1e-9.
Use mode="joint" or mode="hierarchy" for multiple axes. Hierarchy pool
shares normalize within each displayed level; artifact metadata retains the
deepest joint-partition pools used for reconciliation. Contributions describe
arithmetic decomposition of an observed change, not causality.
attribute → AttributionFrame
Section titled “attribute → AttributionFrame”Attribute a delta’s movement across explicit axes. This is the default public entry point for “why did it change?” analysis. Component-aware ratio and weighted-mean deltas use mix attribution. Plain non-linear sampled folds such as quantile, min, max, first, or last cannot be summed by axis unless they are part of a persisted component-aware derived metric delta.
The DeltaFrame persists the semantic gate used by attribution; Marivo does
not look up a possibly changed catalog at attribution time. The original delta
is checked before Marivo replays observations to materialize a requested axis.
DeltaFrame.show() states whether attribution is supported, conditional, or
blocked, and DeltaFrame.contract() exposes the same boundary as a typed precondition:
unknown and ordinary non-additive deltas fail, semi-additive deltas require axes
that exclude the status time dimension, and persisted ratio/weighted-mean
component paths remain available.
| Persisted metric semantics | Axis attribution |
|---|---|
additive | Supported by sum and hierarchy attribution. |
semi_additive | Supported on non-time axes; rejected on its status_time_dimension. |
Component-aware ratio / weighted_mean | Supported by ratio/weighted mix attribution. |
Tier-1 mean over a measure | Automatically lowered to sum(measure) / count_non_null(measure) and supported by weighted mix attribution. |
Other non_additive metrics | Rejected, including opaque/tier-2 means, median, percentile, min, max, count-distinct, tier-2 non-additive metrics, and non-additive linear compositions. |
| Missing additivity metadata | Rejected. Re-run observe and compare before retrying. |
Mean lowering uses non-null measure count, not entity row count. For rejected metrics, model explicit ratio/weighted-mean components or attribute additive numerator and denominator metrics separately.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
frame | DeltaFrame | Yes | — | The delta to attribute. |
axes | list[dimension] | Yes | — | Segment or time axes to attribute over. |
mode | "joint" | "hierarchy" | Multi-axis only | — | joint returns one row per full axis combination; hierarchy returns ordered prefix rows. |
attribution = session.attribute(delta, axes=[region, platform], mode="joint")attribution.show()For joint, contribution rows are additive and sum to the overall change. For
hierarchy, parent rows repeat their descendants’ total; sum only the deepest
level when reconciling the change. Component-aware ratio and weighted-mean
attribution preserves value_effect, mix_effect, and residual in both modes.
Multi-axis calls have no default mode; omit mode for a single axis, where a
supplied value has no effect. Single-axis output keeps the resolved dimension
column name, such as cluster, and can be joined directly to the source
DeltaFrame on that column. The generic level, axis, driver, and path
columns appear only in multi-axis hierarchy output. Additive single-axis
attribution reports attribution_shape="sum".
The preserved dimension name must not collide with attribution result, value,
or panel bucket columns. A collision fails closed with a
structured semantic-authoring repair instead of producing duplicate or
ambiguous columns. Evidence protocol fields use explicit metadata mapping and
do not reserve user dimension names.
attribution.attribution_mode exposes the row
layout, while attribution.attribution_shape / meta.method expose the
attribution math. Both layouts can therefore report method="weighted_mix".
Use marivo.help("analysis.AttributionMode") for the focused distinction.
Contribution shares name their denominator explicitly:
share_of_total_deltais signed and may exceed 100% when a driver offsets the net movement;share_of_positive_poolis populated only for positive contributions;share_of_negative_poolis populated only for negative contributions and reports a positive within-pool share.
Marivo does not rename either sign pool to “improvement” or “degradation”
because metric desirability is not part of the persisted metric contract. Apply
that interpretation only when the business direction is known. New and churned
segments retain exact one-sided component contributions. AttributionFrame.show()
prints a reconciliation line with the total delta, contribution sum, one-sided
contribution sum, unattributed contribution sum, and residual; attribution fails
closed if a deepest partition does not reconcile within numeric tolerance.
correlate → AssociationResult
Section titled “correlate → AssociationResult”Measure the association between two metrics over aligned buckets.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
a, b | MetricFrame | Yes | — | The two frames to associate. |
measure_a, measure_b | str | No | frame measure | Public value name from each frame’s value_columns. |
alignment | AlignmentPolicy | No | window_bucket | Bucket pairing. |
method | "pearson", "spearman", "kendall" | No | "pearson" | Correlation method. |
lag_range | range or sequence of signed integers | No | lag 0 | Lags to evaluate; k pairs a[t] with b[t+k]. Positive means a leads b; negative means b leads a. |
Non-zero lags require time_series or panel inputs. Panel lags shift within
each dimension series, never across series boundaries, and null pairs are
dropped after shifting so missing buckets do not collapse the time axis. Each
lag requires at least two overlapping, non-constant pairs. The result contains
one row per lag and records the strongest absolute correlation as
meta.best_lag.
Omit measure_a / measure_b to use each frame’s unique metric value, or pass
the exact public names returned by a.value_columns and b.value_columns.
forecast → ForecastFrame
Section titled “forecast → ForecastFrame”Project a time series or panel forward.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
history | MetricFrame (time_series/panel) | Yes | — | Continuous history, no NaNs. |
horizon | int | Yes | — | Buckets to project (≥ 1). |
model | "naive" | "seasonal_naive" | "drift" | No | "seasonal_naive" | Forecast strategy. |
seasonality_period | int | No | by grain | Override the seasonal period (day=7, week=52, month=12, quarter=4). |
interval_level | float | No | 0.95 | Confidence level for the prediction interval. |
measure_column | str | No | frame measure | Public value name from history.value_columns. |
Every panel series must cover the same continuous training buckets. Forecast
raises ForecastInputQualityError when one segment is missing a bucket; it does
not silently fill the bucket with zero or move that segment’s forecast origin.
history = session.observe(revenue, time_scope={"start": "2026-01-01", "end": "2026-04-01"}, grain="day")projection = session.forecast(history, horizon=30, measure_column=history.value_columns[0])assess_quality → QualityReport
Section titled “assess_quality → QualityReport”Run quality checks over an artifact and return a separate QualityReport.
artifact.quality_summary is only a cheap persisted metadata projection; it is
not the same thing as running session.assess_quality(artifact).
Read report.overall_status, report.blocking_issue_count, and
report.warning_count for the authoritative verdict and counts.
report.state remains ArtifactState materialization metadata; it is not a
quality status.
The report’s contract().issues contains typed data-quality failures. Do not
report a window as a complete period fact when its report contains a blocking
time_coverage_incomplete issue.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
frame | MetricFrame | Yes | — | The frame to inspect. |
hypothesis_test → HypothesisTestResult
Section titled “hypothesis_test → HypothesisTestResult”Paired test of whether a metric’s mean changed between two periods.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
a, b | MetricFrame | Yes | — | Current and baseline frames. |
hypothesis | "mean_changed" | No | "mean_changed" | Test type (v1). |
value_a, value_b | str | No | frame measure | Public value name from each frame’s value_columns. |
alignment | AlignmentPolicy | No | window_bucket | Pairing for the test. |
sampling | SamplingPolicy | No | inferred | Pairing/min-sample rules. |
alpha | float | No | 0.05 | Significance level. |
Discovery — session.discover.* → CandidateSet
Section titled “Discovery — session.discover.* → CandidateSet”Discovery operators search an artifact for deterministic candidate rows and
return a CandidateSet. Candidate row order is a deterministic score order, not
a recommendation from Marivo.
| Helper | Source shape | Required | Key options |
|---|---|---|---|
point_anomalies | MetricFrame time_series/panel | — | value, threshold=3.0 |
period_shifts | DeltaFrame time_series/panel | ≥ 7 buckets | value, threshold=2.0 |
driver_axes | DeltaFrame | search_space | value, limit |
interesting_slices | MetricFrame or DeltaFrame | — | search_space, value, threshold=2.0, limit |
interesting_windows | time_series/panel frame | — | value, threshold=2.0 |
cross_sectional_outliers | MetricFrame segmented/panel | — | peer_scope, value, threshold=3.0 |
series = session.observe(revenue, time_scope={"start": "2026-01-01", "end": "2026-04-01"}, grain="day")candidates = session.discover.point_anomalies(series, threshold=2.0)candidates.show()selection = candidates.select(rank=1)print(selection.kind, selection.window, selection.keys)select(rank=1) returns a closed immutable selection variant for the
candidate shape. It does not accept an arbitrary attribute name and creates no
job, artifact, lineage step, or evidence record.
Transform reference — frame.transform.*
Section titled “Transform reference — frame.transform.*”Transforms reshape a frame while preserving its family (MetricFrame →
MetricFrame, DeltaFrame → DeltaFrame). Each transform is a method on the
frame itself — call frame.transform.<op>(...), not a session-level helper.
| Transform | Key parameters | Effect |
|---|---|---|
filter | predicate (callable) | Keep rows where the predicate returns true. |
slice | slice_by (axis → value/list/range) | Keep rows matching exact axis values. |
rollup | drop_axes and/or grain | Drop axes and re-aggregate, or re-bucket the time axis to a coarser grain. Cumulative frames take the last bucket per period (rollup_fold="last"). |
topk | by, limit | Keep the top N rows by a measure. |
bottomk | by, limit | Keep the bottom N rows. |
rank | by, method, rank_column | Add a rank column ordered by a measure. |
normalize | mode, baseline | index / share / pct_change / per_unit / z_score (MetricFrame only). |
window | window | Restrict to a time window. |
For deltas, bottomk(by="delta") returns the largest declines because it selects the most-negative values.
Cumulative frame caveats
Section titled “Cumulative frame caveats”Cumulative MetricFrames store running-total values whose semantics depend on the
accumulation anchor (all_history, grain_to_date, or trailing).
show(),contract(), andtransform.window(...)work normally on cumulative frames.contract()andshow()surface the anchor-specific caveat, so the allowed path is visible at the frame you are reading.correlate,discover,assess_quality, andhypothesis_testare allowed. Trailing frames are independent windowed aggregations (not running totals), so these intents produce meaningful results. The monotonic-trend caveat applies only toall_historyandgrain_to_dateanchors — keep it in mind when interpreting results.compareis anchor-dispatched:all_history: rejected. Observe the base flow metric and compare that — a cumulative delta over a window equals the base total over that window.trailing: allowed when both frames share the same trailing anchor payload (count,unit). The windowed rolling values align ordinally.grain_to_date: allowed for a single-period, boundary-anchored window. The window must start on a reset boundary and span at most one reset period, and both frames must share the reset grain and query grain. The resulting DeltaFrame records the to-date alignment underalignment_dump["to_date"].
- For derived metrics, the same compare paths are allowed only when every outer component
is cumulative and all components share exactly one
trailingorgrain_to_dateanchor.all_history, mixed anchors, cumulative/non-cumulative mixes, malformed metadata, and current/baseline anchor differences fail closed. attribute,decompose, andforecastreject cumulative frames regardless of anchor; this includes a derived cumulative DeltaFrame that compare accepted. Re-observe the base flow metric for those intents.transform.rollup(...)re-aggregates cumulative frames withrollup_fold="last": the time axis is re-bucketed to the targetgrainand each period contributes its last bucket (the period-end value). Frames that are neither re-aggregatable nor carrying arollup_foldare rejected — re-observe at the target grain instead.
cum_frame = session.observe( cumulative_active_users, time_scope={"start": "2026-01-01", "end": "2026-04-01"}, grain="day",)cum_frame.contract() # shows the anchor-specific caveatwindowed = cum_frame.transform.window(window={"start": "2026-02-01", "end": "2026-03-01"})# Roll up a cumulative frame to a coarser grain (period-end semantics):monthly = cum_frame.transform.rollup(grain="month")# For attribute/forecast, observe the base metric instead:base_frame = session.observe(active_users, ...)Terminal custom analysis
Section titled “Terminal custom analysis”First use a closed mv.runtime_metric expression when governed measures or
catalog metrics can express the required analysis-only caliber. When the
calculation still cannot be expressed through session.observe(...), use
md.raw_sql(...) — the sole terminal raw SQL execution path. It enforces
a timeout, bounds returned rows, and runs read-only. The result is a
RawSqlResult that cannot re-enter typed analysis; call
RawSqlResult.to_pandas() for the terminal pandas exit. Its ordered columns,
shape, and row_count describe returned bounded rows only:
row_count == shape[0] == returned_row_count. Read requested_limit and
is_truncated alongside that count. It has no .contract() or typed
affordances.
A semantic gap may also use this path without prior approval. The agent may infer a temporary metric, dimension, relationship, or filter definition, but must put those assumptions in the analysis record and keep raw-SQL-supported claims separate from canonical artifact evidence.
import marivo.datasource as md
result = md.raw_sql( ms.ref.datasource("warehouse"), "SELECT region, SUM(amount) AS revenue FROM orders GROUP BY region", reason="custom revenue breakdown",)result.show()df = result.to_pandas()Use artifact.to_pandas() for terminal local pandas work from a typed
frame. Neither md.raw_sql(...) results nor to_pandas() exports can
re-enter typed analysis. Raw SQL does not repair missing business semantics:
the closeout still requests approval for the smallest semantic change.
Typed regression is not supported by the current Marivo operator surface; a
regression task therefore remains explicit terminal custom analysis and cannot
be promoted back into the typed chain.
Read and recover artifacts
Section titled “Read and recover artifacts”The agent read path is layered to keep scripts cheap across loops:
repr(delta) # cheap single-line hintdelta.show() # bounded current-state readdelta.contract() # mechanical compatibility and typed issuesdelta.to_pandas() # terminal export for custom local analysisWhen an operator emits evidence, artifact.show() includes its bounded typed
digest before the result preview. The same value is available as
artifact.evidence_digest; check artifact.evidence_status for degradation.
Use session.evidence for exact findings and derivation traces.
Use artifact.contract().affordances to inspect mechanical compatibility. Each
affordance carries a capability_id (stable registry id), public_entrypoint,
help_target, role-preserving inputs, preconditions, and
expected_output_family. Every input records its exact parameter name,
accepted artifact families, and whether the current artifact can bind it. The
contract does not rank, recommend, enumerate calls, or decide the next step.
artifact.contract().boundary_ports lists typed terminal-exit ports
(e.g. boundary.to_pandas) with preserves and does_not_preserve guarantees.
Structured errors
Section titled “Structured errors”When an operator fails, it raises a typed AnalysisError subclass. Every error
carries stable typed fields — expected, received, location, and repair —
not a generic details bag. The repair field is a typed AnalysisRepair object
with kind (retry, inspect, user_choice, semantic_authoring,
environment), action, help_target, optional snippet, and optional
candidates drawn from live state. user_choice means several mechanically
legal alternatives remain and business judgment must select one. Follow the
repair guidance; it points to the exact marivo.help("analysis.<target>") target for the failing
capability.
Event continuation errors use the same contract. Matching-policy and step
choices are user_choice; stale artifacts and unknown coverage are inspect;
unsafe relationship paths or temporal authoring gaps are
semantic_authoring.
Terminal boundary exits
Section titled “Terminal boundary exits”Marivo maintains a typed analysis chain. The terminal exits are
frame.to_pandas() and md.raw_sql(...). frame.to_pandas() returns a
defensive copy for custom local analysis; md.raw_sql(...) returns a
RawSqlResult with RawSqlResult.to_pandas() as its own terminal pandas
exit. Neither preserves lineage or evidence continuity. Terminal results
cannot re-enter typed analysis. If business semantics are missing, terminal
raw SQL may provide only a provisional result; retain the gap for closeout
approval before semantic authoring.
Semantic authoring
Section titled “Semantic authoring”Typed analysis accepts exact current catalog entries or stable semantic refs at
the qualifying runtime boundaries, normalizes both forms immediately to refs,
and never guesses business meaning.
If a required metric, dimension, or time dimension is missing or disputed, the
affected typed branch stops. The agent may infer temporary semantics only in a
terminal md.raw_sql(...) branch and must disclose the datasource, purpose,
assumptions, bounded result, and loss of canonical identity, lineage, and
evidence continuity. It proposes the smallest durable semantic change at
closeout and follows marivo-semantic only after explicit approval. The semantic
layer owns the business-object contract; the analysis layer owns typed
computation over those objects.
When a later script needs previous work, recover persisted facts instead of rerunning upstream queries:
summaries = session.frame_summaries(limit=20)jobs = session.recent_jobs(limit=5)previous = session.get_frame(ref)
# Only when recovery, a repeated task, or a recurring failure makes history relevant:history = mv.session.recent(limit=10)prior = mv.session.inspect(history.items[0].name) if history.items else Noneframe_summaries() returns a FrameSummaryPage. Continue with the opaque
next_cursor when has_more is true. Pages use ordinary keyset semantics, not
snapshot isolation.
Use marivo.help("analysis.recovery") to list these real Session members. recovery and
artifacts are help topics, not session.recovery / session.artifacts
namespaces.
Evidence and audit
Section titled “Evidence and audit”Every committed operator records deterministic typed evidence into the session. Marivo does not create cross-artifact judgments or choose the next operation.
session.evidence.digests(...)— bounded newest-first digest pages.session.evidence.findings(...)— bounded newest-first finding pages.session.evidence.digest(ref),.finding(id),.trace(id)— exact reads and finding derivation audit.
See Evidence for the full model.
End-to-end
Section titled “End-to-end”import marivo.analysis as mv
session = mv.session.get_or_create(name="revenue-check", question="Why did Q4 drop?")catalog = session.catalogrevenue = catalog.metrics.get("sales.revenue")region = catalog.dimensions.get("sales.orders.region")
current = session.observe( revenue, time_scope={"start": "2026-10-01", "end": "2027-01-01"}, grain="month", dimensions=[region],)baseline = session.observe( revenue, time_scope={"start": "2025-10-01", "end": "2026-01-01"}, grain="month", dimensions=[region],)delta = session.compare(current, baseline)attribution = session.attribute(delta, axes=[region])attribution.show()From the delta you can branch: session.discover.period_shifts(delta) to find
when it moved, or session.forecast(current, horizon=3) to project it forward.
Frame types
Section titled “Frame types”| Frame | Produced by |
|---|---|
MetricFrame | observe |
DeltaFrame | compare |
AttributionFrame | attribute |
AssociationResult | correlate |
ForecastFrame | forecast |
QualityReport | assess_quality |
HypothesisTestResult | hypothesis_test |
CandidateSet | discover.* |