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.
Most analysis starts with session.observe(...) and continues through metric
comparison, attribution, discovery, or quality assessment. Event journey and
replay-based Lifecycle analysis are specialized paths covered later on this page.
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 — usually
observe,compare,attribute, ordiscover.<objective>, with specialized namespace operators such asevents.matchused when needed. Its 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(), recent(), inspect(name), and delete(name).
Reusing a session name with a different non-empty question emits
SessionQuestionMismatchWarning and keeps the original question — pass a fresh
name to start a new analysis, or omit question to resume the existing session.
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.
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 | mv.time_scope(start="2026-10-01", end="2027-01-01") | Half-open time range — start inclusive, end exclusive. |
grain | mv.grain("day") or ms.calendar_grain(...) | Unified time bucket value. 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 | TimeScope | No | None | One scope returned by mv.time_scope(...) or an exact catalog period lookup. |
grain | Grain | No | None | One value returned by mv.grain(...) or ms.calendar_grain(...). |
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=mv.time_scope(start="2026-10-01", end="2027-01-01"), grain=mv.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=mv.time_scope(start="2026-10-01", end="2027-01-01"), grain=mv.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=mv.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=mv.time_scope(start="2025-10-01", end="2026-01-01"), grain=mv.grain("month"), dimensions=[region],)delta = session.compare(current, baseline)compare pairs buckets with window_bucket by default. Pass alignment= to
choose one closed helper: mv.window_bucket(), mv.day_of_week(),
mv.period_progress(), mv.period_correspondence(), or
mv.occurrence_progress(), or mv.working_day_progress(schedule=...). The latter five
require exact certified authority carried by the source frames; the period
helpers use a period authority, occurrence_progress uses temporal-set
occurrence scopes, and working_day_progress uses one exact work schedule.
There is no analysis-local holiday-calendar parameter.
mv.occurrence_progress(anchor="start" | "end") is reserved for day-grain
time-series or panel frames selected by exact TemporalSet occurrence scopes.
It pairs zero-based effective local-day ordinals (forward from the start or
backward from the exclusive end); it does not infer recurrence or label matches.
The built-in day grain still uses each session’s report timezone; it must match
the occurrence boundary timezone for occurrence_progress admission.
mv.working_day_progress(schedule=...) is also restricted to day-grain
time-series or panel frames. It excludes certified non-working dates, pairs the
remaining zero-based working-day ordinals, and records exclusions and unmatched
ordinals in alignment evidence.
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.
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
segmented non-linear point estimates still cannot be summed. Graph-owned
count_distinct, median, and percentile(q) roots instead use dedicated
distribution-aware methods when their persisted attribution basis is admitted.
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 or blocked, and
DeltaFrame.contract().attribute_admission is the sole typed source for that
same boundary:
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. |
Graph-owned count_distinct | Distinct membership allocation for reproducible scalar keys; raw distinct keys remain in the datasource. |
Graph-owned median / percentile(q) | Exact DuckDB value-frequency or mergeable Trino qdigest replacement attribution. ClickHouse reservoir sampling remains blocked. |
Other non_additive metrics | Rejected, including opaque/tier-2 means, min, max, unsupported quantile sources, 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 a rejected
metric, follow DeltaFrame.contract().attribute_admission: re-observe legacy
artifacts or author the aggregate-specific component/distribution evidence named
by its repair.
| 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" | "multiresolution" | Multi-axis only | — | Additive/component methods allow joint or hierarchy; non-additive methods allow joint or independent multiresolution. Read the delta admission for the legal pair. |
attribution = session.attribute(delta, axes=[region, platform], mode="joint")attribution.show()For the canonical single-axis case, omit mode:
attribution = session.attribute(delta, axes=[region])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 namespaced attribution_level,
attribution_axis, attribution_driver, and attribution_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.
Count-distinct membership is deduplicated and allocated in the datasource as
1 / membership_degree; raw keys do not enter frames, jobs, lineage, evidence,
telemetry, logs, or errors. Quantiles replay an independent unsegmented endpoint
instead of summing segmented point estimates. DuckDB uses exact value-frequency
evidence with exact Shapley through 8 partitions and 128 deterministic
permutations through 64; Trino merges qdigest sketches server-side. Empty
intermediate coalitions, more than 64 partitions, more than 250,000 frequency
rows, or failure to reproduce the independent endpoint block the call and do
not persist an attribution artifact.
mode="multiresolution" recomputes every ordered axis prefix as an independent
game. Complete rows are not additive across resolutions. Select one immutable,
query-free view by exact semantic-ref prefix before consuming it:
regional = attribution.at_resolution(axes=[region])regional.to_pandas() # sum once within each comparison bucketContribution 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. A history observed with a certified
semantic grain keeps its exact SemanticPeriodBindingV1: forecast periods are
complete, consecutive ordinal steps, and future keys/bounds come from the same
snapshot rather than a fixed-frequency approximation.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
history | MetricFrame (time_series/panel) | Yes | — | Continuous history, no NaNs. |
horizon | int | Yes | — | Periods to project (≥ 1); for semantic grains, the count is in certified period ordinals. |
model | "naive" | "seasonal_naive" | "drift" | No | "seasonal_naive" | Forecast strategy. |
seasonality_period | int | No | by grain | Seasonal ordinal distance. Built-in defaults are day=7, week=52, month=12, quarter=4; semantic grains require an explicit value for seasonal_naive. |
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 periods. For semantic
grains, an incomplete/mismatched period, unavailable exact snapshot, unsupported
model, or future coverage gap raises ForecastShapeUnsupportedError before an
artifact is written. Forecast does not silently fill a period, infer seasonality,
or manufacture a boundary from average history duration. Built-in panel gaps
continue to raise ForecastInputQualityError.
history = session.observe( revenue, time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"), grain=mv.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.
For a cumulative comparable-period DeltaFrame, the report reads the typed
pairing evidence and surfaces matched-null, unpaired, and fallback counts.
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 | supported analysis artifact | Yes | — | The frame to inspect, including metric DeltaFrames. |
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. Scored objectives use deterministic score order;
ontology hypotheses are unscored and use deterministic semantic identity order.
Neither order is 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 |
semantic_hypotheses | arity-one catalog MetricFrame or same-Metric DeltaFrame | ready optional ontology | limit=50 (1..200) |
series = session.observe( revenue, time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"), grain=mv.grain("day"),)candidates = session.discover.point_anomalies(series, threshold=2.0)candidates.show()item_id = str(candidates.to_pandas().iloc[0]["item_id"])selection = candidates.select(item_id=item_id)print(selection.kind, selection.window, selection.keys)select(item_id=...) returns a closed immutable selection variant for the
candidate shape. Numeric rank is not accepted. Selection creates no job,
artifact, lineage step, or evidence record.
When a source artifact’s contract() advertises the optional ontology
continuation, use it as an unscored hypothesis path:
hypotheses = session.discover.semantic_hypotheses(series, limit=50)hypotheses.show() # edge meaning, guardrails, exclusions, and copyable item_idcandidate = hypotheses.select(item_id="candidate_<full-sha256>")driver = session.observe(candidate, analysis_purpose="test a reviewed hypothesis")The selected OntologyMetricCandidate inherits the source observation scope;
scope overrides are rejected. Ontology edges and candidates do not seed
findings or prove causality. Statistical evidence begins only after the explicit
observation and a suitable downstream operator.
Other scored discovery selections are not accepted by session.observe. They
identify a row, period, axis, slice, window, or peer already computed in the
source artifact rather than a new Metric that still needs materialization.
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: allowed when both frames share the valid anchor and have paired business coordinates with exactevaluation_endcutoffs. The result is current minus baseline observed level, not asserted interval flow; source revision is unverified.trailing: allowed when both frames have the same canonical fixed-duration span; equivalent units such as7 dayand1 weekcompare successfully.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.- Both comparable-period anchors support ordinal window alignment plus DOW, holiday,
and holiday-then-DOW position alignment. For
grain_to_date, the calendar policy’speriodmust equal the reset grain. Workday-position alignment is not a public comparison mode in this version.
- For derived metrics, the same compare paths are allowed only when every outer component
is cumulative and all components share exactly one anchor, including
all_history. Mixed anchors, cumulative/non-cumulative mixes, malformed metadata, and current/baseline anchor differences fail closed. All-history component sidecars use the parent’s exact paired business-coordinate set. attributeaccepts only a current cumulative DeltaFrame produced bycompare. Business dimensions replay the same cumulative observations and explain endpoint level change; requesting exactly the cumulativeoveraxis uses an additive base-flow bridge for directsum/countstructures. Homogeneous ratio and weighted cumulative deltas support business-axis component mix, but their time bridge is blocked. Mixed time/business axes and cumulativecount_distinctfail closed; inspectdelta.contract().cumulative_attributionfor the typed route status.decomposeandforecaststill reject cumulative frames. Useattributeon the current cumulative delta when one of its typed routes is supported, or observe the base flow metric for a different analysis.transform.rollup(...)re-aggregates cumulative frames withrollup_fold="last": the time axis is re-bucketed to the targetgrainand each period contributes its complete last row, includingevaluation_end. Frames that are neither re-aggregatable nor carrying arollup_foldare rejected — re-observe at the target grain instead.
Every direct or structurally complete derived cumulative row carries the reserved,
UTC-serialized evaluation_end coordinate. For all-history deltas, inspect the exact
current_evaluation_end and baseline_evaluation_end columns. One-sided coordinates are
dropped and counted in delta.meta.alignment["cumulative_pairs"]; a matched null remains
paired and produces a null delta.
Trailing and grain-to-date deltas expose delta.meta.cumulative_alignment, which retains
both authored anchors, the canonical anchor used for identity, and exact matched-null,
current-only, baseline-only, and fallback counts. Only matched coordinates enter the final
delta, including for panel frames.
cum_frame = session.observe( cumulative_active_users, time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"), grain=mv.grain("day"),)cum_frame.contract().show() # 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=mv.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().show() # 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=mv.time_scope(start="2026-10-01", end="2027-01-01"), grain=mv.grain("month"), dimensions=[region],)baseline = session.observe( revenue, time_scope=mv.time_scope(start="2025-10-01", end="2026-01-01"), grain=mv.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.
Advanced Event and Lifecycle analysis
Section titled “Advanced Event and Lifecycle analysis”Most questions are answered with metrics and the core operators above. Use the Event and Lifecycle paths when the analysis depends on ordered occurrences, attempt matching, funnel progression, elapsed time, or replayed state.
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.time_scope(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.time_scope(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.time_scope(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.
Inspect the exact Event range before choosing a replay or matching window:
lifecycle = session.catalog.state_models.get("commerce.order_lifecycle")bounds = session.events.occurrence_bounds(lifecycle)print(bounds.earliest_occurrence_at, bounds.latest_occurrence_at)For a StateModel, Marivo infers its distinct inception and transition Events
and aggregates only occurrences matching those Event predicates. The
EventOccurrenceBounds result is not a Datasource-wide maximum and does not
prove that the source is complete through its latest occurrence. Use it to
choose a candidate window, then resolve completeness separately. A StateModel
with no Event triggers returns event_refs=() and None for both bounds.
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. A caller reads that
authoritative watermark with session.events.watermark(event, through=...),
which returns the provider’s EventWatermarkReceipt — or None when no
provider exists or the Event has no authoritative watermark. 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.time_scope(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.time_scope(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.
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.
DeltaFrame[funnel] is a closed artifact family: it exposes only its own
funnel fields and never projects Metric Delta facets. Metric-only
continuations (components(), transform.*) fail closed with a structured
error instead of pretending a nonexistent component graph or metric contract.
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.time_scope(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.
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.* |