API Reference

This section contains the complete API reference for all modules in the rowvoi package.

Core Types

Core types and data structures for rowvoi.

This module defines the fundamental building blocks used throughout the rowvoi package: - CandidateState: Represents current uncertainty over which row is “the one” - FeatureSuggestion: A recommendation for which column to query next

class rowvoi.core.CandidateState(candidate_rows, posterior, observed_cols, observed_values)[source]

Bases: object

Represents the current uncertainty over which row is “the one”.

Parameters:
candidate_rows

List of row indices under consideration

Type:

collections.abc.Sequence[collections.abc.Hashable]

posterior

Probabilities over candidate_rows, shape (n_candidates,) Use uniform if deterministic / no model

Type:

numpy.ndarray

observed_cols

Set of columns that have been queried

Type:

set[collections.abc.Hashable]

observed_values

Mapping col -> observed value (may be empty in planning mode)

Type:

collections.abc.Mapping[collections.abc.Hashable, Any]

property entropy: float

Shannon entropy H(posterior) in bits.

property max_posterior: float

max_r p(r | E).

property residual_uncertainty: float

1 - max_posterior.

property is_unique: bool

True if there is a single candidate with posterior ~1.

property unique_row: Hashable | None

Return the most probable row if unique, else None.

classmethod uniform(candidate_rows, observed_cols=None, observed_values=None)[source]

Create a state with uniform posterior over candidates.

Parameters:
Returns:

State with uniform posterior distribution

Return type:

CandidateState

filter_candidates(df, col, value)[source]

Filter candidates to those matching the observed value.

Parameters:
  • df (DataFrame) – The data frame containing candidate rows

  • col (Hashable) – The column that was observed

  • value (Any) – The observed value

Returns:

New state with filtered candidates and renormalized posterior

Return type:

CandidateState

reweight(likelihoods, *, observed_col=None, observed_value=None)[source]

Apply soft evidence: multiply the posterior by a likelihood vector.

The soft counterpart to filter_candidates(). Where that method drops every candidate whose value differs, this one only reweights, so a candidate is never eliminated on evidence that is merely improbable. Use it when evidence is graded rather than exact – retrieval scores, model-predicted answers, noisy observations.

Candidates are all retained (including zero-likelihood ones, whose posterior becomes 0), so positional alignment with likelihoods holds across successive updates.

Parameters:
  • likelihoods (Sequence[float] | ndarray) – P(evidence | candidate) for each candidate, in candidate_rows order. Need not be normalized, but must be non-negative Need not be normalized, but must be non-negative

  • observed_col (Hashable | None) – Column or question this evidence came from, recorded in observed_cols

  • observed_value (Any) – The observed value, recorded in observed_values

Returns:

New state with the renormalized posterior

Raises:

ValueError – If likelihoods has the wrong length, contains a negative value, or drives the total posterior mass to zero (evidence impossible under every candidate – the candidate set is wrong, not merely narrowed)

Return type:

CandidateState

class rowvoi.core.FeatureSuggestion(col, score, expected_voi=None, marginal_cost=None, debug=None)[source]

Bases: object

A recommendation of which column to query next.

Parameters:
col

The column name suggested to query next

Type:

collections.abc.Hashable | None

score

Raw score used to rank columns (e.g., MI, coverage gain)

Type:

float

expected_voi

Expected value of information in bits

Type:

float | None

marginal_cost

Cost of querying this column

Type:

float | None

debug

Additional debugging information

Type:

dict[str, Any] | None

property cost_adjusted_score: float

Score divided by cost (if cost is available).

Set Cover Engine

Weighted set cover: the solver engine shared by keys and RAG context selection.

Nothing here knows what an element means. KeyProblem builds a universe of row pairs on top of it; rowvoi.rag.context builds a universe of claims. The seven strategies and the greedy path planner are common to both.

exception rowvoi.setcover.SolverUnavailableError[source]

Bases: RuntimeError

No linear programming solver is usable.

Raised rather than quietly substituting an approximate strategy: greedy is an ln(m) approximation and ILP is exact, so returning one where the caller asked for the other is a silent correctness change.

class rowvoi.setcover.CoverStep(name, newly_covered, cumulative_covered, total_elements, marginal_cost, cumulative_cost, newly_covered_weight=None, cumulative_covered_weight=None)[source]

Bases: object

A single step in a cover path showing incremental progress.

Parameters:
  • name (Hashable)

  • newly_covered (int)

  • cumulative_covered (int)

  • total_elements (int)

  • marginal_cost (float)

  • cumulative_cost (float)

  • newly_covered_weight (float | None)

  • cumulative_covered_weight (float | None)

name

The set added in this step

Type:

collections.abc.Hashable

newly_covered

Number of elements newly covered by this set

Type:

int

cumulative_covered

Total elements covered up to and including this step

Type:

int

total_elements

Size of the universe

Type:

int

marginal_cost

Cost of adding this specific set

Type:

float

cumulative_cost

Total cost up to and including this step

Type:

float

newly_covered_weight

Weighted coverage gain (for weighted objectives)

Type:

float | None

cumulative_covered_weight

Total weighted coverage so far

Type:

float | None

property coverage: float

Fraction of the universe covered so far.

class rowvoi.setcover.CoverPath(steps)[source]

Bases: object

Ordered sequence of sets and their contribution to coverage/cost.

Parameters:

steps (list[CoverStep])

steps

Ordered list of steps showing incremental progress

Type:

list[rowvoi.setcover.CoverStep]

names()[source]

Return the ordered list of set names in the path.

Return type:

list[Hashable]

prefix_for_budget(budget)[source]

Return the longest prefix of sets whose cumulative_cost <= budget.

Parameters:

budget (float) – Maximum allowed cumulative cost

Returns:

Sets that fit within the budget

Return type:

list[Hashable]

prefix_for_epsilon(epsilon)[source]

Return the shortest prefix that leaves <= epsilon fraction uncovered.

Parameters:

epsilon (float) – Maximum allowed fraction of uncovered elements

Returns:

Minimum sets needed to achieve (1-epsilon) coverage

Return type:

list[Hashable]

coverage_curve()[source]

Return the coverage curve as (cumulative_cost, coverage_fraction) points.

Return type:

list[tuple[float, float]]

class rowvoi.setcover.SetCoverProblem(sets, *, universe=None, costs=None)[source]

Bases: object

Weighted set cover over an arbitrary universe.

Parameters:
  • sets (Mapping[Any, Iterable[Any]]) – Maps each selectable set to the elements it covers

  • universe (Iterable[Any] | None) – Elements that must be covered. If None, the union of all sets is used. Pass it explicitly when some elements are covered by no set (they then count against coverage rather than being silently ignored).

  • costs (Mapping[Any, float] | None) – Cost of selecting each set. Missing entries default to 1.0

cost(name)[source]

Cost of selecting one set (1.0 when unspecified).

Parameters:

name (Hashable)

Return type:

float

total_cost(names)[source]

Total cost of a selection.

Parameters:

names (Iterable[Hashable])

Return type:

float

covered(names)[source]

Elements of the universe covered by a selection.

Parameters:

names (Iterable[Hashable])

Return type:

set[Hashable]

coverage(names)[source]

Fraction of the universe covered by a selection.

Parameters:

names (Iterable[Hashable])

Return type:

float

is_cover(names, *, epsilon=0.0)[source]

Whether a selection covers all but at most epsilon of the universe.

Parameters:
Return type:

bool

solve(strategy='greedy', *, epsilon=0.0, time_limit=None)[source]

Find a minimum-cost selection covering (1-epsilon) of the universe.

Parameters:
  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Algorithm to use: - “greedy”: greedy cost/gain ratio (ln m approximation) - “exact”: brute force enumeration (only for small problems) - “ilp”: Integer Linear Programming (requires pulp) - “sa”: Simulated Annealing metaheuristic - “ga”: Genetic Algorithm metaheuristic - “lp”: Linear Programming relaxation with rounding - “hybrid”: Combined SA+GA approach

  • epsilon (float) – Allow this fraction of the universe to remain uncovered

  • time_limit (float | None) – Maximum time in seconds

Returns:

Minimal (or near-minimal) selection

Raises:

ValueError – If strategy is not one of the supported names.

Return type:

list[Hashable]

element_weights(weighting='uniform')[source]

Per-element weights used by the path planner.

“idf” gives elements covered by fewer sets a higher weight, so scarce coverage is prioritized. Elements covered by no set get weight 1.0.

Parameters:

weighting (Literal['uniform', 'idf'])

