Skip to content

Analysis Workflow

Every Marivo analysis starts from a metric 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.

The pieces:

  • 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, 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.analysis as mv
import marivo.semantic as ms
session = mv.session.get_or_create(name="revenue-investigation", question="Why did Q4 drop?")
catalog = session.catalog
revenue = catalog.get("sales.revenue") # a metric object
region = catalog.get("sales.orders.region") # a dimension object

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(), and delete(name).

Operators take catalog objects and a few shared value objects. You will reuse these across almost every intent:

ValueShapeMeaning
metric inputcatalog.get("sales.revenue")A catalog metric object or its SemanticRef subclass (e.g. MetricRef). Authoring refs from ms.aggregate(...) also work. Bare strings are rejected.
dimension inputcatalog.get("sales.orders.region")A catalog dimension object or its DimensionRef / TimeDimensionRef (see Semantic refs). Used for dimensions, where keys, and axes.
timescope{"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.
where{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.

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
metricmetric object / refYesThe metric to materialize.
timescopedictNoNoneHalf-open {"start", "end"} window.
graingrainNoNoneTime bucket; present ⇒ time series or panel.
dimensionslist[ref]NoNoneSegment axes.
wheredictNoNonePre-aggregation row filter.
time_dimensionrefNoentity defaultPick the time axis when the entity declares several.
expect_shapeshapeNoNoneGuard; raises before backend work if the predicted shape differs.
current = session.observe(
revenue,
timescope={"start": "2026-10-01", "end": "2027-01-01"},
grain="month",
dimensions=[region],
)

Quantify change between two observe results (current minus baseline). The frames must share metric and semantic kind.

ParameterTypeRequiredDefaultMeaning
currentMetricFrameYesCurrent-period frame.
baselineMetricFrameYesBaseline-period frame.
alignmentAlignmentPolicyNowindow_bucketHow buckets/segments are paired.
baseline = session.observe(
revenue,
timescope={"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(...).

Attribute a delta’s movement across explicit axes. This is the default public entry point for “why did it change?” analysis. If a requested axis is missing from the delta and the source observe/compare lineage is replayable, Marivo materializes the expanded delta before decomposing it.

ParameterTypeRequiredDefaultMeaning
frameDeltaFrameYesThe delta to attribute.
axeslist[dimension]YesSegment or time axes to attribute over.
attribution = session.attribute(delta, axes=[region])
attribution.show()

Measure the association between two metrics over aligned buckets.

ParameterTypeRequiredDefaultMeaning
a, bMetricFrameYesThe two frames to associate.
measure_a, measure_bstrNoframe measureNumeric column on each frame.
alignmentAlignmentPolicyNowindow_bucketBucket pairing.
method"pearson"No"pearson"Correlation method (v1: Pearson, zero lag).

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 measureColumn to forecast.
history = session.observe(revenue, timescope={"start": "2026-01-01", "end": "2026-04-01"}, grain="day")
projection = session.forecast(history, horizon=30)

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

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 measureNumeric column on each frame.
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≥ 4 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, timescope={"start": "2026-01-01", "end": "2026-04-01"}, grain="day")
candidates = session.discover.point_anomalies(series, threshold=2.0)
candidates.show()

Advanced reference — session.transform.*

Section titled “Advanced reference — session.transform.*”

Transforms reshape a frame while preserving its family (MetricFrameMetricFrame, DeltaFrameDeltaFrame).

TransformKey parametersEffect
filterpredicate (callable)Keep rows where the predicate returns true.
slicewhere (axis → value/list/range)Keep rows matching exact axis values.
rollupdrop_axesDrop axes and re-aggregate measures.
topkby, limit, orderKeep the top N rows by a measure (order="decrease" default).
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.

Governed derive — session.derive_metric_frame(...)

Section titled “Governed derive — session.derive_metric_frame(...)”

Use derive_metric_frame when a custom Ibis calculation must re-enter the typed metric flow. It always returns a MetricFrame. Semantic refs identify metric, time, and dimension bindings; query output columns are plain strings.

import marivo.datasource as md
warehouse = md.ref("datasource.warehouse")
custom = session.derive_metric_frame(
metric=session.catalog.get("sales.revenue"),
query=mv.ibis_query(
datasource=warehouse,
build=lambda db, ctx: db.table("orders"),
),
columns=mv.metric_columns(
value="value",
time=mv.time_column(
column="order_date",
ref=session.catalog.get("sales.orders.order_date"),
),
dimensions=[
mv.dimension_column(
column="region",
ref=session.catalog.get("sales.orders.region"),
),
],
),
timescope={"start": "2026-06-18", "end": "2026-06-25"},
grain="day",
label="custom_revenue_by_region",
)
custom.show()

Use artifact.to_pandas() for terminal local pandas work that does not need to feed typed Marivo operators.

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

repr(delta)
delta.summary()
delta.schema()
delta.contract()
delta.preview(limit=10)

Use artifact.contract().affordances to inspect mechanical compatibility. It only describes which public operators can mechanically consume the artifact and which inputs are missing; it does not rank, recommend, or decide the next step.

When a later script needs previous work, recover persisted facts instead of rerunning upstream queries:

summaries = session.frame_summaries()
jobs = session.recent_jobs(limit=5)
previous = session.get_frame(ref)

Every operator records evidence into the session, so conclusions stay auditable.

  • session.knowledge() — established facts, driver facts, open anomalies, and suggested follow-ups for the whole session.
  • session.evidence.findings(...), .propositions(...), .assessments(...), .proposition(id), .latest_assessment(id), .trace(id) — look up the evidence objects, and trace a proposition back to the findings that support it.

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.get("sales.revenue")
region = catalog.get("sales.orders.region")
current = session.observe(
revenue,
timescope={"start": "2026-10-01", "end": "2027-01-01"},
grain="month",
dimensions=[region],
)
baseline = session.observe(
revenue,
timescope={"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, derive_metric_frame
DeltaFramecompare
AttributionFrameattribute
AssociationResultcorrelate
ForecastFrameforecast
QualityReportassess_quality
HypothesisTestResulthypothesis_test
CandidateSetdiscover.*