Source code for prefgraph.algorithms.abstract_choice
"""Abstract Choice Theory algorithms for menu-based preference analysis.
This module implements revealed preference axioms for abstract choice data
(choices from menus without prices). Based on Chapters 1-2 of
Chambers & Echenique (2016) "Revealed Preference Theory".
Tech-Friendly Names (Primary):
- validate_menu_warp(): Check WARP for menu choices
- validate_menu_sarp(): Check SARP for menu choices
- validate_menu_consistency(): Check full rationalizability (Congruence)
- compute_menu_efficiency(): Houtman-Maks efficiency index
- fit_menu_preferences(): Recover ordinal preference ranking
Economics Names (Legacy Aliases):
- check_abstract_warp() -> validate_menu_warp()
- check_abstract_sarp() -> validate_menu_sarp()
- check_congruence() -> validate_menu_consistency()
- compute_abstract_efficiency() -> compute_menu_efficiency()
- recover_ordinal_utility() -> fit_menu_preferences()
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import numpy as np
from numpy.typing import NDArray
from prefgraph.core.result import (
AbstractWARPResult,
AbstractSARPResult,
CongruenceResult,
HoutmanMaksAbstractResult,
OrdinalUtilityResult,
)
from prefgraph.core.types import Cycle
from prefgraph.graph.transitive_closure import floyd_warshall_transitive_closure
from prefgraph._kernels import (
bfs_find_path_numba,
find_symmetric_pairs_bool_numba,
topological_sort_numba,
)
if TYPE_CHECKING:
from prefgraph.core.session import MenuChoiceLog
def _find_cycle_from_pair(
R: NDArray[np.bool_],
start: int,
end: int,
) -> Cycle | None:
"""
Find a cycle between two nodes using BFS (numba-accelerated).
Args:
R: Revealed preference adjacency matrix
start: Starting node
end: Ending node (should be reachable from start and vice versa)
Returns:
Tuple of node indices forming the cycle, or None if not found
"""
# BFS from start to end using numba kernel
path_to_end = bfs_find_path_numba(R, np.int64(start), np.int64(end))
if path_to_end[0] == -1:
return None
# BFS from end back to start using numba kernel
path_back = bfs_find_path_numba(R, np.int64(end), np.int64(start))
if path_back[0] == -1:
return None
# Combine paths to form cycle
# path_to_end: start -> ... -> end -> start (numba returns with start at end)
# path_back: end -> ... -> start -> end (numba returns with end at end)
# We need: start -> ... -> end -> ... -> start
# path_to_end already goes start->...->end->start, just need start->end path
# path_back goes end->...->start->end, we need end->start path
# Extract path from start to end (exclude the cycling back)
path_to_end_list = list(path_to_end[:-1]) # Remove the repeat at end
path_back_list = list(path_back[:-1]) # Remove the repeat at end
# Cycle: start -> ... -> end -> ... -> start
cycle = path_to_end_list[:-1] + path_back_list
return tuple(cycle)
# =============================================================================
# LEGACY ALIASES (Economics terminology)
# =============================================================================
check_abstract_warp = validate_menu_warp
"""Legacy alias: use validate_menu_warp instead."""
check_abstract_sarp = validate_menu_sarp
"""Legacy alias: use validate_menu_sarp instead."""
menu_sarp_check = validate_menu_sarp
"""Technical alias: use validate_menu_sarp instead."""
menu_warp_check = validate_menu_warp
"""Technical alias: use validate_menu_warp instead."""
check_congruence = validate_menu_consistency
"""Legacy alias: use validate_menu_consistency instead."""
compute_abstract_efficiency = compute_menu_efficiency
"""Legacy alias: use compute_menu_efficiency instead."""
recover_ordinal_utility = fit_menu_preferences
"""Legacy alias: use fit_menu_preferences instead."""