Return type:

dict[Hashable, float]

plan_path(*, objective='coverage', weighting='uniform')[source]

Build a greedy ordering of sets, tracking coverage and cost per step.

Unlike solve(), which returns an unordered selection, this returns the order in which to acquire sets — so callers can cut the sequence at a budget or a coverage target.

Parameters:
  • objective (Literal['coverage', 'entropy']) –

    • “coverage”: gain = weighted newly covered elements

    • ”entropy”: gain = reduction in log cluster size

  • weighting (Literal['uniform', 'idf']) –

    • “uniform”: all elements weighted equally

    • ”idf”: elements covered by fewer sets weighted higher

Returns:

Ordered sequence with coverage information

Return type:

CoverPath

rowvoi.setcover.solve_set_cover(sets, *, universe=None, costs=None, strategy='greedy', epsilon=0.0, time_limit=None)[source]

Solve a weighted set cover instance.

Convenience wrapper around SetCoverProblem.solve().

Parameters:
Return type:

list[Hashable]

rowvoi.setcover.coverage_of(sets, selection, *, universe=None)[source]

Fraction of the universe covered by selection.

Parameters:
Return type:

float

Deterministic Keys

Deterministic key and path algorithms for row disambiguation.

This module handles the deterministic case where all column values are known. It solves the minimal set cover problem: find the smallest set of columns that distinguishes all pairs of rows in a candidate set.

The universe of row pairs is built here; the solvers themselves live in rowvoi.setcover, which knows nothing about rows.

class rowvoi.keys.KeyPathStep(col, newly_covered_pairs, cumulative_covered_pairs, total_pairs, marginal_cost, cumulative_cost, newly_covered_weight=None, cumulative_covered_weight=None)[source]

Bases: object

A single step in a key path showing incremental progress.

Parameters:
  • col (Hashable)

  • newly_covered_pairs (int)

  • cumulative_covered_pairs (int)

  • total_pairs (int)

  • marginal_cost (float)

  • cumulative_cost (float)

  • newly_covered_weight (float | None)

  • cumulative_covered_weight (float | None)

col

The column added in this step

Type:

collections.abc.Hashable

newly_covered_pairs

Number of pairs newly covered by this column

Type:

int

cumulative_covered_pairs

Total pairs covered up to and including this step

Type:

int

total_pairs

Total number of pairs that need to be covered

Type:

int

marginal_cost

Cost of adding this specific column

Type:

float

cumulative_cost

Total cost up to and including this step

Type:

float

newly_covered_weight

Weighted coverage gain (for weighted objectives)

Type:

float | None

cumulative_covered_weight

Total weighted coverage so far

Type:

float | None

property coverage: float

Fraction of pairs covered so far.

class rowvoi.keys.KeyPath(steps)[source]

Bases: object

Ordered sequence of columns and their contribution to coverage/cost.

Parameters:

steps (list[KeyPathStep])

steps

Ordered list of steps showing incremental progress

Type:

list[rowvoi.keys.KeyPathStep]

columns()[source]

Return the ordered list of columns in the path.

Return type:

list[Hashable]

prefix_for_budget(budget)[source]

Return the longest prefix of columns whose cumulative_cost <= budget.

Parameters:

budget (float) – Maximum allowed cumulative cost

Returns:

Columns that fit within the budget

Return type:

list[Hashable]

prefix_for_epsilon_pairs(epsilon)[source]

Return the shortest prefix that leaves <= epsilon fraction unresolved.

Parameters:

epsilon (float) – Maximum allowed fraction of unresolved pairs

Returns:

Minimum columns needed to achieve (1-epsilon) coverage

Return type:

list[Hashable]

coverage_curve()[source]

Return the coverage curve as (cumulative_cost, coverage_fraction) points.

Return type:

list[tuple[float, float]]

rowvoi.keys.pairwise_coverage(df, rows, cols)[source]

Fraction of unordered row pairs in rows that are distinguished by cols.

Parameters:
Returns:

Fraction of pairs that differ on at least one column in cols

Return type:

float

class rowvoi.keys.KeyProblem(df, rows, *, columns=None, costs=None)[source]

Bases: object

Deterministic key-finding problem for a fixed subset of rows.

Under the hood: universe = row pairs; columns cover pairs they separate. Solving is delegated to SetCoverProblem.

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • columns (Sequence[Hashable] | None) – Columns to consider. If None, use all columns

  • costs (Mapping[Hashable, float] | None) – Cost of each column. If None, unit cost

is_key(cols, *, epsilon_pairs=0.0)[source]

Check if cols distinguish all but at most epsilon_pairs fraction.

Parameters:
  • cols (Sequence[Hashable]) – Columns to check

  • epsilon_pairs (float) – Maximum allowed fraction of unresolved pairs

Returns:

True if cols form an epsilon-key

Return type:

bool

pairwise_coverage(cols)[source]

Compute pairwise coverage for this problem.

Parameters:

cols (Sequence[Hashable])

Return type:

float

minimal_key(strategy='greedy', *, epsilon_pairs=0.0, time_limit=None)[source]

Solve deterministic min-key / set-cover for this row set.

Parameters:
  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Algorithm to use: - “greedy”: greedy set cover on row pairs - “exact”: brute force enumeration (only for small problems) - “ilp”: Integer Linear Programming (requires pulp) - “sa”: Simulated Annealing metaheuristic - “ga”: Genetic Algorithm metaheuristic - “lp”: Linear Programming relaxation with rounding - “hybrid”: Combined SA+GA approach

  • epsilon_pairs (float) – Allow some unresolved pairs to remain

  • time_limit (float | None) – Maximum time in seconds

Returns:

Minimal (or near-minimal) set of columns

Return type:

list[Hashable]

plan_path(*, objective='pair_coverage', weighting='uniform')[source]

Build a greedy ordering of columns for this row set.

Parameters:
  • objective (Literal['pair_coverage', 'entropy']) –

    • “pair_coverage”: gain = newly covered pairs

    • ”entropy”: gain = reduction in log cluster size

  • weighting (Literal['uniform', 'pair_idf']) –

    • “uniform”: all pairs weighted equally

    • ”pair_idf”: weight hard-to-separate pairs more

Returns:

Ordered sequence with coverage information

Return type:

KeyPath

rowvoi.keys.find_key(df, rows, *, columns=None, costs=None, strategy='greedy', epsilon_pairs=0.0, time_limit=None)[source]

Find a minimal key for distinguishing a set of rows.

Convenience wrapper around KeyProblem.minimal_key().

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • columns (Sequence[Hashable] | None) – Columns to consider

  • costs (Mapping[Hashable, float] | None) – Cost of each column

  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Algorithm to use

  • epsilon_pairs (float) – Allow some unresolved pairs

  • time_limit (float | None) – Maximum time in seconds

Returns:

Minimal set of columns

Return type:

list[Hashable]

rowvoi.keys.plan_key_path(df, rows, *, columns=None, costs=None, objective='pair_coverage', weighting='uniform')[source]

Plan an ordered path of columns for disambiguation.

Convenience wrapper around KeyProblem.plan_path().

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • columns (Sequence[Hashable] | None) – Columns to consider

  • costs (Mapping[Hashable, float] | None) – Cost of each column

  • objective (Literal['pair_coverage', 'entropy']) – Objective function for ordering

  • weighting (Literal['uniform', 'pair_idf']) – Weighting scheme for pairs

Returns:

Ordered sequence with coverage information

Return type:

KeyPath

Probabilistic Keys

Probabilistic key and path algorithms.

This module handles the probabilistic case where column values are unknown and we work with a model to predict expected information gain and coverage.

rowvoi.prob_keys.find_key_probabilistic(df, rows, model, *, epsilon_posterior=0.05, columns=None, costs=None, objective='mi_over_cost', max_steps=None)[source]

Find a probabilistic min-key under a model.

Runs a greedy MI/VOI policy (non-adaptively) until max_r p(r | E) >= 1 - epsilon_posterior, then returns the set of columns used.

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • model (RowVoiModel) – Trained model for computing expected information

  • epsilon_posterior (float) – Target residual uncertainty

  • columns (Sequence[Hashable] | None) – Columns to consider

  • costs (Mapping[Hashable, float] | None) – Cost of each column

  • objective (Literal['mi', 'mi_over_cost']) – Objective for column selection

  • max_steps (int | None) – Maximum number of columns to select

Returns:

Columns selected by the greedy policy

Return type:

list[Hashable]

