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:
objectRepresents the current uncertainty over which row is “the one”.
- Parameters:
- candidate_rows¶
List of row indices under consideration
- 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:
- observed_values¶
Mapping col -> observed value (may be empty in planning mode)
- Type:
- classmethod uniform(candidate_rows, observed_cols=None, observed_values=None)[source]¶
Create a state with uniform posterior over candidates.
- filter_candidates(df, col, value)[source]¶
Filter candidates to those matching the observed value.
- Parameters:
- Returns:
New state with filtered candidates and renormalized posterior
- Return type:
- 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:
- class rowvoi.core.FeatureSuggestion(col, score, expected_voi=None, marginal_cost=None, debug=None)[source]¶
Bases:
objectA recommendation of which column to query next.
- Parameters:
- col¶
The column name suggested to query next
- Type:
collections.abc.Hashable | None
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.
Bases:
RuntimeErrorNo 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:
objectA single step in a cover path showing incremental progress.
- Parameters:
- name¶
The set added in this step
- Type:
- class rowvoi.setcover.CoverPath(steps)[source]¶
Bases:
objectOrdered sequence of sets and their contribution to coverage/cost.
- steps¶
Ordered list of steps showing incremental progress
- Type:
- prefix_for_budget(budget)[source]¶
Return the longest prefix of sets whose cumulative_cost <= budget.
- class rowvoi.setcover.SetCoverProblem(sets, *, universe=None, costs=None)[source]¶
Bases:
objectWeighted 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
- is_cover(names, *, epsilon=0.0)[source]¶
Whether a selection covers all but at most epsilon of the universe.
- 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:
- 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.
- 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:
- Returns:
Ordered sequence with coverage information
- Return type:
- 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().
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:
objectA single step in a key path showing incremental progress.
- Parameters:
- col¶
The column added in this step
- Type:
- class rowvoi.keys.KeyPath(steps)[source]¶
Bases:
objectOrdered sequence of columns and their contribution to coverage/cost.
- Parameters:
steps (list[KeyPathStep])
- steps¶
Ordered list of steps showing incremental progress
- Type:
- prefix_for_budget(budget)[source]¶
Return the longest prefix of columns whose cumulative_cost <= budget.
- rowvoi.keys.pairwise_coverage(df, rows, cols)[source]¶
Fraction of unordered row pairs in rows that are distinguished by cols.
- class rowvoi.keys.KeyProblem(df, rows, *, columns=None, costs=None)[source]¶
Bases:
objectDeterministic 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:
- is_key(cols, *, epsilon_pairs=0.0)[source]¶
Check if cols distinguish all but at most epsilon_pairs fraction.
- 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:
- 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:
- Returns:
Minimal set of columns
- Return type:
- 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:
- Returns:
Ordered sequence with coverage information
- Return type:
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
model (RowVoiModel) – Trained model for computing expected information
epsilon_posterior (float) – Target residual uncertainty
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:
- 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
model (RowVoiModel) – Trained model for computing expected information
objective (Literal['mi', 'mi_over_cost', 'expected_entropy_reduction']) – Objective for ordering columns
costs (Mapping[Hashable, float] | None) – Cost of each column
- Returns:
Expected path with coverage information
- Return type:
- 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
model (RowVoiModel | None) – Model for probabilistic estimation. If None, uses deterministic coverage.
- Returns:
Estimated probability of full disambiguation
- Return type:
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:
ProtocolA 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:
- class rowvoi.policies.GreedyCoveragePolicy(costs=None, objective='pairs', weighting='uniform')[source]¶
Bases:
objectStateless 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:
df (DataFrame)
state (CandidateState)
- Return type:
- class rowvoi.policies.MIPolicy(model, objective='mi_over_cost', feature_costs=None)[source]¶
Bases:
objectPolicy that uses mutual information from a RowVoiModel.
- Parameters:
model (RowVoiModel)
objective (Literal['mi', 'mi_over_cost'])
- model¶
The trained model for computing MI
- Type:
- 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:
- class rowvoi.policies.CandidateMIPolicy(normalize=False, costs=None)[source]¶
Bases:
objectPolicy using local mutual information on candidate set only.
This policy doesn’t require a trained model - it computes MI directly from the candidate rows.
- 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:
df (DataFrame)
state (CandidateState)
- Return type:
- 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:
df (DataFrame)
state (CandidateState)
col (Hashable)
- Return type:
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:
objectConditions for stopping a disambiguation session.
- Parameters:
- 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:
- class rowvoi.session.SessionStep(col, observed_value, suggestion, cost, cumulative_cost, entropy_before, entropy_after, pair_coverage_after=None)[source]¶
Bases:
objectRecord of a single step in a disambiguation session.
- Parameters:
- col¶
Column that was queried
- Type:
- observed_value¶
Value observed for the true row
- Type:
Any
- suggestion¶
The suggestion that led to this query
- class rowvoi.session.DisambiguationSession(df, candidate_rows, *, prior=None, policy, feature_costs=None)[source]¶
Bases:
objectInteractive 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.
- next_question(candidate_cols=None)[source]¶
Ask the policy for the next best column.
Does NOT update state yet - just returns the suggestion.
- observe(col, value)[source]¶
Incorporate an observation into the state.
- Parameters:
- Returns:
Record of this step
- Return type:
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:
objectModel for computing expected value of information across features.
A
RowVoiModelencapsulates 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,
noisespreads 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
noiseis 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_colswill be treated as discrete; ifNone, 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
NaNas 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 sizebins.bins (int) – Number of quantile bins for discretization of numeric columns not specified in
discrete_cols. Default is 3.
- Returns:
Returns
selffor chaining.- Return type:
- 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 informationI(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_colswas set when constructing the model, the normalized mutual information (MI divided by the feature entropy) is returned in theFeatureSuggestionfor diagnostic purposes but is not used to rank features unlessobjectiveis 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 instate.observed_colsare 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
FeatureSuggestioncontaining the best column and associated information gain estimates, orNoneif 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. IfFalse, continue untilmax_stepsis 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:
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:
- Returns:
List of row index lists
- Raises:
ValueError – If subset_size exceeds the number of rows in the frame.
- Return type:
- 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
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:
- 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:
- class rowvoi.eval.KeyEvalResult(method, rows, key, key_cost, pair_coverage, runtime_sec, gold_key=None, gold_cost=None, optimality_gap=None)[source]¶
Bases:
objectResult of evaluating a key-finding method.
- Parameters:
- rows¶
The candidate row set
- Type:
- key¶
Columns selected by the method
- Type:
- gold_key¶
Optimal key if computed
- Type:
list[collections.abc.Hashable] | 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:
df (DataFrame) – The data table
candidate_sets (Sequence[Sequence[Hashable]]) – Test cases (row subsets)
methods (Mapping[str, Callable[[DataFrame, Sequence[Hashable]], Sequence[Hashable]]]) – Methods to evaluate (name -> function)
epsilon_pairs (float) – Coverage tolerance
gold_solver (Callable[[...], Sequence[Hashable]] | None) – Function to compute optimal solution
- Returns:
Evaluation results for each method and candidate set
- Return type:
- 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:
objectStatistics for a policy’s performance.
- Parameters:
- 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:
- class rowvoi.eval.AcquisitionResult(subset_size, steps_used, unique_identified, optimal_steps=None, cols_used=None)[source]¶
Bases:
objectResult of a single feature acquisition simulation.
- Parameters:
- 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:
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 |
|
Next column by MI |
|
Sequential acquisition |
|
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:
objectA retrieved chunk.
- id¶
Stable identifier, used everywhere else in this module
- Type:
- class rowvoi.rag.ContextSelection(chunks=<factory>, covered_claims=<factory>, missing_claims=<factory>, coverage=1.0, total_cost=0.0)[source]¶
Bases:
objectThe chunks chosen to support a set of claims.
- Parameters:
- chunks¶
Selected chunk ids
- Type:
- covered_claims¶
Claims supported by the selection
- Type:
- 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:
- 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
Chunkobjects lets tokens act as the cost; passing bare ids minimizes chunk count unless costs is givenclaims (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:
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 exposesprefix_for_budget()andcoverage_curve(), so you can fill a context window and see exactly what the last token bought.- Parameters:
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:
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
ClaimExtractorandSupportJudge(seerowvoi.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:
- 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:
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:
state (CandidateState) – Belief before the answer
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) – Per-candidate probability of an off-prediction answer
- Return type:
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.
- 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:
- class rowvoi.rag.RetrievalSession(outcomes, *, runner=None, probes=None, prior=None, costs=None, noise=0.0)[source]¶
Bases:
objectRun probes against a candidate set until the answer is clear enough.
- Parameters:
outcomes (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted outcome matrix: outcomes[i][p] is what probe p would return if candidate i were the right one. Same shapes as
rowvoi.rag.questions.answer_frame()runner (ProbeRunner | None) – Executes probes in
run(). Not needed for manualnext_probe()/observe()drivingprobes (Sequence[Hashable] | None) – Column labels, required when outcomes is a nested sequence
prior (Sequence[float] | None) – Per-candidate prior, typically the retrieval scores. Normalized internally; defaults to uniform
costs (Mapping[Hashable, float] | None) – Per-probe cost in whatever unit the budget is denominated
noise (float) – Probability a probe returns something other than predicted. Leave at 0 only if the outcome predictions are exact; retrieval rarely is
- Raises:
ValueError – If prior has the wrong length, or is negative or all-zero.
- property state: CandidateState¶
Current belief over candidates.
- 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:
- 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:
- 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:
- class rowvoi.rag.ProbeStep(probe, outcome, cost, cumulative_cost, entropy_before, entropy_after, expected_voi=None)[source]¶
Bases:
objectRecord of a single probe in a retrieval session.
- Parameters:
- probe¶
The probe that was run
- Type:
- outcome¶
What it returned (None when likelihoods were supplied directly)
- Type:
Any
- class rowvoi.rag.AnswerPredictor(*args, **kwargs)[source]¶
Bases:
ProtocolPredicts 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.
- class rowvoi.rag.ClaimExtractor(*args, **kwargs)[source]¶
Bases:
ProtocolDecomposes a query into the claims an answer must support.
- class rowvoi.rag.ProbeRunner(*args, **kwargs)[source]¶
Bases:
ProtocolExecutes a retrieval probe and reports what came back.
- class rowvoi.rag.QuestionGenerator(*args, **kwargs)[source]¶
Bases:
ProtocolProposes clarifying questions that might separate candidates.
- class rowvoi.rag.SupportJudge(*args, **kwargs)[source]¶
Bases:
ProtocolDecides which chunks support which claims.
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:
objectA retrieved chunk.
- id¶
Stable identifier, used everywhere else in this module
- Type:
- class rowvoi.rag.context.ContextSelection(chunks=<factory>, covered_claims=<factory>, missing_claims=<factory>, coverage=1.0, total_cost=0.0)[source]¶
Bases:
objectThe chunks chosen to support a set of claims.
- Parameters:
- chunks¶
Selected chunk ids
- Type:
- covered_claims¶
Claims supported by the selection
- Type:
- 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:
- 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
Chunkobjects lets tokens act as the cost; passing bare ids minimizes chunk count unless costs is givenclaims (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:
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 exposesprefix_for_budget()andcoverage_curve(), so you can fill a context window and see exactly what the last token bought.- Parameters:
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:
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
ClaimExtractorandSupportJudge(seerowvoi.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:
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:
- 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:
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:
state (CandidateState) – Belief before the answer
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) – Per-candidate probability of an off-prediction answer
- Return type:
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.
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:
objectRecord of a single probe in a retrieval session.
- Parameters:
- probe¶
The probe that was run
- Type:
- outcome¶
What it returned (None when likelihoods were supplied directly)
- Type:
Any
- class rowvoi.rag.retrieval.RetrievalSession(outcomes, *, runner=None, probes=None, prior=None, costs=None, noise=0.0)[source]¶
Bases:
objectRun probes against a candidate set until the answer is clear enough.
- Parameters:
outcomes (DataFrame | Mapping[Hashable, Sequence[Any]] | Sequence[Sequence[Any]]) – Predicted outcome matrix: outcomes[i][p] is what probe p would return if candidate i were the right one. Same shapes as
rowvoi.rag.questions.answer_frame()runner (ProbeRunner | None) – Executes probes in
run(). Not needed for manualnext_probe()/observe()drivingprobes (Sequence[Hashable] | None) – Column labels, required when outcomes is a nested sequence
prior (Sequence[float] | None) – Per-candidate prior, typically the retrieval scores. Normalized internally; defaults to uniform
costs (Mapping[Hashable, float] | None) – Per-probe cost in whatever unit the budget is denominated
noise (float) – Probability a probe returns something other than predicted. Leave at 0 only if the outcome predictions are exact; retrieval rarely is
- Raises:
ValueError – If prior has the wrong length, or is negative or all-zero.
- property state: CandidateState¶
Current belief over candidates.
- 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:
- 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:
- 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:
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:
ProtocolDecomposes a query into the claims an answer must support.
- class rowvoi.rag.protocols.SupportJudge(*args, **kwargs)[source]¶
Bases:
ProtocolDecides which chunks support which claims.
- class rowvoi.rag.protocols.QuestionGenerator(*args, **kwargs)[source]¶
Bases:
ProtocolProposes clarifying questions that might separate candidates.
- class rowvoi.rag.protocols.AnswerPredictor(*args, **kwargs)[source]¶
Bases:
ProtocolPredicts 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.