Examples#

SARP / WARP Consistency#

Test whether a user’s menu choices form a consistent ranking:

from prefgraph import MenuChoiceLog, validate_menu_warp, validate_menu_sarp

log = MenuChoiceLog(
    menus=[
        frozenset({0, 1, 2}),  # chose 0 from {Pizza, Burger, Salad}
        frozenset({1, 2, 3}),  # chose 1 from {Burger, Salad, Pasta}
        frozenset({0, 3}),     # chose 0 from {Pizza, Pasta}
        frozenset({0, 1, 3}),  # chose 0 from {Pizza, Burger, Pasta}
    ],
    choices=[0, 1, 0, 0],
    item_labels=["Pizza", "Burger", "Salad", "Pasta"],
)

warp = validate_menu_warp(log)
sarp = validate_menu_sarp(log)
print(f"WARP: {warp.is_consistent}  SARP: {sarp.is_consistent}")
WARP: True  SARP: True

Detecting Violations#

A WARP violation: choosing differently from the same pair.

from prefgraph import MenuChoiceLog, validate_menu_warp

log = MenuChoiceLog(
    menus=[frozenset({0, 1}), frozenset({0, 1})],
    choices=[0, 1],
)
result = validate_menu_warp(log)
print(f"WARP: {result.is_consistent}  Violations: {result.violations}")
WARP: False  Violations: [(0, 1)]

Houtman-Maks Efficiency#

How many observations to remove to restore consistency:

from prefgraph import MenuChoiceLog, compute_menu_efficiency

log = MenuChoiceLog(
    menus=[frozenset({0, 1}), frozenset({0, 1}), frozenset({1, 2}), frozenset({0, 2})],
    choices=[0, 1, 1, 0],
)
result = compute_menu_efficiency(log)
print(f"Efficiency: {result.efficiency_index:.2f}")
print(f"Removed: {result.removed_observations}")
Efficiency: 0.75
Removed: [1]

Ordinal Utility Recovery#

Recover a preference ranking from consistent choices:

from prefgraph import MenuChoiceLog, fit_menu_preferences

log = MenuChoiceLog(
    menus=[frozenset({0, 1, 2}), frozenset({1, 2}), frozenset({0, 2})],
    choices=[0, 1, 0],
)
result = fit_menu_preferences(log)
print(f"Preference order: {result.preference_order}")
print(f"Utility values: {result.utility_values}")
Preference order: [0, 1, 2]
Utility values: [3. 2. 1.]

Limited Attention (WARP-LA)#

Test whether violations can be explained by inattention rather than irrationality:

from prefgraph import MenuChoiceLog, test_warp_la

log = MenuChoiceLog(
    menus=[frozenset({0, 1, 2}), frozenset({0, 1}), frozenset({1, 2}), frozenset({0, 2})],
    choices=[0, 0, 1, 2],
)
result = test_warp_la(log)
print(f"WARP(LA): {result.satisfies_warp_la}")
WARP(LA): True

Stochastic Choice (RUM)#

Fit a random utility model to choice frequency data:

from prefgraph import StochasticChoiceLog, fit_random_utility_model

log = StochasticChoiceLog(
    menus=[frozenset({0, 1, 2}), frozenset({0, 1}), frozenset({1, 2})],
    choice_frequencies=[
        {0: 60, 1: 30, 2: 10},
        {0: 70, 1: 30},
        {1: 55, 2: 45},
    ],
    item_labels=["Apple", "Banana", "Cherry"],
)
result = fit_random_utility_model(log, model_type="logit")
print(f"Log-likelihood: {result.log_likelihood:.2f}")
print(f"Satisfies IIA: {result.satisfies_iia}")
Log-likelihood: -89.34
Satisfies IIA: True

Risk Preferences#

Classify risk attitudes from lottery choices:

import numpy as np
from prefgraph import RiskChoiceLog, compute_risk_profile

log = RiskChoiceLog(
    safe_values=np.array([50.0, 40.0, 30.0, 20.0, 10.0]),
    risky_outcomes=np.array([[100.0, 0.0]] * 5),
    risky_probabilities=np.array([[0.5, 0.5]] * 5),
    choices=np.array([False, False, False, True, True]),
)
result = compute_risk_profile(log)
print(f"Risk category: {result.risk_category}")
print(f"Consistency: {result.consistency_score:.0%}")
Risk category: risk_averse
Consistency: 100%

Context Effects (Decoy Detection)#

Detect whether adding items shifts choice probabilities:

from prefgraph import StochasticChoiceLog, detect_decoy_effect

log = StochasticChoiceLog(
    menus=[
        frozenset({0, 1, 2}),
        frozenset({0, 1, 2, 3}),  # decoy added
        frozenset({0, 1}),
        frozenset({1, 2}),
    ],
    choice_frequencies=[
        {0: 30, 1: 45, 2: 25},
        {0: 22, 1: 38, 2: 35, 3: 5},
        {0: 40, 1: 60},
        {1: 55, 2: 45},
    ],
    total_observations_per_menu=[100, 100, 100, 100],
    item_labels=["Basic", "Standard", "Premium", "Decoy"],
)
result = detect_decoy_effect(log, threshold=0.05)
print(f"Decoy effect: {result.has_decoy_effect}")
print(f"Magnitude: {result.magnitude:.1%}")
Decoy effect: True
Magnitude: 10.0%

Ranking and Pairwise Comparison#

Fit a Bradley-Terry model from pairwise data:

from prefgraph import fit_bradley_terry

# (winner, loser, count)
comparisons = [
    (0, 1, 15), (1, 0, 5),
    (0, 2, 18), (2, 0, 2),
    (1, 2, 12), (2, 1, 8),
]
result = fit_bradley_terry(comparisons, method="mle")
print(f"Ranking: {result.ranking}")
for item, score in sorted(result.scores.items(), key=lambda x: -x[1]):
    print(f"  Item {item}: {score:.3f}")
Ranking: [0, 1, 2]
  Item 0: 1.523
  Item 1: 0.412
  Item 2: 0.000

Batch Menu Scoring (Engine)#

Score thousands of users via the Rust engine:

from prefgraph import MenuChoicePanel
from prefgraph.engine import Engine

panel = MenuChoicePanel.from_dataframe(
    df, user_col="user_id", menu_col="shown_items", choice_col="clicked"
)
engine = Engine()
results = engine.analyze_menus(panel.to_engine_tuples())

for r in results[:3]:
    print(r)