rowvoi.prob_keys.plan_key_path_probabilistic(df, rows, model, *, objective='mi_over_cost', columns=None, costs=None)[source]

Build an expected greedy path under the model.

For each step, compute MI or expected entropy reduction for each column given the current posterior (without actually observing values), pick the best, and iterate. Returns a KeyPath with expected coverage/cost.

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • model (RowVoiModel) – Trained model for computing expected information

  • objective (Literal['mi', 'mi_over_cost', 'expected_entropy_reduction']) – Objective for ordering columns

  • columns (Sequence[Hashable] | None) – Columns to consider

  • costs (Mapping[Hashable, float] | None) – Cost of each column

Returns:

Expected path with coverage information

Return type:

KeyPath

rowvoi.prob_keys.estimate_coverage_probability(df, rows, cols, model=None)[source]

Estimate the probability that cols will distinguish rows.

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to consider

  • cols (Sequence[Hashable]) – Columns to evaluate

  • model (RowVoiModel | None) – Model for probabilistic estimation. If None, uses deterministic coverage.

Returns:

Estimated probability of full disambiguation

Return type:

float

Policies

Policy abstractions for selecting the next best column.

This module defines the Policy protocol and various concrete implementations for deciding which column to query next during disambiguation.

class rowvoi.policies.Policy(*args, **kwargs)[source]

Bases: Protocol

A strategy for picking the next column given the current state.

suggest(df, state, candidate_cols=None)[source]

Suggest the next best column to query.

Parameters:
  • df (DataFrame) – The data table

  • state (CandidateState) – Current disambiguation state

  • candidate_cols (Sequence[Hashable] | None) – Columns to consider. If None, consider all columns

Returns:

Recommendation for next column to query

Return type:

FeatureSuggestion

class rowvoi.policies.GreedyCoveragePolicy(costs=None, objective='pairs', weighting='uniform')[source]

Bases: object

Stateless policy: choose column that maximizes pairwise separation.

Can be used for “next best column” in deterministic mode.

Parameters:
costs

Cost of querying each column

Type:

collections.abc.Mapping[collections.abc.Hashable, float] | None

objective
  • “pairs”: maximize newly covered pairs

  • “entropy”: maximize entropy reduction

Type:

Literal[‘pairs’, ‘entropy’]

weighting
  • “uniform”: all pairs weighted equally

  • “pair_idf”: weight hard-to-separate pairs more

Type:

Literal[‘uniform’, ‘pair_idf’]

suggest(df, state, candidate_cols=None)[source]

Suggest column with best coverage gain.

Parameters:
Return type:

FeatureSuggestion

class rowvoi.policies.MIPolicy(model, objective='mi_over_cost', feature_costs=None)[source]

Bases: object

Policy that uses mutual information from a RowVoiModel.

Parameters:
model

The trained model for computing MI

Type:

RowVoiModel

objective
  • “mi”: raw mutual information

  • “mi_over_cost”: MI divided by feature cost

Type:

Literal[‘mi’, ‘mi_over_cost’]

feature_costs

Cost of querying each feature

Type:

collections.abc.Mapping[collections.abc.Hashable, float] | None

suggest(df, state, candidate_cols=None)[source]

Use the model to suggest the next feature.

The objective and costs are forwarded to the model, so cost affects which column is chosen. Rescaling the winner’s score afterwards would leave the choice itself made on raw mutual information.

Parameters:
  • df (DataFrame) – The data table.

  • state (CandidateState) – Current disambiguation state.

  • candidate_cols (Sequence[Hashable] | None) – Columns to consider. If None, consider all columns.

Returns:

Recommendation for the next column. col is None when the model has nothing left to suggest.

Return type:

FeatureSuggestion

class rowvoi.policies.CandidateMIPolicy(normalize=False, costs=None)[source]

Bases: object

Policy using local mutual information on candidate set only.

This policy doesn’t require a trained model - it computes MI directly from the candidate rows.

Parameters:
normalize

Whether to normalize MI by maximum entropy

Type:

bool

costs

Cost of querying each column

Type:

collections.abc.Mapping[collections.abc.Hashable, float] | None

suggest(df, state, candidate_cols=None)[source]

Compute MI for each column and suggest the best.

Parameters:
Return type:

FeatureSuggestion

compute_mi(df, state, col)[source]

Compute conditional mutual information I(R; X_col | E) in bits.

Model-free: groups candidates by their value in col and measures how much the posterior’s entropy drops in expectation. Public because callers often want the full ranking, not just the argmax.

Parameters:
Return type:

float

class rowvoi.policies.RandomPolicy(seed=None)[source]

Bases: object

Random policy for baseline comparisons.

Parameters:

seed (int | None)

seed

Random seed for reproducibility

Type:

int | None

suggest(df, state, candidate_cols=None)[source]

Select a random column.

Parameters:
Return type:

FeatureSuggestion

Sessions

Interactive disambiguation sessions.

This module provides tools for running interactive disambiguation sessions where columns are queried sequentially based on a policy until stopping criteria are met.

class rowvoi.session.StopRules(max_steps=None, cost_budget=None, epsilon_posterior=None, epsilon_pairs=None, target_unique=True)[source]

Bases: object

Conditions for stopping a disambiguation session.

Parameters:
  • max_steps (int | None)

  • cost_budget (float | None)

  • epsilon_posterior (float | None)

  • epsilon_pairs (float | None)

  • target_unique (bool)

max_steps

Maximum number of columns to query

Type:

int | None

cost_budget

Maximum total cost to spend

Type:

float | None

epsilon_posterior

Stop when residual_uncertainty <= epsilon

Type:

float | None

epsilon_pairs

Stop when unresolved pair fraction <= epsilon

Type:

float | None

target_unique

Stop when state.is_unique is True

Type:

bool

should_stop(state, steps, total_cost, df=None)[source]

Check if any stopping condition is met.

Parameters:
  • state (CandidateState) – Current state

  • steps (int) – Number of steps taken so far

  • total_cost (float) – Total cost incurred so far

  • df (DataFrame | None) – Data frame (needed for epsilon_pairs check)

Returns:

(should_stop, reason) where reason explains why stopping

Return type:

tuple[bool, str]

class rowvoi.session.SessionStep(col, observed_value, suggestion, cost, cumulative_cost, entropy_before, entropy_after, pair_coverage_after=None)[source]

Bases: object

Record of a single step in a disambiguation session.

Parameters:
col

Column that was queried

Type:

collections.abc.Hashable

observed_value

Value observed for the true row

Type:

Any

suggestion

The suggestion that led to this query

Type:

rowvoi.core.FeatureSuggestion

cost

Cost of this query

Type:

float

cumulative_cost

Total cost up to this point

Type:

float

entropy_before

Entropy before observing this column

Type:

float

entropy_after

Entropy after observing this column

Type:

float

pair_coverage_after

Pairwise coverage after this step

Type:

float | None

class rowvoi.session.DisambiguationSession(df, candidate_rows, *, prior=None, policy, feature_costs=None)[source]

Bases: object

Interactive disambiguation session manager.

Maintains CandidateState, queries a Policy for the next column, and updates with observations.

Parameters:
  • df (DataFrame) – The data table

  • candidate_rows (Sequence[Hashable]) – Initial candidate row indices

  • prior (Mapping[Hashable, float] | None) – Prior probabilities over candidates

  • policy (Policy) – Policy for selecting next column

  • feature_costs (Mapping[Hashable, float] | None) – Cost of querying each column

property state: CandidateState

Current disambiguation state.

property history: list[SessionStep]

History of all steps taken.

property cumulative_cost: float

Total cost incurred so far.

property steps_taken: int

Number of steps taken so far.

next_question(candidate_cols=None)[source]

Ask the policy for the next best column.

Does NOT update state yet - just returns the suggestion.

Parameters:

candidate_cols (Sequence[Hashable] | None) – Columns to consider

Returns:

Recommendation for next column

Return type:

FeatureSuggestion

observe(col, value)[source]

Incorporate an observation into the state.

Parameters:
  • col (Hashable) – Column that was queried

  • value (Any) – Observed value

Returns:

Record of this step

Return type:

SessionStep

run(stop, *, candidate_cols=None, true_row=None)[source]

Run an entire session until a stop rule triggers.

Parameters:
  • stop (StopRules) – Stopping criteria

  • candidate_cols (Sequence[Hashable] | None) – Columns to consider

  • true_row (Hashable | None) – The true row index (for simulation). If None, picks the highest posterior candidate.

Returns:

Full sequence of steps taken

Return type:

list[SessionStep]

