Examples¶
Every example on this page is executed as part of the documentation build, so the printed output is what the code actually produces.
Entity resolution¶
You have records that may refer to the same person and need to know which fields settle it.
import pandas as pd
from rowvoi import KeyProblem
customers = pd.DataFrame({
"name": ["Alice Smith", "Alice Smith", "Bob Jones", "Alice Smith"],
"city": ["NYC", "NYC", "NYC", "SF"],
"email": ["a@x.com", "alice@y.com", "b@x.com", "a@x.com"],
"tier": ["gold", "gold", "gold", "gold"],
})
problem = KeyProblem(customers, rows=[0, 1, 2, 3])
print(problem.is_key(["name"]))
print(problem.is_key(["name", "city"]))
print(problem.is_key(["email", "city"]))
False
False
True
is_key is a method on the problem, not a free function. Partial credit is
available through epsilon_pairs, which asks only that a given fraction of
row pairs be separated:
print(f"{problem.pairwise_coverage(['name']):.2f}")
print(problem.is_key(["name"], epsilon_pairs=0.5))
0.50
True
Cost-aware acquisition¶
Fields differ in what they cost to obtain, so a minimal key by count is not necessarily a minimal key by effort.
from rowvoi import find_key, plan_key_path
costs = {"name": 1.0, "city": 1.0, "email": 20.0, "tier": 1.0}
print(find_key(customers, [0, 1, 2, 3], strategy="exact"))
print(find_key(customers, [0, 1, 2, 3], costs=costs, strategy="exact"))
['city', 'email']
['city', 'email']
Here the expensive field is genuinely required – nothing else separates the
two New York Alices – so pricing it does not change the answer.
plan_key_path gives the acquisition order along with what each step
buys:
path = plan_key_path(customers, [0, 1, 2, 3], costs=costs)
for step in path.steps:
print(f"{step.col:6s} cost={step.cumulative_cost:5.1f} "
f"coverage={step.coverage:.0%}")
name cost= 1.0 coverage=50%
city cost= 2.0 coverage=83%
email cost= 22.0 coverage=100%
That curve is the useful part: five sixths of the ambiguity goes away for two units of effort, and the last sixth costs twenty more.
print(path.prefix_for_budget(10.0))
print(path.prefix_for_epsilon_pairs(0.2))
['name', 'city']
['name', 'city']
Running an interactive session¶
A session drives a policy to completion, either against a real user or, as here, a simulated one.
from rowvoi import CandidateMIPolicy, DisambiguationSession, StopRules
session = DisambiguationSession(
customers,
candidate_rows=[0, 1, 2, 3],
policy=CandidateMIPolicy(costs=costs),
feature_costs=costs,
)
steps = session.run(StopRules(target_unique=True), true_row=1)
for step in steps:
print(f"asked {step.col!r} -> {step.observed_value!r}")
print("resolved to row", session.state.unique_row)
asked 'name' -> 'Alice Smith'
asked 'city' -> 'NYC'
asked 'email' -> 'alice@y.com'
resolved to row 1
Model-based selection¶
RowVoiModel learns value frequencies from historical data and
carries a noise model, so a disagreeing observation lowers a candidate’s
probability instead of eliminating it.
from rowvoi import CandidateState, RowVoiModel
model = RowVoiModel(noise=0.05).fit(customers)
state = CandidateState.uniform([0, 1, 2, 3])
suggestion = model.suggest_next_feature(customers, state)
print(suggestion.col)
print(f"{suggestion.expected_voi:.2f} bits")
email
1.17 bits
Benchmarking policies¶
evaluate_policies runs several policies over sampled candidate sets and
reports averages.
from rowvoi import (
GreedyCoveragePolicy,
RandomPolicy,
evaluate_policies,
sample_candidate_sets,
)
candidate_sets = sample_candidate_sets(
customers, subset_size=2, n_samples=5, random_state=0
)
stats = evaluate_policies(
customers,
candidate_sets,
policies={
"coverage": GreedyCoveragePolicy(),
"mutual_info": CandidateMIPolicy(),
"random": RandomPolicy(seed=0),
},
stop=StopRules(target_unique=True),
)
for s in sorted(stats, key=lambda s: s.name):
print(f"{s.name:12s} mean_steps={s.mean_steps:.2f}")
coverage mean_steps=1.00
mutual_info mean_steps=1.00
random mean_steps=1.60
sample_candidate_sets takes subset_size as a keyword-only argument,
and evaluate_policies takes a mapping of names to policy objects.