Skip to content

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.

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.

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 marivo
import marivo.analysis as mv
import marivo.semantic as ms

When 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.

session = mv.session.get_or_create(name="revenue-investigation", question="Why did Q4 drop?")
catalog = session.catalog
revenue = catalog.metrics.get("sales.revenue")
region = catalog.dimensions.get("sales.orders.region")
revenue.details().show() # definition, candidate axes, and measure lineage
region.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.

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.

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.

Operators take exact current entries/refs, closed expressions, and a few shared value objects. You will reuse these across almost every intent:

ValueShapeMeaning
metric inputcatalog.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 inputcatalog.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).
AlignmentPolicymv.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).

Keys are catalog dimensions; values are a scalar (==), a list (in), or a structured {"op": ..., "value": ...} form:

FormMeaning
"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)

The starting point for any analysis: materialize a metric over a time range and/or segments.

ParameterTypeRequiredDefaultMeaning
metrics`MetricEntryRef[metric]RuntimeMetricExpr`, or non-empty sequenceYes
time_scopedictNoNoneHalf-open {"start", "end"} window.
graingrainNoNoneTime bucket; present ⇒ time series or panel.
dimensionsentry/ref sequenceNoNoneExact current dimension/time-dimension entries or refs.
slice_byentry/ref-keyed mappingNoNoneGlobal pre-aggregation row filter.
time_dimensiontime-dimension entry/refNoentity defaultPick the time axis when the entity declares several.
expect_shapeshapeNoNoneGuard; 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.

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.

ParameterTypeRequiredDefaultMeaning
currentMetricFrameYesCurrent-period frame.
baselineMetricFrameYesBaseline-period frame.
alignmentAlignmentPolicyNowindow_bucketHow 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 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 semanticsAxis attribution
additiveSupported by sum and hierarchy attribution.
semi_additiveSupported on non-time axes; rejected on its status_time_dimension.
Component-aware ratio / weighted_meanSupported by ratio/weighted mix attribution.
Tier-1 mean over a measureAutomatically lowered to sum(measure) / count_non_null(measure) and supported by weighted mix attribution.
Other non_additive metricsRejected, including opaque/tier-2 means, median, percentile, min, max, count-distinct, tier-2 non-additive metrics, and non-additive linear compositions.
Missing additivity metadataRejected. 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.

ParameterTypeRequiredDefaultMeaning
frameDeltaFrameYesThe delta to attribute.
axeslist[dimension]YesSegment or time axes to attribute over.
mode"joint" | "hierarchy"Multi-axis onlyjoint 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_delta is signed and may exceed 100% when a driver offsets the net movement;
  • share_of_positive_pool is populated only for positive contributions;
  • share_of_negative_pool is 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.

Measure the association between two metrics over aligned buckets.

ParameterTypeRequiredDefaultMeaning
a, bMetricFrameYesThe two frames to associate.
measure_a, measure_bstrNoframe measurePublic value name from each frame’s value_columns.
alignmentAlignmentPolicyNowindow_bucketBucket pairing.
method"pearson", "spearman", "kendall"No"pearson"Correlation method.
lag_rangerange or sequence of signed integersNolag 0Lags 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.

Project a time series or panel forward.

ParameterTypeRequiredDefaultMeaning
historyMetricFrame (time_series/panel)YesContinuous history, no NaNs.
horizonintYesBuckets to project (≥ 1).
model"naive" | "seasonal_naive" | "drift"No"seasonal_naive"Forecast strategy.
seasonality_periodintNoby grainOverride the seasonal period (day=7, week=52, month=12, quarter=4).
interval_levelfloatNo0.95Confidence level for the prediction interval.
measure_columnstrNoframe measurePublic 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])

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.

ParameterTypeRequiredDefaultMeaning
frameMetricFrameYesThe frame to inspect.

Paired test of whether a metric’s mean changed between two periods.

ParameterTypeRequiredDefaultMeaning
a, bMetricFrameYesCurrent and baseline frames.
hypothesis"mean_changed"No"mean_changed"Test type (v1).
value_a, value_bstrNoframe measurePublic value name from each frame’s value_columns.
alignmentAlignmentPolicyNowindow_bucketPairing for the test.
samplingSamplingPolicyNoinferredPairing/min-sample rules.
alphafloatNo0.05Significance 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.

HelperSource shapeRequiredKey options
point_anomaliesMetricFrame time_series/panelvalue, threshold=3.0
period_shiftsDeltaFrame time_series/panel≥ 7 bucketsvalue, threshold=2.0
driver_axesDeltaFramesearch_spacevalue, limit
interesting_slicesMetricFrame or DeltaFramesearch_space, value, threshold=2.0, limit
interesting_windowstime_series/panel framevalue, threshold=2.0
cross_sectional_outliersMetricFrame segmented/panelpeer_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.

Transforms reshape a frame while preserving its family (MetricFrameMetricFrame, DeltaFrameDeltaFrame). Each transform is a method on the frame itself — call frame.transform.<op>(...), not a session-level helper.

TransformKey parametersEffect
filterpredicate (callable)Keep rows where the predicate returns true.
sliceslice_by (axis → value/list/range)Keep rows matching exact axis values.
rollupdrop_axes and/or grainDrop 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").
topkby, limitKeep the top N rows by a measure.
bottomkby, limitKeep the bottom N rows.
rankby, method, rank_columnAdd a rank column ordered by a measure.
normalizemode, baselineindex / share / pct_change / per_unit / z_score (MetricFrame only).
windowwindowRestrict to a time window.

For deltas, bottomk(by="delta") returns the largest declines because it selects the most-negative values.

Cumulative MetricFrames store running-total values whose semantics depend on the accumulation anchor (all_history, grain_to_date, or trailing).

  • show(), contract(), and transform.window(...) work normally on cumulative frames. contract() and show() surface the anchor-specific caveat, so the allowed path is visible at the frame you are reading.
  • correlate, discover, assess_quality, and hypothesis_test are allowed. Trailing frames are independent windowed aggregations (not running totals), so these intents produce meaningful results. The monotonic-trend caveat applies only to all_history and grain_to_date anchors — keep it in mind when interpreting results.
  • compare is 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 under alignment_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 trailing or grain_to_date anchor. all_history, mixed anchors, cumulative/non-cumulative mixes, malformed metadata, and current/baseline anchor differences fail closed.
  • attribute, decompose, and forecast reject 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 with rollup_fold="last": the time axis is re-bucketed to the target grain and each period contributes its last bucket (the period-end value). Frames that are neither re-aggregatable nor carrying a rollup_fold are 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 caveat
windowed = 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, ...)

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.

The agent read path is layered to keep scripts cheap across loops:

repr(delta) # cheap single-line hint
delta.show() # bounded current-state read
delta.contract() # mechanical compatibility and typed issues
delta.to_pandas() # terminal export for custom local analysis

When 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.

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.

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.

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 None

frame_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.

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.

import marivo.analysis as mv
session = mv.session.get_or_create(name="revenue-check", question="Why did Q4 drop?")
catalog = session.catalog
revenue = 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.

FrameProduced by
MetricFrameobserve
DeltaFramecompare
AttributionFrameattribute
AssociationResultcorrelate
ForecastFrameforecast
QualityReportassess_quality
HypothesisTestResulthypothesis_test
CandidateSetdiscover.*