reset(candidate_rows=None)[source]

Reset the session to initial state.

Parameters:

candidate_rows (Sequence[Hashable] | None) – New candidate rows. If None, reset to original candidates.

Return type:

None

Machine Learning

Model-based value-of-information routines for rowvoi.

Sequential conditional selection: which column to acquire next, given what has already been observed in the current case plus patterns learned from historical data. This is prediction conditioned on evidence, not guessing.

RowVoiModel learns per-column value frequencies and entropies from a DataFrame, then uses a CandidateState – which columns have been observed and with what values – to rank the remaining columns by expected information gain about the true row. Unlike the model-free policies, it carries a noise model, so an observation that disagrees with a candidate reduces that candidate’s probability rather than eliminating it.

Typical uses: interactive interviews (given the answers so far, which question tells you most?), sequential experiments, adaptive diagnosis.

The design follows the active feature acquisition literature but is kept deliberately small.

See also

rowvoi.setcover and rowvoi.find_key() when every value is already known and the task is choosing a minimal set rather than a sequence. rowvoi.CandidateMIPolicy for the model-free equivalent of the ranking done here.

class rowvoi.ml.RowVoiModel(smoothing=1e-06, noise=0.0, normalize_cols=True)[source]

Bases: object

Model for computing expected value of information across features.

A RowVoiModel encapsulates global information about the distribution of values for each feature in a dataset, together with optional discretization rules and a simple noise model. It provides methods to fit to a DataFrame, rank features by expected information gain, and simulate sequential acquisition procedures to disambiguate an unknown row among a candidate set.

Parameters:
  • smoothing (float) – A pseudo-count added to each category when computing frequencies. This mitigates zero-probability issues when some candidate values are rare. Default is 1e-6.

  • noise (float) – Probability that the observed feature value does not equal the candidate row’s true value. When greater than zero, noise spreads probability mass over other candidate values according to the global frequency distribution. Default is 0.0 (no noise).

  • normalize_cols (bool) – Whether to compute and use normalized mutual information values (i.e. divide by the feature entropy) when ranking features. Default is True.

Raises:

ValueError – If noise is outside [0, 1).

fit(df, discrete_cols=None, bins=3)[source]

Fit the model to a DataFrame by computing column frequencies and entropies.

This method prepares the model to evaluate expected information gain by storing global frequencies and, if necessary, discretizing numeric columns. Only columns in discrete_cols will be treated as discrete; if None, all columns are treated as discrete. The DataFrame is not modified in place; a copy with discretized values is stored.

Parameters:
  • df (DataFrame) – The dataset from which to learn frequencies. Should contain no missing values; callers should handle missing values externally (e.g. by imputation or by treating NaN as a category).

  • discrete_cols (Sequence[Hashable] | None) – Columns to treat as discrete. If None, all columns are considered discrete. Numeric columns not in this list are discretized into quantile bins of size bins.

  • bins (int) – Number of quantile bins for discretization of numeric columns not specified in discrete_cols. Default is 3.

Returns:

Returns self for chaining.

Return type:

Self

suggest_next_feature(df, state, candidate_cols=None, objective='mi', feature_costs=None)[source]

Rank candidate columns by expected value of information.

Given a DataFrame df, a current candidate state, and an optional set of candidate columns, this method evaluates each column for its expected mutual information I(R; X_col | E) under the model’s smoothing and noise assumptions. It then returns the column with the best score. Two objectives are supported:

  • 'mi' - select the column with the highest expected MI.

  • 'mi_over_cost' - divide expected MI by a user-supplied cost for that feature. This allows penalizing expensive features.

If normalize_cols was set when constructing the model, the normalized mutual information (MI divided by the feature entropy) is returned in the FeatureSuggestion for diagnostic purposes but is not used to rank features unless objective is set accordingly.

Parameters:
  • df (DataFrame) – The data table. If different from the DataFrame passed to fit(), it will be discretized using the same rules.

  • state (CandidateState) – The current candidate state.

  • candidate_cols (Sequence[Hashable] | None) – A list of columns to consider. If None, all columns not yet observed in state.observed_cols are considered.

  • objective (str) – Objective used for ranking. One of 'mi' or 'mi_over_cost'. Default is 'mi'.

  • feature_costs (dict[Hashable, float] | None) – Mapping of feature costs. Required if objective is 'mi_over_cost'. Costs must be positive.

Returns:

A FeatureSuggestion containing the best column and associated information gain estimates, or None if no eligible columns remain.

Raises:
  • RuntimeError – If fit() has not been called.

  • ValueError – If objective is 'mi_over_cost' and a candidate column has no cost, or a supplied cost is not positive.

Return type:

FeatureSuggestion | None

run_acquisition(df, true_row, initial_state, candidate_cols=None, stop_when_unique=True, max_steps=None, objective='mi', feature_costs=None)[source]

Simulate a sequential feature acquisition session.

Given a true row index and an initial candidate state, this method repeatedly calls suggest_next_feature() to select the next column to query. It then simulates acquiring the feature value from the true row, updates the posterior and candidate list accordingly, and continues until either only one candidate row remains or a maximum number of steps has been reached. The history of suggestions (with associated VOI metrics) is returned.

Parameters:
  • df (DataFrame) – The data table (same columns as used for fitting). If different from self._df, it will be discretized consistently.

  • true_row (Hashable) – The index of the actual row to identify. Must be contained in initial_state.candidate_rows.

  • initial_state (CandidateState) – The starting candidate state. This object is not modified; a new state is created for the simulation.

  • candidate_cols (Sequence[Hashable] | None) – Optional subset of columns to consider when selecting features. If None, all columns not yet observed are considered at each step.

  • stop_when_unique (bool) – If True (default), stop the acquisition as soon as the posterior concentrates all mass on a single row. If False, continue until max_steps is reached.

  • max_steps (int | None) – Maximum number of features to query. If None, no explicit limit is imposed.

  • objective (str) – Objective passed to suggest_next_feature() (either 'mi' or 'mi_over_cost'). Default is 'mi'.

  • feature_costs (dict[Hashable, float] | None) – Feature cost mapping used if objective='mi_over_cost'.

Returns:

A list of suggestions (one per query) containing the column chosen at each step and the associated VOI metrics. The length of the list equals the number of queries made.

Return type:

list[FeatureSuggestion]

Evaluation

Simulation and evaluation tools for rowvoi.

This module provides comprehensive evaluation tools for comparing different key-finding algorithms and policies, including gold standard computation and systematic benchmarking.

rowvoi.eval.sample_candidate_sets(df, *, subset_size, n_samples, random_state=None)[source]

Randomly sample subsets of rows from a DataFrame.

Parameters:
  • df (DataFrame) – The data frame to sample from

  • subset_size (int) – Number of rows in each subset

  • n_samples (int) – Number of subsets to generate

  • random_state (int | None) – Random seed for reproducibility

Returns:

List of row index lists

Raises:

ValueError – If subset_size exceeds the number of rows in the frame.

Return type:

list[list[Hashable]]

rowvoi.eval.compute_gold_key(df, rows, *, columns=None, costs=None, epsilon_pairs=0.0, time_limit=10.0, allow_approximate=False)[source]

Compute the provably optimal deterministic key.

Tries ILP, then exhaustive search. Both are exact, so whichever answers first is the optimum.

This deliberately does not fall back to greedy. Callers use the result as the baseline for optimality_gap, and a greedy baseline makes that number wrong – it can even go negative, showing a method beating the “optimum”. Failing is the honest outcome; evaluate_keys already treats a failed gold solve as “no baseline” and reports no gap.

Parameters:
  • df (DataFrame) – The data table

  • rows (Sequence[Hashable]) – Row indices to distinguish

  • columns (Sequence[Hashable] | None) – Columns to consider

  • costs (Mapping[Hashable, float] | None) – Cost of each column

  • epsilon_pairs (float) – Tolerance for unresolved pairs

  • time_limit (float) – Maximum time for each exact strategy

  • allow_approximate (bool) – Permit a greedy result when no exact one is obtainable. The return value is then not necessarily optimal, so do not use it as an optimality baseline.

Returns:

Optimal key columns, or an approximate key when allow_approximate is set and no exact strategy succeeded.

Raises:

SolverUnavailableError – If no exact strategy could produce a key and allow_approximate is False.

Return type:

list[Hashable]

rowvoi.eval.compute_gold_next_column_probabilistic(df, state, model, *, candidate_cols=None, objective='mi')[source]

Compute the ‘gold standard’ next column under a model.

Parameters:
  • df (DataFrame) – The data table

  • state (CandidateState) – Current state

  • model (RowVoiModel) – Trained model

  • candidate_cols (Sequence[Hashable] | None) – Columns to consider

  • objective (Literal['mi', 'expected_entropy_reduction']) – Objective function

Returns:

Optimal next column

Return type:

Hashable

class rowvoi.eval.KeyEvalResult(method, rows, key, key_cost, pair_coverage, runtime_sec, gold_key=None, gold_cost=None, optimality_gap=None)[source]

Bases: object

Result of evaluating a key-finding method.

Parameters:
method

Name of the method

Type:

str

rows

The candidate row set

Type:

tuple[collections.abc.Hashable, …]

key

Columns selected by the method

Type:

list[collections.abc.Hashable]

key_cost

Total cost of the key

Type:

float

pair_coverage

Fraction of pairs distinguished

Type:

float

runtime_sec

Time taken to compute the key

Type:

float

gold_key

Optimal key if computed

Type:

list[collections.abc.Hashable] | None

gold_cost

Cost of optimal key

Type:

float | None

optimality_gap

key_cost - gold_cost

Type:

float | None

rowvoi.eval.evaluate_keys(df, candidate_sets, methods, *, costs=None, epsilon_pairs=0.0, gold_solver=None)[source]

Evaluate multiple key-finding methods on candidate sets.

Parameters:
Returns:

Evaluation results for each method and candidate set

Return type:

list[KeyEvalResult]

class rowvoi.eval.PolicyEvalStats(name, mean_steps, mean_cost, mean_final_entropy, mean_final_pair_coverage, std_steps=0.0, std_cost=0.0, success_rate=0.0)[source]

Bases: object

Statistics for a policy’s performance.

Parameters:
name

Policy name

Type:

str

mean_steps

Average number of steps to termination

Type:

float

mean_cost

Average total cost

Type:

float

mean_final_entropy

Average final entropy

Type:

float

mean_final_pair_coverage

Average final pairwise coverage

Type:

float

std_steps

Standard deviation of steps

Type:

float

std_cost

Standard deviation of cost

Type:

float

success_rate

Fraction of runs that achieved uniqueness

Type:

float

rowvoi.eval.evaluate_policies(df, candidate_sets, policies, *, feature_costs=None, stop=None, n_repeats=1)[source]

Evaluate disambiguation policies on candidate sets.

Parameters:
  • df (DataFrame) – The data table

  • candidate_sets (Sequence[Sequence[Hashable]]) – Test cases (row subsets)

  • policies (Mapping[str, Policy]) – Policies to evaluate (name -> policy)

  • feature_costs (Mapping[Hashable, float] | None) – Cost of each feature

  • stop (StopRules | None) – Stopping criteria (default: target_unique=True)

  • n_repeats (int) – Number of times to run each policy per candidate set

Returns:

Performance statistics for each policy

Return type:

list[PolicyEvalStats]

class rowvoi.eval.AcquisitionResult(subset_size, steps_used, unique_identified, optimal_steps=None, cols_used=None)[source]

Bases: object

Result of a single feature acquisition simulation.

Parameters:
subset_size

Size of the candidate set

Type:

int

steps_used

Number of queries made

Type:

int

unique_identified

Whether unique row was found

Type:

bool

optimal_steps

Size of minimal key if computed

Type:

int | None

cols_used

Sequence of columns queried

Type:

list[collections.abc.Hashable] | None

rowvoi.eval.benchmark_policy(df, policy, subset_sizes, n_samples, *, compute_optimal=True, max_cols_for_exact=10, feature_costs=None, random_state=None)[source]

Benchmark a policy across different subset sizes.

Parameters:
  • df (DataFrame) – The data table

  • policy (Policy) – Policy to benchmark

  • subset_sizes (Sequence[int]) – Different candidate set sizes to test

  • n_samples (int) – Number of samples per size

  • compute_optimal (bool) – Whether to compute optimal key size

  • max_cols_for_exact (int) – Max columns for exact solution

  • feature_costs (Mapping[Hashable, float] | None) – Column costs

  • random_state (int | None) – Random seed

Returns:

Results keyed by subset size

Return type:

dict[int, list[AcquisitionResult]]

RAG Adapters

Retrieval-augmented generation adapters over rowvoi’s core engines.

rowvoi answers two questions about tabular data: which minimal set of columns distinguishes a set of candidate rows, and which column to observe next. Both have direct analogues in RAG, and this package is the adapter layer – the algorithms themselves are unchanged.

Tabular

RAG

Set cover over row pairs

context – cover claims with chunks

Next column by MI

questions – next clarifying question

Sequential acquisition

retrieval – next retrieval probe

Everything here is deterministic and depends only on pandas and numpy: the functions take matrices (which chunks support which claims, what answer each question gets per candidate) and return selections. Producing those matrices is what needs a language model, and that boundary is rowvoi.rag.protocols. For an Anthropic-backed implementation, install the claude extra and import rowvoi.rag.claude explicitly – it is deliberately not imported here, so rowvoi.rag never pulls in an LLM SDK.

class rowvoi.rag.Chunk(id, text='', tokens=None)[source]

Bases: object

A retrieved chunk.

Parameters:
id

Stable identifier, used everywhere else in this module

Type:

collections.abc.Hashable

text

The chunk body

Type:

str

tokens

Token count, used as the set-cover cost. When omitted, cost falls back to an explicit costs mapping and then to 1.0 (chunk-count minimization)

Type:

int | None

class rowvoi.rag.ContextSelection(chunks=<factory>, covered_claims=<factory>, missing_claims=<factory>, coverage=1.0, total_cost=0.0)[source]

Bases: object

The chunks chosen to support a set of claims.

Parameters:
chunks

Selected chunk ids

Type:

list[collections.abc.Hashable]

covered_claims

Claims supported by the selection

Type:

set[collections.abc.Hashable]

missing_claims

Claims left unsupported – either traded away via epsilon_claims, or unsupported by any retrieved chunk, which is a retrieval failure rather than a selection one

Type:

set[collections.abc.Hashable]

coverage

Fraction of claims covered

Type:

float

total_cost

Summed cost (tokens, when supplied) of the selection

Type:

float

property unsupportable_claims: set[Hashable]

Alias for missing_claims, read as “no chunk could have helped”.

rowvoi.rag.select_context(chunks, claims, support, *, costs=None, epsilon_claims=0.0, strategy='greedy', time_limit=None)[source]

Select the cheapest set of chunks that supports the required claims.

Parameters:
  • chunks (Sequence[Chunk] | Sequence[Any]) – Retrieved chunks. Passing Chunk objects lets tokens act as the cost; passing bare ids minimizes chunk count unless costs is given

  • claims (Sequence[Hashable]) – Claims the answer must support. This is the universe – a claim absent from support counts as uncovered rather than being ignored

  • support (Mapping[Any, Iterable[Any]] | DataFrame) – Which chunks support which claims. As a DataFrame: claims on the index, chunk ids as columns, truthy cells meaning support

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens. Defaults to 1.0

  • epsilon_claims (float) – Permit this fraction of claims to go unsupported. Trading 5% of claims often halves the context

  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Any strategy accepted by rowvoi.setcover.SetCoverProblem.solve()

  • time_limit (float | None) – Maximum seconds for the solvers that respect one

Returns:

Selected chunks plus coverage accounting

Return type:

ContextSelection

Examples

>>> from rowvoi.rag import Chunk, select_context
>>> chunks = [Chunk("a", tokens=100), Chunk("b", tokens=100),
...           Chunk("c", tokens=400)]
>>> claims = ["price", "release_date"]
>>> support = {"price": {"a", "c"}, "release_date": {"b", "c"}}
>>> selection = select_context(chunks, claims, support)
>>> sorted(selection.chunks)
['a', 'b']
>>> selection.total_cost
200.0
rowvoi.rag.plan_context_path(chunks, claims, support, *, costs=None, weighting='uniform')[source]

Order chunks by marginal claim coverage per token.

Use this instead of select_context() when the budget is the binding constraint rather than the coverage target: the returned path exposes prefix_for_budget() and coverage_curve(), so you can fill a context window and see exactly what the last token bought.

Parameters:
  • chunks (Sequence[Chunk] | Sequence[Any]) – Retrieved chunks

  • claims (Sequence[Hashable]) – Claims the answer must support

  • support (Mapping[Any, Iterable[Any]] | DataFrame) – Which chunks support which claims

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens

  • weighting (str) – “uniform” weights all claims equally; “idf” upweights claims that few chunks support, so scarce evidence is acquired earlier

Returns:

Ordered acquisition path with per-step coverage and cost

Return type:

CoverPath

Examples

>>> from rowvoi.rag import Chunk, plan_context_path
>>> chunks = [Chunk("a", tokens=100), Chunk("b", tokens=100),
...           Chunk("c", tokens=400)]
>>> claims = ["price", "release_date"]
>>> support = {"price": {"a", "c"}, "release_date": {"b", "c"}}
>>> path = plan_context_path(chunks, claims, support)
>>> path.prefix_for_budget(150)
['a']
rowvoi.rag.extract_and_select(query, chunks, *, extractor, judge, costs=None, epsilon_claims=0.0, strategy='greedy')[source]

End-to-end selection: extract claims, judge support, then cover.

Convenience wrapper for when you have a ClaimExtractor and SupportJudge (see rowvoi.rag.claude). The two LLM calls happen here; the selection itself is deterministic.

Parameters:
  • query (str) – The user’s question

  • chunks (Sequence[Chunk]) – Retrieved chunks, with text (the judge needs it)

  • extractor (Any) – Turns the query into claims

  • judge (Any) – Decides which chunks support which claims

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens

  • epsilon_claims (float) – Permit this fraction of claims to go unsupported

  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Set cover strategy

Returns:

Selected chunks plus coverage accounting

Return type:

ContextSelection

rowvoi.rag.answer_frame(answers, *, questions=None)[source]

Coerce a predicted answer matrix into a candidates x questions frame.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Three accepted shapes: - DataFrame: one row per candidate, one column per question - Mapping: question -> per-candidate answers (column-oriented) - Nested sequence: answers[i][q] (row-oriented); needs questions

  • questions (Sequence[Hashable] | None) – Column labels. Required for the nested-sequence form

Returns:

Candidates as positional rows, questions as columns

Raises:

ValueError – If the nested-sequence form is given without questions, if rows are ragged, or if the matrix has no questions.

Return type:

DataFrame

rowvoi.rag.answer_likelihoods(answers, question, value, *, questions=None, noise=0.0)[source]

P(observed answer | candidate) for one question.

With noise=0 this is a hard indicator. With noise>0 a candidate whose predicted answer disagrees keeps a share of the mass, so one surprising answer – a user typo, a shaky prediction – cannot eliminate the right candidate outright.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • question (Hashable) – Which question was asked

  • value (Any) – The answer actually received

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • noise (float) – Probability that a candidate produces an answer other than its predicted one, spread evenly over the other observed answers. Must be in [0, 1)

Returns:

Likelihood per candidate, in candidate order

Raises:
  • ValueError – If noise is outside [0, 1).

  • KeyError – If question is not a column of the answer matrix.

Return type:

ndarray

rowvoi.rag.next_question(answers, *, questions=None, state=None, prior=None, costs=None, normalize=False)[source]

Pick the question with the best information gain per unit cost.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • state (CandidateState | None) – Current belief. Defaults to uniform over candidates. Questions already in state.observed_cols are excluded

  • prior (Sequence[float] | None) – Per-candidate prior weights, used when state is not given

  • costs (Mapping[Hashable, float] | None) – Per-question cost. With users, cost is patience: a yes/no question is cheap, “paste your config” is not. Ranking is by MI/cost

  • normalize (bool) – Score against normalized MI instead of raw bits

Returns:

.col is the chosen question, .expected_voi its information gain in bits, .score the cost-adjusted ranking value. .col is None when no question is left to ask

Return type:

FeatureSuggestion

Examples

>>> from rowvoi.rag import next_question
>>> # q1 splits the four candidates evenly; q2 tells them apart not at all
>>> answers = {"q1": ["a", "a", "b", "b"], "q2": ["x", "x", "x", "x"]}
>>> suggestion = next_question(answers)
>>> suggestion.col
'q1'
>>> f"{suggestion.expected_voi:.2f}"
'1.00'
rowvoi.rag.observe_answer(state, answers, question, value, *, questions=None, noise=0.0)[source]

Fold a received answer into the belief over candidates.

Soft by construction: this reweights via rowvoi.CandidateState.reweight() rather than filtering, so candidates are never dropped and the posterior stays aligned with the answer matrix across successive questions.

Parameters:
Return type:

CandidateState

Note

Propagates ValueError from rowvoi.CandidateState.reweight() when no candidate could have produced value (with noise=0). That means the candidate set is wrong, not merely narrowed – re-retrieve rather than continuing to ask.

Returns:

Updated belief, with question recorded in observed_cols

Parameters:
Return type:

CandidateState

rowvoi.rag.question_values(answers, *, questions=None, state=None, prior=None, normalize=False)[source]

Score every question by expected information gain, in bits.

Returns the whole ranking rather than just the winner – useful for showing a user their options, or for logging why a question was chosen.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • state (CandidateState | None) – Current belief. Defaults to uniform over candidates

  • prior (Sequence[float] | None) – Per-candidate prior weights (e.g. retrieval scores), used when state is not given. Normalized internally

  • normalize (bool) – Divide by prior entropy, giving a 0-1 fraction of the uncertainty resolved

Returns:

Mutual information per question, in bits (or fractions if normalized)

Return type:

dict[Hashable, float]

class rowvoi.rag.RetrievalSession(outcomes, *, runner=None, probes=None, prior=None, costs=None, noise=0.0)[source]

Bases: object

Run probes against a candidate set until the answer is clear enough.

Parameters:
Raises:

ValueError – If prior has the wrong length, or is negative or all-zero.

property state: CandidateState

Current belief over candidates.

property history: list[ProbeStep]

Probes run so far, in order.

property cumulative_cost: float

Total cost incurred so far.

property steps_taken: int

Number of probes run so far.

property best_candidate: int

Index of the highest-posterior candidate.

next_probe()[source]

Score the unrun probes and return the best one.

Does not run it – call observe() with the result.

Returns:

.col is the probe (None when every probe has been run), .expected_voi its predicted gain in bits

Return type:

FeatureSuggestion

observe(probe, outcome=None, *, likelihoods=None, expected_voi=None)[source]

Fold a probe’s result into the belief.

Parameters:
  • probe (Hashable) – The probe that was run

  • outcome (Any) – What it returned, matched against the predicted outcome matrix. Ignored when likelihoods is given

  • likelihoods (Sequence[float] | None) – P(result | candidate), for callers with their own scoring function (a similarity kernel, a reranker’s scores). Bypasses the matrix

  • expected_voi (float | None) – Predicted gain, recorded for comparison against what was realized

Returns:

Record of this step

Return type:

ProbeStep

run(stop, *, runner=None)[source]

Probe repeatedly until a stop rule fires or probes run out.

Parameters:
  • stop (StopRules) – Stopping criteria. epsilon_posterior is the natural one here – “stop when one candidate holds 95% of the mass”. epsilon_pairs also works, measured over the predicted outcome matrix

  • runner (ProbeRunner | None) – Overrides the runner given to the constructor

Returns:

Every step taken this session, including any from earlier calls

Raises:

ValueError – If no runner is available

Return type:

list[ProbeStep]

reset(*, prior=None)[source]

Clear history and return to the initial (or a new) prior.

Parameters:

prior (Sequence[float] | None)

Return type:

None

class rowvoi.rag.ProbeStep(probe, outcome, cost, cumulative_cost, entropy_before, entropy_after, expected_voi=None)[source]

Bases: object

Record of a single probe in a retrieval session.

Parameters:
probe

The probe that was run

Type:

collections.abc.Hashable

outcome

What it returned (None when likelihoods were supplied directly)

Type:

Any

cost

Cost of this probe

Type:

float

cumulative_cost

Total cost through this step

Type:

float

entropy_before

Posterior entropy in bits before the update

Type:

float

entropy_after

Posterior entropy in bits after the update

Type:

float

expected_voi

Information gain predicted for this probe before running it

Type:

float | None

property realized_gain: float

Bits actually resolved, against expected_voi’s prediction.

class rowvoi.rag.AnswerPredictor(*args, **kwargs)[source]

Bases: Protocol

Predicts the answer each question gets, per candidate.

This is what makes value-of-information computable before asking anything: the predicted answer matrix says how each question would split the candidate set.

predict(candidates, questions)[source]

Return an answer matrix answers[i][q].

answers[i][q] is the answer question questions[q] would receive if candidates[i] were the right candidate. Answers are compared for equality, so normalize them (lowercase, canonical labels) rather than returning free prose.

Parameters:
Return type:

list[list[Any]]

class rowvoi.rag.ClaimExtractor(*args, **kwargs)[source]

Bases: Protocol

Decomposes a query into the claims an answer must support.

extract(query)[source]

Return the list of claims an answer to query must support.

Parameters:

query (str)

Return type:

list[str]

class rowvoi.rag.ProbeRunner(*args, **kwargs)[source]

Bases: Protocol

Executes a retrieval probe and reports what came back.

run(probe)[source]

Run probe and return the observed outcome.

The outcome is compared against the predicted outcome matrix to build a likelihood vector, so it must be drawn from the same value space.

Parameters:

probe (Hashable)

Return type:

Any

class rowvoi.rag.QuestionGenerator(*args, **kwargs)[source]

Bases: Protocol

Proposes clarifying questions that might separate candidates.

generate(candidates, n)[source]

Propose up to n clarifying questions for these candidates.

Parameters:
Return type:

list[str]

class rowvoi.rag.SupportJudge(*args, **kwargs)[source]

Bases: Protocol

Decides which chunks support which claims.

judge(chunks, claims)[source]

Map each claim to the set of chunk ids that support it.

Parameters:
Returns:

Supporting chunks per claim. A claim with no support maps to an empty set rather than being omitted.

Return type:

dict[Hashable, set[Hashable]]

Minimal Sufficient Context

Minimal sufficient context: cover the claims with the fewest/cheapest chunks.

The RAG analogue of rowvoi.find_key(). Where key-finding covers row pairs with columns, this covers claims with chunks – same weighted set cover, same solvers, cost measured in tokens instead of acquisition effort.

Top-k retrieval optimizes each chunk’s relevance independently, so it happily spends the budget on five chunks that all support the same claim while a sixth claim goes unsupported. Set cover optimizes the selection jointly.

class rowvoi.rag.context.Chunk(id, text='', tokens=None)[source]

Bases: object

A retrieved chunk.

Parameters:
id

Stable identifier, used everywhere else in this module

Type:

collections.abc.Hashable

text

The chunk body

Type:

str

tokens

Token count, used as the set-cover cost. When omitted, cost falls back to an explicit costs mapping and then to 1.0 (chunk-count minimization)

Type:

int | None

class rowvoi.rag.context.ContextSelection(chunks=<factory>, covered_claims=<factory>, missing_claims=<factory>, coverage=1.0, total_cost=0.0)[source]

Bases: object

The chunks chosen to support a set of claims.

Parameters:
chunks

Selected chunk ids

Type:

list[collections.abc.Hashable]

covered_claims

Claims supported by the selection

Type:

set[collections.abc.Hashable]

missing_claims

Claims left unsupported – either traded away via epsilon_claims, or unsupported by any retrieved chunk, which is a retrieval failure rather than a selection one

Type:

set[collections.abc.Hashable]

coverage

Fraction of claims covered

Type:

float

total_cost

Summed cost (tokens, when supplied) of the selection

Type:

float

property unsupportable_claims: set[Hashable]

Alias for missing_claims, read as “no chunk could have helped”.

rowvoi.rag.context.select_context(chunks, claims, support, *, costs=None, epsilon_claims=0.0, strategy='greedy', time_limit=None)[source]

Select the cheapest set of chunks that supports the required claims.

Parameters:
  • chunks (Sequence[Chunk] | Sequence[Any]) – Retrieved chunks. Passing Chunk objects lets tokens act as the cost; passing bare ids minimizes chunk count unless costs is given

  • claims (Sequence[Hashable]) – Claims the answer must support. This is the universe – a claim absent from support counts as uncovered rather than being ignored

  • support (Mapping[Any, Iterable[Any]] | DataFrame) – Which chunks support which claims. As a DataFrame: claims on the index, chunk ids as columns, truthy cells meaning support

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens. Defaults to 1.0

  • epsilon_claims (float) – Permit this fraction of claims to go unsupported. Trading 5% of claims often halves the context

  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Any strategy accepted by rowvoi.setcover.SetCoverProblem.solve()

  • time_limit (float | None) – Maximum seconds for the solvers that respect one

Returns:

Selected chunks plus coverage accounting

Return type:

ContextSelection

Examples

>>> from rowvoi.rag import Chunk, select_context
>>> chunks = [Chunk("a", tokens=100), Chunk("b", tokens=100),
...           Chunk("c", tokens=400)]
>>> claims = ["price", "release_date"]
>>> support = {"price": {"a", "c"}, "release_date": {"b", "c"}}
>>> selection = select_context(chunks, claims, support)
>>> sorted(selection.chunks)
['a', 'b']
>>> selection.total_cost
200.0
rowvoi.rag.context.plan_context_path(chunks, claims, support, *, costs=None, weighting='uniform')[source]

Order chunks by marginal claim coverage per token.

Use this instead of select_context() when the budget is the binding constraint rather than the coverage target: the returned path exposes prefix_for_budget() and coverage_curve(), so you can fill a context window and see exactly what the last token bought.

Parameters:
  • chunks (Sequence[Chunk] | Sequence[Any]) – Retrieved chunks

  • claims (Sequence[Hashable]) – Claims the answer must support

  • support (Mapping[Any, Iterable[Any]] | DataFrame) – Which chunks support which claims

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens

  • weighting (str) – “uniform” weights all claims equally; “idf” upweights claims that few chunks support, so scarce evidence is acquired earlier

Returns:

Ordered acquisition path with per-step coverage and cost

Return type:

CoverPath

Examples

>>> from rowvoi.rag import Chunk, plan_context_path
>>> chunks = [Chunk("a", tokens=100), Chunk("b", tokens=100),
...           Chunk("c", tokens=400)]
>>> claims = ["price", "release_date"]
>>> support = {"price": {"a", "c"}, "release_date": {"b", "c"}}
>>> path = plan_context_path(chunks, claims, support)
>>> path.prefix_for_budget(150)
['a']
rowvoi.rag.context.extract_and_select(query, chunks, *, extractor, judge, costs=None, epsilon_claims=0.0, strategy='greedy')[source]

End-to-end selection: extract claims, judge support, then cover.

Convenience wrapper for when you have a ClaimExtractor and SupportJudge (see rowvoi.rag.claude). The two LLM calls happen here; the selection itself is deterministic.

Parameters:
  • query (str) – The user’s question

  • chunks (Sequence[Chunk]) – Retrieved chunks, with text (the judge needs it)

  • extractor (Any) – Turns the query into claims

  • judge (Any) – Decides which chunks support which claims

  • costs (Mapping[Any, float] | None) – Per-chunk cost, overriding Chunk.tokens

  • epsilon_claims (float) – Permit this fraction of claims to go unsupported

  • strategy (Literal['greedy', 'exact', 'ilp', 'sa', 'ga', 'lp', 'hybrid']) – Set cover strategy

Returns:

Selected chunks plus coverage accounting

Return type:

ContextSelection

Clarifying Questions

Clarifying questions: ask the one question that best splits the candidates.

The RAG analogue of “which column should I observe next”. When the retriever returns k chunks with near-tied scores, the posterior over “which one actually answers the query” is flat. Stuffing all k into the context or guessing the top one both discard that ambiguity; asking one good question resolves it.

The mutual information here is computed by rowvoi.policies.CandidateMIPolicy unchanged – a predicted answer matrix is structurally the same object as a table of candidate rows.

rowvoi.rag.questions.answer_frame(answers, *, questions=None)[source]

Coerce a predicted answer matrix into a candidates x questions frame.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Three accepted shapes: - DataFrame: one row per candidate, one column per question - Mapping: question -> per-candidate answers (column-oriented) - Nested sequence: answers[i][q] (row-oriented); needs questions

  • questions (Sequence[Hashable] | None) – Column labels. Required for the nested-sequence form

Returns:

Candidates as positional rows, questions as columns

Raises:

ValueError – If the nested-sequence form is given without questions, if rows are ragged, or if the matrix has no questions.

Return type:

DataFrame

rowvoi.rag.questions.question_values(answers, *, questions=None, state=None, prior=None, normalize=False)[source]

Score every question by expected information gain, in bits.

Returns the whole ranking rather than just the winner – useful for showing a user their options, or for logging why a question was chosen.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • state (CandidateState | None) – Current belief. Defaults to uniform over candidates

  • prior (Sequence[float] | None) – Per-candidate prior weights (e.g. retrieval scores), used when state is not given. Normalized internally

  • normalize (bool) – Divide by prior entropy, giving a 0-1 fraction of the uncertainty resolved

Returns:

Mutual information per question, in bits (or fractions if normalized)

Return type:

dict[Hashable, float]

rowvoi.rag.questions.next_question(answers, *, questions=None, state=None, prior=None, costs=None, normalize=False)[source]

Pick the question with the best information gain per unit cost.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • state (CandidateState | None) – Current belief. Defaults to uniform over candidates. Questions already in state.observed_cols are excluded

  • prior (Sequence[float] | None) – Per-candidate prior weights, used when state is not given

  • costs (Mapping[Hashable, float] | None) – Per-question cost. With users, cost is patience: a yes/no question is cheap, “paste your config” is not. Ranking is by MI/cost

  • normalize (bool) – Score against normalized MI instead of raw bits

Returns:

.col is the chosen question, .expected_voi its information gain in bits, .score the cost-adjusted ranking value. .col is None when no question is left to ask

Return type:

FeatureSuggestion

Examples

>>> from rowvoi.rag import next_question
>>> # q1 splits the four candidates evenly; q2 tells them apart not at all
>>> answers = {"q1": ["a", "a", "b", "b"], "q2": ["x", "x", "x", "x"]}
>>> suggestion = next_question(answers)
>>> suggestion.col
'q1'
>>> f"{suggestion.expected_voi:.2f}"
'1.00'
rowvoi.rag.questions.answer_likelihoods(answers, question, value, *, questions=None, noise=0.0)[source]

P(observed answer | candidate) for one question.

With noise=0 this is a hard indicator. With noise>0 a candidate whose predicted answer disagrees keeps a share of the mass, so one surprising answer – a user typo, a shaky prediction – cannot eliminate the right candidate outright.

Parameters:
  • answers (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted answer matrix; see answer_frame()

  • question (Hashable) – Which question was asked

  • value (Any) – The answer actually received

  • questions (Sequence[Hashable] | None) – Column labels for the nested-sequence form

  • noise (float) – Probability that a candidate produces an answer other than its predicted one, spread evenly over the other observed answers. Must be in [0, 1)

Returns:

Likelihood per candidate, in candidate order

Raises:
  • ValueError – If noise is outside [0, 1).

  • KeyError – If question is not a column of the answer matrix.

Return type:

ndarray

rowvoi.rag.questions.observe_answer(state, answers, question, value, *, questions=None, noise=0.0)[source]

Fold a received answer into the belief over candidates.

Soft by construction: this reweights via rowvoi.CandidateState.reweight() rather than filtering, so candidates are never dropped and the posterior stays aligned with the answer matrix across successive questions.

Parameters:
Return type:

CandidateState

Note

Propagates ValueError from rowvoi.CandidateState.reweight() when no candidate could have produced value (with noise=0). That means the candidate set is wrong, not merely narrowed – re-retrieve rather than continuing to ask.

Returns:

Updated belief, with question recorded in observed_cols

Parameters:
Return type:

CandidateState

Adaptive Retrieval

Adaptive retrieval: choose the next probe by value of information.

Fixed top-k and fixed multi-hop both spend the same budget on every query – the easy ones and the genuinely ambiguous ones alike. This module treats a retrieval probe (a sub-query, an index, a reranker, a tool call) the way rowvoi.rag.questions treats a clarifying question: score it by how much it is expected to sharpen the posterior, run the best one, stop when the residual uncertainty is small enough to answer.

The cost here is latency and tokens rather than user patience, which is why costs matters more than in the clarifying-question case: a cheap BM25 probe and an expensive cross-encoder rerank should not be ranked on raw bits.

class rowvoi.rag.retrieval.ProbeStep(probe, outcome, cost, cumulative_cost, entropy_before, entropy_after, expected_voi=None)[source]

Bases: object

Record of a single probe in a retrieval session.

Parameters:
probe

The probe that was run

Type:

collections.abc.Hashable

outcome

What it returned (None when likelihoods were supplied directly)

Type:

Any

cost

Cost of this probe

Type:

float

cumulative_cost

Total cost through this step

Type:

float

entropy_before

Posterior entropy in bits before the update

Type:

float

entropy_after

Posterior entropy in bits after the update

Type:

float

expected_voi

Information gain predicted for this probe before running it

Type:

float | None

property realized_gain: float

Bits actually resolved, against expected_voi’s prediction.

class rowvoi.rag.retrieval.RetrievalSession(outcomes, *, runner=None, probes=None, prior=None, costs=None, noise=0.0)[source]

Bases: object

Run probes against a candidate set until the answer is clear enough.

Parameters:
Raises:

ValueError – If prior has the wrong length, or is negative or all-zero.

property state: CandidateState

Current belief over candidates.

property history: list[ProbeStep]

Probes run so far, in order.

property cumulative_cost: float

Total cost incurred so far.

property steps_taken: int

Number of probes run so far.

property best_candidate: int

Index of the highest-posterior candidate.

next_probe()[source]

Score the unrun probes and return the best one.

Does not run it – call observe() with the result.

Returns:

.col is the probe (None when every probe has been run), .expected_voi its predicted gain in bits

Return type:

FeatureSuggestion

observe(probe, outcome=None, *, likelihoods=None, expected_voi=None)[source]

Fold a probe’s result into the belief.

Parameters:
  • probe (Hashable) – The probe that was run

  • outcome (Any) – What it returned, matched against the predicted outcome matrix. Ignored when likelihoods is given

  • likelihoods (Sequence[float] | None) – P(result | candidate), for callers with their own scoring function (a similarity kernel, a reranker’s scores). Bypasses the matrix

  • expected_voi (float | None) – Predicted gain, recorded for comparison against what was realized

Returns:

Record of this step

Return type:

ProbeStep

run(stop, *, runner=None)[source]

Probe repeatedly until a stop rule fires or probes run out.

Parameters:
  • stop (StopRules) – Stopping criteria. epsilon_posterior is the natural one here – “stop when one candidate holds 95% of the mass”. epsilon_pairs also works, measured over the predicted outcome matrix

  • runner (ProbeRunner | None) – Overrides the runner given to the constructor

Returns:

Every step taken this session, including any from earlier calls

Raises:

ValueError – If no runner is available

Return type:

list[ProbeStep]

reset(*, prior=None)[source]

Clear history and return to the initial (or a new) prior.

Parameters:

prior (Sequence[float] | None)

Return type:

None

LLM Interfaces

Interfaces for the parts of RAG that need a language model.

Nothing in rowvoi.rag imports an LLM SDK. The algorithms take matrices; these protocols describe the components that fill those matrices. Supply your own, or use rowvoi.rag.claude (needs the claude extra).

class rowvoi.rag.protocols.ClaimExtractor(*args, **kwargs)[source]

Bases: Protocol

Decomposes a query into the claims an answer must support.

extract(query)[source]

Return the list of claims an answer to query must support.

Parameters:

query (str)

Return type:

list[str]

class rowvoi.rag.protocols.SupportJudge(*args, **kwargs)[source]

Bases: Protocol

Decides which chunks support which claims.

judge(chunks, claims)[source]

Map each claim to the set of chunk ids that support it.

Parameters:
Returns:

Supporting chunks per claim. A claim with no support maps to an empty set rather than being omitted.

Return type:

dict[Hashable, set[Hashable]]

class rowvoi.rag.protocols.QuestionGenerator(*args, **kwargs)[source]

Bases: Protocol

Proposes clarifying questions that might separate candidates.

generate(candidates, n)[source]

Propose up to n clarifying questions for these candidates.

Parameters:
Return type:

list[str]

class rowvoi.rag.protocols.AnswerPredictor(*args, **kwargs)[source]

Bases: Protocol

Predicts the answer each question gets, per candidate.

This is what makes value-of-information computable before asking anything: the predicted answer matrix says how each question would split the candidate set.

predict(candidates, questions)[source]

Return an answer matrix answers[i][q].

answers[i][q] is the answer question questions[q] would receive if candidates[i] were the right candidate. Answers are compared for equality, so normalize them (lowercase, canonical labels) rather than returning free prose.

Parameters:
Return type:

list[list[Any]]

class rowvoi.rag.protocols.ProbeRunner(*args, **kwargs)[source]

Bases: Protocol

Executes a retrieval probe and reports what came back.

run(probe)[source]

Run probe and return the observed outcome.

The outcome is compared against the predicted outcome matrix to build a likelihood vector, so it must be drawn from the same value space.

Parameters:

probe (Hashable)

Return type:

Any