API#

Most users need only the Engine class. For budget data (prices and quantities), call Engine.analyze_arrays() or Engine.analyze_parquet(). For menu data (discrete choices from item sets), call Engine.analyze_menus(). The one-liner prefgraph.analyze() handles DataFrames directly. See the Loading Data guide for required input schemas and the Installation page for choosing between array, Parquet, and event-log workflows.

One-Liner API#

prefgraph.analyze(df, *, user_col='user_id', item_col=None, cost_col=None, action_col=None, time_col=None, cost_cols=None, action_cols=None, menu_col=None, choice_col=None, metrics=None, output='dataframe', nan_policy='raise', price_col=None, qty_col=None, price_cols=None, qty_cols=None)[source]#

Score rationality of choices in a pandas DataFrame.

Auto-detects whether your data is wide-format, long-format (transaction logs), or menu-choice based on which parameters you provide.

Parameters:
  • df (Any) – pandas DataFrame containing choice data.

  • user_col (str) – Column name for user/household IDs (default "user_id").

  • item_col (str | None) – (Long format) Column for item/product identifiers.

  • cost_col (str | None) – (Long format) Column for prices/costs.

  • action_col (str | None) – (Long format) Column for quantities/actions.

  • time_col (str | None) – (Long format) Column for time/observation identifiers.

  • cost_cols (list[str] | None) – (Wide format) List of column names for cost vectors.

  • action_cols (list[str] | None) – (Wide format) List of column names for action vectors.

  • menu_col (str | None) – (Menu) Column containing sets/lists of available items.

  • choice_col (str | None) – (Menu) Column containing the chosen item.

  • metrics (list[str] | None) – Engine metrics to compute. Default ["garp", "ccei", "mpi"] for budget data. Ignored for menu data (always SARP/WARP/HM).

  • output (Literal['dataframe', 'objects']) – "dataframe" (default) returns a pandas DataFrame with one row per user. "objects" returns a list of EngineResult/MenuResult.

  • nan_policy (Literal['raise', 'warn', 'drop']) – How to handle NaN/Inf values. "raise" (default) raises an error. "drop" silently removes affected rows. "warn" drops with a warning.

  • price_col (str | None) – Alias for cost_col.

  • qty_col (str | None) – Alias for action_col.

  • price_cols (list[str] | None) – Alias for cost_cols.

  • qty_cols (list[str] | None) – Alias for action_cols.

Returns:

pandas DataFrame (default) or list of result objects.

Return type:

Any

Examples

Wide format:

results = rp.analyze(df,
    cost_cols=["price_A", "price_B"],
    action_cols=["qty_A", "qty_B"])

Long format (transaction logs):

results = rp.analyze(df,
    item_col="product", cost_col="price",
    action_col="quantity", time_col="week")

Menu choice:

results = rp.analyze(df,
    menu_col="shown_items", choice_col="clicked")

Engine (Batch Scoring)#

Engine#

class prefgraph.Engine(metrics=('garp', 'ccei'), chunk_size=50000, tolerance=1e-10)[source]#

Bases: object

Analyzes revealed preference for millions of users.

Automatically routes to Rust (if available) or Python backend.

Parameters:
  • metrics (tuple[str, ...] | list[str]) – Which metrics to compute. “garp” is always included. Supported: “garp”, “ccei”, “mpi”, “harp”, “hm”, “utility”, “vei”.

  • chunk_size (int) – Number of users per batch (for streaming / memory bounding).

  • tolerance (float) – Numerical tolerance for GARP comparisons.

SUPPORTED_METRICS = {'ccei', 'garp', 'harp', 'hm', 'mpi', 'network', 'utility', 'vei', 'vei_exact'}#
__init__(metrics=('garp', 'ccei'), chunk_size=50000, tolerance=1e-10)[source]#
Parameters:
analyze_arrays(users, data_type='budget')[source]#

Analyze users from a list of array pairs.

Parameters:
  • users (list[tuple[ndarray, ndarray]]) – For budget data: list of (prices T*K, quantities T*K).

  • data_type (str) – “budget” (default). “menu” and “production” not yet implemented.

Return type:

list[EngineResult]

Returns list of EngineResult, one per user.

build_graph(prices, quantities, tolerance=None)[source]#

Build an observation graph and return it as numpy arrays.

Tier 2 entry point for deep per-user analysis. Python modules (utility.py, welfare.py, etc.) can consume the Rust-computed graph.

Returns dict with keys:

r, p, r_star: T*T uint8 arrays (boolean preference matrices) expenditure: T*T float64 (expenditure matrix E) edge_weights: T*T float64 (log-ratios for HARP) own_expenditure: T float64 (diagonal of E) scc_labels: T uint32 (SCC component IDs) is_garp, n_violations, max_scc, n_components, t: scalars

Parameters:
Return type:

dict

analyze_menus(users, compute_warp_la=False)[source]#

Analyze discrete/menu choice data for multiple users.

Each user tuple (menus, choices, n_items) where:

  • menus: list of menus, each a list of item indices shown

  • choices: list of chosen item index per menu

  • n_items: total number of distinct items for this user

Returns list of MenuResult with SARP, WARP, HM scores.

Example:

users = [
    ([[0,1,2,3], [1,2,4], [0,3,4]], [2, 1, 0], 5),  # user 0
    ([[0,1], [1,2], [0,2]], [0, 1, 2], 3),            # user 1
]
results = engine.analyze_menus(users)
Parameters:
Return type:

list[MenuResult]

analyze_parquet(path, *, user_col='user_id', cost_cols=None, action_cols=None, item_col=None, cost_col=None, action_col=None, time_col=None, output_path=None)[source]#

Stream-analyze a Parquet file without loading it all into memory.

Reads row groups incrementally, groups by user, and feeds chunks to the Rust engine. Memory stays bounded at O(chunk_size) regardless of total dataset size.

Parameters:
  • path (str | Any) – Path to Parquet file.

  • user_col (str) – Column for user identifiers.

  • cost_cols (list[str] | None) – (Wide format) Price column names.

  • action_cols (list[str] | None) – (Wide format) Quantity column names.

  • item_col (str | None) – (Long format) Item identifier column.

  • cost_col (str | None) – (Long format) Price column.

  • action_col (str | None) – (Long format) Quantity column.

  • time_col (str | None) – (Long format) Time/period column.

  • output_path (str | None) – If set, write results incrementally to this Parquet file instead of accumulating in memory.

Returns:

pandas DataFrame with one row per user (or path to output Parquet if output_path is set).

Return type:

Any

EngineResult#

class prefgraph.EngineResult(is_garp, n_violations=0, ccei=1.0, mpi=0.0, is_harp=None, hm_consistent=None, hm_total=None, utility_success=None, vei_mean=1.0, vei_min=1.0, vei_exact_mean=1.0, vei_exact_min=1.0, max_scc=0, compute_time_us=0, vei_std=0.0, vei_q25=1.0, vei_q75=1.0, vei_exact_std=0.0, vei_exact_q25=1.0, vei_exact_q75=1.0, n_scc=0, harp_severity=1.0, scc_mean_size=0.0, r_density=0.0, r_out_degree_std=0.0, degree_gini=0.0, ew_mean=0.0, ew_std=0.0, ew_skew=0.0)[source]#

Bases: object

Result for one user from the Engine (budget data).

Contains all metrics requested via Engine(metrics=[...]). Unrequested numeric metrics retain mathematically correct defaults (ccei=1.0, mpi=0.0). Unrequested boolean/count metrics default to None (not False/0) so they render as NaN in DataFrames - unambiguously “not computed” rather than “failed”.

Variables:
  • is_garp (bool) – True if choices satisfy GARP (no revealed-preference cycles).

  • n_violations (int) – Number of GARP violation pairs. 0 when consistent.

  • ccei (float) – Critical Cost Efficiency Index (Afriat 1967). 1.0 = perfectly rational; lower values indicate wasted budget. Range: (0, 1].

  • mpi (float) – Money Pump Index (Echenique, Lee & Shum 2011). Average exploitability per dollar. 0.0 = unexploitable. Range: [0, 1).

  • is_harp (bool | None) – True if choices satisfy HARP (homothetic preferences).

  • hm_consistent (int | None) – Houtman-Maks: number of consistent observations (budget) or items (menu).

  • hm_total (int | None) – Total observations (budget) or items (menu).

  • utility_success (bool | None) – True if Afriat’s LP recovered a rationalizing utility.

  • vei_mean (float) – Mean Varian Efficiency Index across observations. Range: [0, 1].

  • vei_min (float) – Worst-observation VEI. Range: [0, 1].

  • vei_exact_mean (float) – VEI via exact LP (vs binary-search approximation).

  • vei_exact_min (float) – Exact VEI, worst observation.

  • max_scc (int) – Largest strongly connected component in observation graph. 1 = acyclic (no entangled violations).

  • compute_time_us (int) – Wall-clock computation time in microseconds.

Parameters:
is_garp: bool#
n_violations: int = 0#
ccei: float = 1.0#
mpi: float = 0.0#
is_harp: bool | None = None#
hm_consistent: int | None = None#
hm_total: int | None = None#
utility_success: bool | None = None#
vei_mean: float = 1.0#
vei_min: float = 1.0#
vei_exact_mean: float = 1.0#
vei_exact_min: float = 1.0#
max_scc: int = 0#
compute_time_us: int = 0#
vei_std: float = 0.0#
vei_q25: float = 1.0#
vei_q75: float = 1.0#
vei_exact_std: float = 0.0#
vei_exact_q25: float = 1.0#
vei_exact_q75: float = 1.0#
n_scc: int = 0#
harp_severity: float = 1.0#
scc_mean_size: float = 0.0#
r_density: float = 0.0#
r_out_degree_std: float = 0.0#
degree_gini: float = 0.0#
ew_mean: float = 0.0#
ew_std: float = 0.0#
ew_skew: float = 0.0#
to_dict()[source]#

Return dictionary representation for serialization.

Return type:

dict[str, Any]

summary()[source]#

Return human-readable summary report.

Return type:

str

__init__(is_garp, n_violations=0, ccei=1.0, mpi=0.0, is_harp=None, hm_consistent=None, hm_total=None, utility_success=None, vei_mean=1.0, vei_min=1.0, vei_exact_mean=1.0, vei_exact_min=1.0, max_scc=0, compute_time_us=0, vei_std=0.0, vei_q25=1.0, vei_q75=1.0, vei_exact_std=0.0, vei_exact_q25=1.0, vei_exact_q75=1.0, n_scc=0, harp_severity=1.0, scc_mean_size=0.0, r_density=0.0, r_out_degree_std=0.0, degree_gini=0.0, ew_mean=0.0, ew_std=0.0, ew_skew=0.0)#
Parameters:
Return type:

None

High-Level Classes#

BehavioralAuditor#

class prefgraph.BehavioralAuditor(precision=1e-06)[source]#

Bases: object

Validates behavioral consistency in user action logs.

BehavioralAuditor is the “linter” for user behavior. It checks if a user’s historical actions are internally consistent with utility maximization.

Example

>>> from prefgraph import BehavioralAuditor, BehaviorLog
>>> import numpy as np
>>> # Create behavior log
>>> log = BehaviorLog(
...     cost_vectors=np.array([[1.0, 2.0], [2.0, 1.0]]),
...     action_vectors=np.array([[3.0, 1.0], [1.0, 3.0]]),
...     user_id="user_123"
... )
>>> # Run audit
>>> auditor = BehavioralAuditor()
>>> if auditor.validate_history(log):
...     print("User behavior is consistent")
... else:
...     print("Inconsistent behavior detected")
>>> # Get detailed scores
>>> score = auditor.get_integrity_score(log)
>>> print(f"Behavioral integrity: {score:.2f}")
Variables:

precision – Numerical precision for consistency checks (default: 1e-6)

Parameters:

precision (float)

__init__(precision=1e-06)[source]#

Initialize the auditor.

Parameters:

precision (float) – Numerical precision for floating-point comparisons. Smaller values are more strict.

Return type:

None

validate_history(log)[source]#

Check if user behavior history is internally consistent.

A consistent history means the user’s choices don’t contradict each other transitively. Inconsistent behavior suggests: - Bot (random choices) - Shared account (multiple users) - Confused user (bad UX)

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

True if behavior is consistent, False otherwise

Return type:

bool

Example

>>> if auditor.validate_history(user_log):
...     trust_level = "high"
... else:
...     trust_level = "low"
get_integrity_score(log)[source]#

Get behavioral integrity score (0-1).

The integrity score measures how “clean” the behavioral signal is: - 1.0 = Perfect integrity, fully consistent behavior - 0.9+ = High integrity, minor noise - 0.7-0.9 = Moderate integrity, some confusion - <0.7 = Low integrity, likely bot or multiple users

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

Float between 0 (chaotic) and 1 (perfectly consistent)

Return type:

float

Example

>>> score = auditor.get_integrity_score(user_log)
>>> if score < 0.85:
...     flag_for_manual_review(user_id)
get_confusion_score(log)[source]#

Get confusion/exploitability score (0-1).

The confusion score measures how exploitable the user’s inconsistencies are. High confusion indicates: - User not understanding the options - Bad UX causing irrational choices - Possible UI dark patterns

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

Float between 0 (no confusion) and 1 (highly confused)

Return type:

float

Example

>>> confusion = auditor.get_confusion_score(user_log)
>>> if confusion > 0.15:
...     alert_ux_team("User showing high confusion")
get_consistency_details(log)[source]#

Get detailed consistency check results.

Returns the full ConsistencyResult with information about specific inconsistencies found.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

ConsistencyResult with is_consistent, violations, etc.

Return type:

ConsistencyResult

get_integrity_details(log)[source]#

Get detailed integrity score results.

Returns the full IntegrityResult with the underlying consistency check at the computed threshold.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

IntegrityResult with integrity_score, waste_fraction, etc.

Return type:

IntegrityResult

get_confusion_details(log)[source]#

Get detailed confusion metric results.

Returns the full ConfusionResult with information about the worst inconsistency cycles.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

ConfusionResult with confusion_score, worst_cycle, etc.

Return type:

ConfusionResult

full_audit(log)[source]#

Run comprehensive behavioral audit.

Computes all core metrics and returns a single report.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

AuditReport with consistency, integrity, and confusion scores

Return type:

AuditReport

Example

>>> report = auditor.full_audit(user_log)
>>> print(f"Consistent: {report.is_consistent}")
>>> print(f"Integrity: {report.integrity_score:.2f}")
>>> print(f"Confusion: {report.confusion_score:.2f}")
compute_test_power(log, n_simulations=1000)[source]#

Compute statistical power of the consistency test.

Bronars’ Power Index measures whether passing the consistency test is statistically meaningful. Low power means even random behavior would pass, making the consistency result uninformative.

Parameters:
  • log (BehaviorLog) – BehaviorLog containing user’s historical actions

  • n_simulations (int) – Number of random simulations (default: 1000)

Returns:

TestPowerResult with power_index and is_significant

Return type:

TestPowerResult

Example

>>> result = auditor.compute_test_power(user_log)
>>> if result.power_index < 0.5:
...     print("Warning: GARP test has low power for this data")
validate_proportional_scaling(log)[source]#

Test if user preferences scale proportionally with budget.

Homothetic preferences mean relative preferences don’t change with budget - useful for demand prediction and user segmentation.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

ProportionalScalingResult with is_homothetic and violations

Return type:

ProportionalScalingResult

Example

>>> result = auditor.validate_proportional_scaling(user_log)
>>> if result.is_homothetic:
...     print("User preferences scale with budget")
compute_granular_integrity(log, efficiency_threshold=0.9)[source]#

Get per-observation integrity scores.

Unlike get_integrity_score which gives one global score, this identifies which specific observations are problematic.

Parameters:
  • log (BehaviorLog) – BehaviorLog containing user’s historical actions

  • efficiency_threshold (float) – Threshold below which observations are flagged (default: 0.9)

Returns:

GranularIntegrityResult with efficiency_vector and problematic_observations

Return type:

GranularIntegrityResult

Example

>>> result = auditor.compute_granular_integrity(user_log)
>>> for idx in result.problematic_observations:
...     print(f"Investigate observation {idx}")
test_income_invariance(log)[source]#

Test if user behavior is invariant to income changes.

Quasilinear preferences have constant marginal utility of money, meaning no income effects on goods choices.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

IncomeInvarianceResult with is_quasilinear and violations

Return type:

IncomeInvarianceResult

Example

>>> result = auditor.test_income_invariance(user_log)
>>> if result.has_income_effects:
...     print("User behavior varies with income")
test_cross_price_effect(log, good_g, good_h)[source]#

Test cross-price relationship between two goods.

Determines if goods are substitutes (price of A up → demand for B up) or complements (price of A up → demand for B down).

Parameters:
  • log (BehaviorLog) – BehaviorLog containing user’s historical actions

  • good_g (int) – Index of first good

  • good_h (int) – Index of second good

Returns:

CrossPriceResult with relationship classification

Return type:

CrossPriceResult

Example

>>> result = auditor.test_cross_price_effect(user_log, 0, 1)
>>> print(f"Goods 0 and 1 are {result.relationship}")
validate_smooth_preferences(log)[source]#

Test if user preferences are smooth (differentiable).

Smooth preferences enable demand function derivatives for price sensitivity analysis. Requires both SARP (no indifferent cycles) and price-quantity uniqueness.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

SmoothPreferencesResult with differentiability status

Return type:

SmoothPreferencesResult

Example

>>> result = auditor.validate_smooth_preferences(user_log)
>>> if result.is_differentiable:
...     print("Preferences are smooth - can compute price elasticities")
validate_strict_consistency(log)[source]#

Test strict behavioral consistency (more lenient than full check).

Tests only strict preference cycles. Passes if violations are only due to weak preferences (indifference). Useful for identifying “approximately rational” behavior.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

StrictConsistencyResult with consistency status

Return type:

StrictConsistencyResult

Example

>>> result = auditor.validate_strict_consistency(user_log)
>>> if result.strict_violations_only:
...     print("GARP fails but only due to indifference")
validate_price_preferences(log)[source]#

Test if user has consistent price preferences.

The dual of consistency validation - tests if the user prefers situations where their desired items are cheaper. Useful for understanding price sensitivity.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

PricePreferencesResult with price preference consistency

Return type:

PricePreferencesResult

Example

>>> result = auditor.validate_price_preferences(user_log)
>>> if result.prefers_lower_prices:
...     print("User consistently seeks lower prices")
validate_menu_history(log)[source]#

Check if menu-based choice history is consistent (SARP).

A consistent history means choices don’t form preference cycles. Inconsistent behavior suggests irrational decision-making.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

True if behavior is SARP-consistent, False otherwise

Return type:

bool

Example

>>> if auditor.validate_menu_history(menu_log):
...     print("Menu choices are consistent")
get_menu_consistency_details(log)[source]#

Get detailed menu consistency (Congruence/rationalizability) results.

Returns the full CongruenceResult with information about SARP violations and maximality violations.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

CongruenceResult with is_rationalizable, violations, etc.

Return type:

CongruenceResult

get_menu_efficiency_score(log)[source]#

Get menu choice efficiency score (Houtman-Maks index).

The efficiency score measures what fraction of observations are consistent. 1.0 means fully consistent, lower values indicate more inconsistencies.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

Float between 0 (all inconsistent) and 1 (perfectly consistent)

Return type:

float

Example

>>> score = auditor.get_menu_efficiency_score(menu_log)
>>> if score < 0.9:
...     print("Some inconsistent choices detected")
recover_menu_preferences(log)[source]#

Recover ordinal preference ranking from menu choices.

If the data is SARP-consistent, computes a preference ranking using topological sort of the revealed preference graph.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

OrdinalUtilityResult with preference_order and utility_ranking

Return type:

OrdinalUtilityResult

Example

>>> result = auditor.recover_menu_preferences(menu_log)
>>> if result.success:
...     print(f"Preference order: {result.preference_order}")
full_menu_audit(log)[source]#

Run comprehensive menu choice audit.

Computes all menu-based consistency metrics and returns a single report.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

MenuAuditReport with WARP, SARP, efficiency, and preference results

Return type:

MenuAuditReport

Example

>>> report = auditor.full_menu_audit(menu_log)
>>> print(f"Rationalizable: {report.is_rationalizable}")
>>> print(f"Efficiency: {report.efficiency_score:.2f}")
summary(log)[source]#

Generate unified summary of all behavioral tests.

Runs GARP, WARP, SARP, AEI, MPI, and Houtman-Maks tests and combines results into a single, statsmodels-style summary object.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

BehavioralSummary with all test results

Return type:

BehavioralSummary

Example

>>> auditor = BehavioralAuditor()
>>> summary = auditor.summary(user_log)
>>> print(summary.summary())  # Print formatted report
>>> summary.score()  # Get aggregate score
menu_summary(log)[source]#

Generate unified summary of menu-based choice tests.

Runs WARP, SARP, Congruence, and efficiency tests for menu choice data.

Parameters:

log (MenuChoiceLog) – MenuChoiceLog containing menu choices

Returns:

MenuChoiceSummary with all test results

Return type:

MenuChoiceSummary

Example

>>> auditor = BehavioralAuditor()
>>> summary = auditor.menu_summary(menu_log)
>>> print(summary.summary())

AuditReport#

class prefgraph.AuditReport(is_consistent, integrity_score, confusion_score)[source]#

Bases: object

Comprehensive audit report for user behavior.

Variables:
  • is_consistent (bool) – True if behavior passes GARP consistency check

  • integrity_score (float) – Afriat Efficiency Index (0-1, higher = more consistent)

  • confusion_score (float) – Money Pump Index (0-1, higher = more exploitable)

Parameters:
  • is_consistent (bool)

  • integrity_score (float)

  • confusion_score (float)

is_consistent: bool#
integrity_score: float#
confusion_score: float#
summary()[source]#

Return human-readable audit summary report.

Return type:

str

score()[source]#

Return scikit-learn style score in [0, 1]. Higher is better.

Returns the average of integrity score and (1 - confusion score).

Return type:

float

__init__(is_consistent, integrity_score, confusion_score)#
Parameters:
  • is_consistent (bool)

  • integrity_score (float)

  • confusion_score (float)

Return type:

None

PreferenceEncoder#

class prefgraph.PreferenceEncoder(precision=1e-08)[source]#

Bases: object

Encodes user preferences into latent value representations.

PreferenceEncoder follows the scikit-learn pattern: fit() to learn from data, then extract features or make predictions.

The encoder solves an optimization problem to find latent values that explain the user’s observed choices. These values can be used as: - Features for downstream ML models - User embeddings for similarity calculations - Inputs to counterfactual simulations

Example

>>> from prefgraph import PreferenceEncoder, BehaviorLog
>>> import numpy as np
>>> # Create behavior log
>>> log = BehaviorLog(
...     cost_vectors=np.array([[1.0, 2.0], [2.0, 1.0], [1.5, 1.5]]),
...     action_vectors=np.array([[3.0, 1.0], [1.0, 3.0], [2.0, 2.0]]),
... )
>>> # Fit encoder
>>> encoder = PreferenceEncoder()
>>> encoder.fit(log)
>>> # Extract latent values as features
>>> features = encoder.extract_latent_values()
>>> print(f"Latent values: {features}")
>>> # Build value function for counterfactuals
>>> value_fn = encoder.get_value_function()
>>> print(f"Value of [2, 2]: {value_fn(np.array([2.0, 2.0]))}")
Variables:

precision – Numerical precision for optimization (default: 1e-8)

Parameters:

precision (float)

__init__(precision=1e-08)[source]#

Initialize the encoder.

Parameters:

precision (float) – Numerical precision for the LP solver.

Return type:

None

fit(log)[source]#

Fit the encoder to a behavior log.

Solves an optimization problem to find latent preference values that explain the user’s observed choices.

Parameters:

log (BehaviorLog) – BehaviorLog containing user’s historical actions

Returns:

self (for method chaining)

Raises:

ValueError – If the behavior is too inconsistent to fit

Return type:

PreferenceEncoder

Example

>>> encoder = PreferenceEncoder().fit(user_log)
property is_fitted: bool#

Check if the encoder has been successfully fitted.

extract_latent_values()[source]#

Extract latent preference values.

Returns an array of latent values, one per observation in the fitted log. These can be used as features for ML models.

Returns:

Array of latent values (T observations)

Raises:

ValueError – If not fitted

Return type:

ndarray[tuple[Any, …], dtype[float64]]

Example

>>> encoder.fit(user_log)
>>> features = encoder.extract_latent_values()
>>> # Use as features in an ML model
>>> X_train = np.column_stack([other_features, features])
extract_marginal_weights()[source]#

Extract marginal weights (sensitivity to costs).

Returns an array of marginal weights representing how sensitive the user’s preferences are to cost changes at each observation.

Returns:

Array of marginal weights (T observations)

Raises:

ValueError – If not fitted

Return type:

ndarray[tuple[Any, …], dtype[float64]]

get_value_function()[source]#

Get a callable value function.

Returns a function that estimates the latent value of any action vector. Useful for counterfactual analysis.

Returns:

Callable that takes an action vector and returns its value

Raises:

ValueError – If not fitted

Return type:

Callable[[ndarray[tuple[Any, …], dtype[_ScalarT]]], float]

Example

>>> encoder.fit(user_log)
>>> value_fn = encoder.get_value_function()
>>> # Estimate value of a hypothetical action
>>> value = value_fn(np.array([5.0, 3.0]))
predict_choice(cost_vector, resource_limit)[source]#

Predict what action the user would take under new conditions.

Given a new cost vector and resource limit (budget), predicts what action vector the user would choose to maximize their latent preference value.

Parameters:
  • cost_vector (ndarray[tuple[Any, ...], dtype[float64]]) – Array of costs for each action dimension

  • resource_limit (float) – Total budget/resource constraint

Returns:

Predicted action vector, or None if prediction failed

Raises:

ValueError – If not fitted

Return type:

ndarray[tuple[Any, …], dtype[float64]] | None

Example

>>> encoder.fit(user_log)
>>> # What would user do with new prices and $100 budget?
>>> new_costs = np.array([1.5, 2.5])
>>> predicted_action = encoder.predict_choice(new_costs, 100.0)
get_fit_details()[source]#

Get detailed results from the fitting process.

Returns the full LatentValueResult with solver status, residuals, and other diagnostic information.

Returns:

LatentValueResult with full details

Raises:

ValueError – If not fitted

Return type:

LatentValueResult

property solver_status: str#

Get the solver status message.

property mean_marginal_weight: float | None#

Get the mean marginal weight across observations.

transform(logs)[source]#

Transform behavior logs to feature array.

For each log, extracts latent values as a feature vector. Each log is fitted independently and its latent values extracted.

Parameters:

logs (list[BehaviorLog] | BehaviorLog) – Single BehaviorLog or list of BehaviorLogs

Returns:

Feature array of shape (n_logs, n_features) where n_features is the number of observations in each log. If logs have different numbers of observations, they are padded with NaN.

Return type:

NDArray[np.float64]

Example

>>> encoder = PreferenceEncoder()
>>> features = encoder.transform([log1, log2, log3])
>>> # Use features in an ML model
>>> model.fit(features, labels)
fit_transform(logs)[source]#

Fit encoder and transform logs in one call.

This is the standard sklearn-style interface. For a single log, fits the encoder to that log and returns its latent values. For multiple logs, fits to the first log and transforms all.

Parameters:

logs (list[BehaviorLog] | BehaviorLog) – Single BehaviorLog or list of BehaviorLogs

Returns:

Feature array of shape (n_logs, n_features)

Return type:

NDArray[np.float64]

Example

>>> encoder = PreferenceEncoder()
>>> features = encoder.fit_transform([log1, log2])
>>> print(f"Feature shape: {features.shape}")

Summary Classes#

BehavioralSummary#

class prefgraph.BehavioralSummary(garp_result, warp_result, sarp_result, aei_result, mpi_result, houtman_maks_result, optimal_efficiency_result, num_observations, num_goods, computation_time_ms, price_stats=None, quantity_stats=None, expenditure_stats=None, r_density=None, p_density=None, r_star_density=None, violation_pair_count=None, user_id=None)[source]#

Bases: ResultDisplayMixin

Unified summary of all behavioral tests (statsmodels-style).

Provides a comprehensive overview of behavioral consistency analysis, combining multiple tests and metrics in a single, professional output.

Variables:
  • garp_result (GARPResult) – GARP consistency test result

  • warp_result (WARPResult | None) – WARP consistency test result (optional)

  • sarp_result (SARPResult | None) – SARP consistency test result (optional)

  • aei_result (AEIResult) – Afriat Efficiency Index result

  • mpi_result (MPIResult) – Money Pump Index result

  • houtman_maks_result (HoutmanMaksResult | None) – Houtman-Maks efficiency result (optional)

  • num_observations (int) – Number of observations in the dataset

  • num_goods (int) – Number of goods/dimensions

  • computation_time_ms (float) – Total computation time in milliseconds

Parameters:
  • garp_result (GARPResult)

  • warp_result (WARPResult | None)

  • sarp_result (SARPResult | None)

  • aei_result (AEIResult)

  • mpi_result (MPIResult)

  • houtman_maks_result (HoutmanMaksResult | None)

  • optimal_efficiency_result (OptimalEfficiencyResult | None)

  • num_observations (int)

  • num_goods (int)

  • computation_time_ms (float)

  • price_stats (dict[str, float] | None)

  • quantity_stats (dict[str, float] | None)

  • expenditure_stats (dict[str, float] | None)

  • r_density (float | None)

  • p_density (float | None)

  • r_star_density (float | None)

  • violation_pair_count (int | None)

  • user_id (str | None)

Example

>>> from prefgraph import BehaviorLog, BehavioralSummary
>>> log = BehaviorLog(prices, quantities)
>>> summary = BehavioralSummary.from_log(log)
>>> print(summary.summary())
garp_result: GARPResult#
warp_result: WARPResult | None#
sarp_result: SARPResult | None#
aei_result: AEIResult#
mpi_result: MPIResult#
houtman_maks_result: HoutmanMaksResult | None#
optimal_efficiency_result: OptimalEfficiencyResult | None#
num_observations: int#
num_goods: int#
computation_time_ms: float#
price_stats: dict[str, float] | None = None#
quantity_stats: dict[str, float] | None = None#
expenditure_stats: dict[str, float] | None = None#
r_density: float | None = None#
p_density: float | None = None#
r_star_density: float | None = None#
violation_pair_count: int | None = None#
user_id: str | None = None#
property is_consistent: bool#

True if data passes GARP consistency test.

property efficiency_index: float#

Afriat Efficiency Index (AEI) score.

property mpi_value: float#

Money Pump Index value.

score()[source]#

Return aggregate scikit-learn style score in [0, 1].

Combines AEI and (1 - MPI) with equal weighting.

Return type:

float

summary()[source]#

Return formatted summary table (statsmodels-style).

Returns a professional text summary including: - Two-column header with key results - Input data statistics (prices, quantities, expenditure) - Revealed preference graph density - Consistency test results with [+]/[-] indicators - Goodness-of-fit metrics with sub-details - Interpretation guidance

Returns:

Multi-line formatted string suitable for printing.

Return type:

str

to_dict()[source]#

Return dictionary representation for serialization.

Return type:

dict[str, Any]

short_summary()[source]#

Return one-liner summary.

Return type:

str

classmethod from_log(log, include_warp=True, include_sarp=True, include_power=False)[source]#

Create BehavioralSummary by running all tests on a BehaviorLog.

This factory method runs GARP, AEI, MPI, and optionally WARP, SARP, Houtman-Maks, and power analysis tests, combining results into a unified summary.

Parameters:
  • log (BehaviorLog) – BehaviorLog containing the behavioral data

  • include_warp (bool) – Whether to include WARP test (default: True)

  • include_sarp (bool) – Whether to include SARP test (default: True)

  • include_power (bool) – Whether to include power analysis (default: False)

Returns:

BehavioralSummary instance with all test results

Return type:

BehavioralSummary

Example

>>> summary = BehavioralSummary.from_log(behavior_log)
>>> print(summary)
>>> # With power analysis
>>> summary = BehavioralSummary.from_log(behavior_log, include_power=True)
>>> print(summary)
__init__(garp_result, warp_result, sarp_result, aei_result, mpi_result, houtman_maks_result, optimal_efficiency_result, num_observations, num_goods, computation_time_ms, price_stats=None, quantity_stats=None, expenditure_stats=None, r_density=None, p_density=None, r_star_density=None, violation_pair_count=None, user_id=None)#
Parameters:
  • garp_result (GARPResult)

  • warp_result (WARPResult | None)

  • sarp_result (SARPResult | None)

  • aei_result (AEIResult)

  • mpi_result (MPIResult)

  • houtman_maks_result (HoutmanMaksResult | None)

  • optimal_efficiency_result (OptimalEfficiencyResult | None)

  • num_observations (int)

  • num_goods (int)

  • computation_time_ms (float)

  • price_stats (dict[str, float] | None)

  • quantity_stats (dict[str, float] | None)

  • expenditure_stats (dict[str, float] | None)

  • r_density (float | None)

  • p_density (float | None)

  • r_star_density (float | None)

  • violation_pair_count (int | None)

  • user_id (str | None)

Return type:

None

PanelSummary#

class prefgraph.PanelSummary(user_summaries, num_users, total_observations, num_goods, obs_per_user_stats, garp_pass_rate, warp_pass_rate, sarp_pass_rate, aei_distribution, mpi_distribution, hm_distribution, top_inconsistent, computation_time_ms, period_stats=None, num_periods=0)[source]#

Bases: ResultDisplayMixin

Aggregate summary for multi-user panel analysis.

Combines per-user BehavioralSummary results into aggregate statistics: consistency rates, efficiency distributions, and identification of the most inconsistent users.

Example

>>> from prefgraph import BehaviorPanel
>>> panel = BehaviorPanel.from_logs(logs)
>>> ps = panel.summary()
>>> print(ps)
Parameters:
user_summaries: dict[str, BehavioralSummary]#
num_users: int#
total_observations: int#
num_goods: int#
obs_per_user_stats: dict[str, float]#
garp_pass_rate: float#
warp_pass_rate: float | None#
sarp_pass_rate: float | None#
aei_distribution: dict[str, float]#
mpi_distribution: dict[str, float]#
hm_distribution: dict[str, float] | None#
top_inconsistent: list[tuple[str, float, float, int]]#
computation_time_ms: float#
period_stats: list[dict[str, Any]] | None = None#
num_periods: int = 0#
classmethod from_summaries(user_summaries, period_map=None)[source]#

Build PanelSummary from per-user BehavioralSummary results.

Parameters:
Return type:

PanelSummary

summary()[source]#

Return formatted panel summary (statsmodels-style).

Return type:

str

score()[source]#

Aggregate score: mean AEI across all users.

Return type:

float

to_dict()[source]#

Return dictionary representation for serialization.

Return type:

dict[str, Any]

__init__(user_summaries, num_users, total_observations, num_goods, obs_per_user_stats, garp_pass_rate, warp_pass_rate, sarp_pass_rate, aei_distribution, mpi_distribution, hm_distribution, top_inconsistent, computation_time_ms, period_stats=None, num_periods=0)#
Parameters:
Return type:

None

Data Containers#

BehaviorLog#

class prefgraph.BehaviorLog(cost_vectors=None, action_vectors=None, user_id=None, metadata=<factory>, nan_policy='raise', prices=None, quantities=None, session_id=None, _expenditure_matrix=None)[source]#

Bases: object

Represents user behavior history as cost/action pairs over T observations.

The fundamental unit of analysis for behavioral consistency validation. Each row represents one observation (transaction, time period, etc.) where the user faced specific costs and took specific actions.

This is the tech-friendly name for what economists call “ConsumerSession”.

Variables:
  • cost_vectors (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]] | None) – T x N matrix of costs (e.g., prices) per observation. Can also be passed as prices for backward compatibility.

  • action_vectors (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]] | None) – T x N matrix of actions (e.g., quantities) per observation. Can also be passed as quantities for backward compatibility.

  • user_id (str | None) – Optional identifier for the user/session. Can also be passed as session_id for backward compatibility.

  • metadata (dict[str, Any]) – Optional dictionary for additional attributes.

Parameters:
Properties:

spend_matrix: Pre-computed T x T matrix where S[i,j] = cost_i @ action_j total_spend: Diagonal of spend matrix (actual spend at each observation) num_records: Number of observations T num_features: Number of goods/actions N

Example

>>> import numpy as np
>>> # User faced different prices and bought different quantities
>>> log = BehaviorLog(
...     cost_vectors=np.array([[1.0, 2.0], [2.0, 1.0]]),
...     action_vectors=np.array([[3.0, 1.0], [1.0, 3.0]]),
...     user_id="user_123"
... )
>>> log.num_records
2
>>> # Backward compatible with old parameter names
>>> log = BehaviorLog(prices=prices_array, quantities=quantities_array)
cost_vectors: ndarray[tuple[Any, ...], dtype[float64]] | None = None#
action_vectors: ndarray[tuple[Any, ...], dtype[float64]] | None = None#
user_id: str | None = None#
metadata: dict[str, Any]#
nan_policy: Literal['raise', 'warn', 'drop'] = 'raise'#
prices: ndarray[tuple[Any, ...], dtype[float64]] | None = None#
quantities: ndarray[tuple[Any, ...], dtype[float64]] | None = None#
session_id: str | None = None#
property spend_matrix: ndarray[tuple[Any, ...], dtype[float64]]#

T x T matrix where S[i,j] = cost to take action j at costs i.

This matrix is fundamental to behavioral consistency analysis:

  • If S[i,i] >= S[i,j], then action i is revealed preferred to action j at costs i (action j was affordable but not chosen).

property total_spend: ndarray[tuple[Any, ...], dtype[float64]]#

Actual spend at each observation (diagonal of spend matrix).

total_spend[i] = cost_i @ action_i = total cost at observation i.

property num_records: int#

Number of observations/records T.

property num_features: int#

Number of features/goods/actions N.

to_engine_tuple()[source]#

Convert to (prices, quantities) tuple for Engine.analyze_arrays().

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]]]

property expenditure_matrix: ndarray[tuple[Any, ...], dtype[float64]]#

Alias for spend_matrix (deprecated, use spend_matrix).

property own_expenditures: ndarray[tuple[Any, ...], dtype[float64]]#

Alias for total_spend (deprecated, use total_spend).

property num_observations: int#

Alias for num_records (deprecated, use num_records).

property num_goods: int#

Alias for num_features (deprecated, use num_features).

classmethod from_dataframe(df, cost_cols=None, action_cols=None, user_id=None, price_cols=None, quantity_cols=None, session_id=None)[source]#

Create BehaviorLog from pandas DataFrame.

Parameters:
  • df (Any) – DataFrame with cost and action columns

  • cost_cols (list[str] | None) – Column names for costs (or use price_cols)

  • action_cols (list[str] | None) – Column names for actions (or use quantity_cols)

  • user_id (str | None) – Optional user identifier (or use session_id)

  • price_cols (list[str] | None)

  • quantity_cols (list[str] | None)

  • session_id (str | None)

Returns:

BehaviorLog instance

Return type:

BehaviorLog

Example

>>> import pandas as pd
>>> df = pd.DataFrame({
...     'cost_A': [1.0, 2.0], 'cost_B': [2.0, 1.0],
...     'action_A': [3.0, 1.0], 'action_B': [1.0, 3.0]
... })
>>> log = BehaviorLog.from_dataframe(
...     df, cost_cols=['cost_A', 'cost_B'],
...     action_cols=['action_A', 'action_B']
... )
classmethod from_long_format(df, time_col='time', item_col='item_id', cost_col=None, action_col=None, user_id=None, price_col=None, qty_col=None, session_id=None)[source]#

Create BehaviorLog from long-format transaction logs.

Pivots SQL-style transaction data (one row per item per time) into wide-format matrices (one row per observation).

Parameters:
  • df (Any) – Long-format DataFrame with one row per item per time

  • time_col (str) – Column name for time/observation identifier

  • item_col (str) – Column name for item/product identifier

  • cost_col (str | None) – Column name for cost (or use price_col)

  • action_col (str | None) – Column name for action (or use qty_col)

  • user_id (str | None) – Optional user identifier (or use session_id)

  • price_col (str | None)

  • qty_col (str | None)

  • session_id (str | None)

Returns:

BehaviorLog instance

Return type:

BehaviorLog

split_by_window(window_size)[source]#

Split log into non-overlapping windows.

Useful for detecting structural breaks or analyzing consistency over different time periods.

Parameters:

window_size (int) – Number of observations per window

Returns:

List of BehaviorLog instances, one per window

Return type:

list[BehaviorLog]

summary(include_power=False)[source]#

Run comprehensive analysis and return unified summary.

This provides a Stata/statsmodels-style summary of all behavioral consistency tests and goodness-of-fit metrics.

Parameters:

include_power (bool) – Whether to include power analysis (default: False). Power analysis computes Bronars power and optimal efficiency via Monte Carlo simulation, which is computationally expensive.

Returns:

BehavioralSummary with GARP, WARP, SARP, AEI, MPI results

Return type:

BehavioralSummary

Example

>>> log = BehaviorLog(prices, quantities)
>>> print(log.summary())  # Standard report
>>> print(log.summary(include_power=True))  # With power analysis
__init__(cost_vectors=None, action_vectors=None, user_id=None, metadata=<factory>, nan_policy='raise', prices=None, quantities=None, session_id=None, _expenditure_matrix=None)#
Parameters:
Return type:

None

BehaviorPanel#

class prefgraph.BehaviorPanel(_logs, metadata=<factory>, _period_map=None)[source]#

Bases: object

Multi-user panel of BehaviorLog objects.

Holds a collection of BehaviorLog objects indexed by user_id, supporting iteration, filtering, and aggregate analysis.

Variables:
  • _logs (dict[str, 'BehaviorLog']) – Internal dict mapping user_id -> BehaviorLog

  • metadata (dict[str, Any]) – Optional metadata for the panel (e.g. dataset name)

Parameters:

Example

>>> import numpy as np
>>> from prefgraph import BehaviorLog, BehaviorPanel
>>> logs = [
...     BehaviorLog(np.random.rand(20,5), np.random.rand(20,5), user_id=f"u{i}")
...     for i in range(10)
... ]
>>> panel = BehaviorPanel.from_logs(logs)
>>> print(f"{panel.num_users} users, {sum(l.num_observations for _, l in panel)} obs")
>>> print(panel.summary())
metadata: dict[str, Any]#
classmethod from_logs(logs)[source]#

Create panel from a list of BehaviorLog objects.

Uses each log’s user_id as the key. Logs without user_id get auto-assigned “user_0”, “user_1”, etc.

Parameters:

logs (list['BehaviorLog'])

Return type:

BehaviorPanel

classmethod from_dict(logs)[source]#

Create panel from a dict of user_id -> BehaviorLog.

Parameters:

logs (dict[str, 'BehaviorLog'])

Return type:

BehaviorPanel

classmethod from_dataframe(df, user_col, cost_cols=None, action_cols=None, price_cols=None, qty_cols=None, period_col=None)[source]#

Create panel from a pandas DataFrame.

Groups by user_col (and optionally period_col) and creates one BehaviorLog per group.

Parameters:
  • df (Any) – pandas DataFrame

  • user_col (str) – Column name for user/household IDs

  • cost_cols (list[str] | None) – Column names for cost/price vectors

  • action_cols (list[str] | None) – Column names for action/quantity vectors

  • price_cols (list[str] | None) – Alias for cost_cols (backward compat)

  • qty_cols (list[str] | None) – Alias for action_cols (backward compat)

  • period_col (str | None) – Optional column to group by time period. If provided, user_ids become “user_id__period”.

Returns:

BehaviorPanel with one BehaviorLog per user (or user-period).

Return type:

BehaviorPanel

property user_ids: list[str]#

List of user IDs in the panel.

property num_users: int#

Number of unique users in the panel.

property has_periods: bool#

True if this panel has period structure.

property periods: list[str]#

List of unique periods (empty if no period structure).

property num_entries: int#

Number of entries (user-period combinations or just users).

get_period(period)[source]#

Return panel filtered to a single period.

Parameters:

period (str)

Return type:

BehaviorPanel

analyze_user(user_id)[source]#

Run full behavioral analysis on a single user.

Parameters:

user_id (str)

Return type:

BehavioralSummary

summary(include_warp=True, include_sarp=True, include_power=False)[source]#

Run analysis on all users and return aggregate PanelSummary.

Parameters:
  • include_warp (bool)

  • include_sarp (bool)

  • include_power (bool)

Return type:

PanelSummary

filter(predicate)[source]#

Return a new panel containing only logs that satisfy predicate.

Parameters:

predicate (Callable[['BehaviorLog'], bool])

Return type:

BehaviorPanel

to_engine_tuples()[source]#

Convert all logs to Engine-compatible format.

Returns:

List of (prices, quantities) tuples for Engine.analyze_arrays().

Return type:

list[tuple[ndarray, ndarray]]

Example

>>> engine = Engine(metrics=["garp", "ccei"])
>>> results = engine.analyze_arrays(panel.to_engine_tuples())
__init__(_logs, metadata=<factory>, _period_map=None)#
Parameters:
Return type:

None

RiskChoiceLog#

class prefgraph.RiskChoiceLog(safe_values, risky_outcomes, risky_probabilities, choices, session_id=None, metadata=<factory>)[source]#

Bases: object

Represents choice data between safe and risky options under uncertainty.

Used for revealed preference analysis of risk attitudes. Each observation presents the decision-maker with a safe option (certain payoff) and a risky option (lottery with multiple possible outcomes).

Variables:
  • safe_values (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T-length array of certain payoff values for the safe option

  • risky_outcomes (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x K matrix of possible outcomes for the risky option (K = max number of outcomes per lottery)

  • risky_probabilities (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x K matrix of objective probabilities for outcomes (must sum to 1 for each row)

  • choices (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.bool]]) – T-length boolean array; True = chose risky, False = chose safe

  • session_id (str | None) – Optional identifier for the session/decision-maker

  • metadata (dict[str, Any]) – Optional dictionary for additional attributes

Parameters:

Example

>>> import numpy as np
>>> # Two choices: risky lottery vs certain amount
>>> safe = np.array([50.0, 100.0])
>>> outcomes = np.array([[100.0, 0.0], [200.0, 0.0]])
>>> probs = np.array([[0.5, 0.5], [0.5, 0.5]])
>>> choices = np.array([True, False])  # Chose risky then safe
>>> session = RiskSession(safe, outcomes, probs, choices)
safe_values: ndarray[tuple[Any, ...], dtype[float64]]#
risky_outcomes: ndarray[tuple[Any, ...], dtype[float64]]#
risky_probabilities: ndarray[tuple[Any, ...], dtype[float64]]#
choices: ndarray[tuple[Any, ...], dtype[bool]]#
session_id: str | None = None#
metadata: dict[str, Any]#
property num_observations: int#

Number of choice observations T.

property num_outcomes: int#

Maximum number of outcomes per lottery K.

property expected_values: ndarray[tuple[Any, ...], dtype[float64]]#

Expected value of each risky lottery.

property risk_neutral_choices: ndarray[tuple[Any, ...], dtype[bool]]#

What a risk-neutral agent would choose (True if EV > safe).

property num_risk_seeking_choices: int#

Count of choices where risky was chosen despite lower EV.

property num_risk_averse_choices: int#

Count of choices where safe was chosen despite lower EV.

summary()[source]#

Run comprehensive analysis and return unified summary.

This provides a Stata/statsmodels-style summary of risk profile analysis and Expected Utility axiom tests.

Returns:

RiskChoiceSummary with risk profile and EU axiom results

Return type:

RiskChoiceSummary

Example

>>> log = RiskChoiceLog(safe_values, risky_outcomes, risky_probs, choices)
>>> print(log.summary())  # Full report
__init__(safe_values, risky_outcomes, risky_probabilities, choices, session_id=None, metadata=<factory>)#
Parameters:
Return type:

None

EmbeddingChoiceLog#

prefgraph.EmbeddingChoiceLog#

alias of SpatialSession

Consistency Functions#

prefgraph.validate_consistency(session, tolerance=1e-10)#

Validate that user behavior is internally consistent.

This is the tech-friendly alias for check_garp (GARP = Generalized Axiom of Revealed Preference). Consistent behavior indicates: - Single user (not a shared account) - Not a bot (bots make random inconsistent choices) - Not confused by the UI

Returns a ConsistencyResult with: - is_consistent: True if behavior is consistent - violations: List of detected violation cycles

Example

>>> from prefgraph import BehaviorLog, validate_consistency
>>> result = validate_consistency(user_log)
>>> if not result.is_consistent:
...     print(f"Found {result.num_violations} violations")
Parameters:
Return type:

GARPResult

prefgraph.validate_consistency_weak(session, tolerance=1e-10)#

Weak consistency check (only checks direct contradictions).

Faster than full validate_consistency but may miss transitive inconsistencies.

Parameters:
Return type:

WARPResult

prefgraph.validate_sarp(session, tolerance=1e-10)#

Validate SARP (no indifferent preference cycles).

Parameters:
Return type:

SARPResult

prefgraph.validate_smooth_preferences(session, tolerance=1e-10)#

Validate that user preferences are smooth (differentiable).

This is the tech-friendly alias for check_differentiable. Smooth preferences enable demand function derivatives for price sensitivity analysis.

Parameters:
Return type:

DifferentiableResult

prefgraph.validate_strict_consistency(session, tolerance=1e-10)#

Validate strict behavioral consistency only.

This is the tech-friendly alias for check_acyclical_p. More lenient than full consistency validation - passes if only weak violations exist.

Parameters:
Return type:

AcyclicalPResult

prefgraph.validate_price_preferences(session, tolerance=1e-10)#

Validate that user has consistent price preferences.

This is the tech-friendly alias for check_gapp. Tests if the user consistently prefers situations where their desired items are cheaper.

Parameters:
Return type:

GAPPResult

Efficiency Functions#

prefgraph.compute_integrity_score(session, tolerance=1e-06, max_iterations=50, method='discrete')#

Compute the behavioral integrity score (0-1).

This is the tech-friendly alias for compute_aei (Afriat Efficiency Index).

The integrity score measures consistency with utility maximization: - 1.0 = Perfectly consistent behavior - 0.9+ = Minor deviations from rationality - 0.7-0.9 = Moderate inconsistencies - <0.7 = Notable violations of rationality

Example

>>> from prefgraph import BehaviorLog, compute_integrity_score
>>> result = compute_integrity_score(user_log)
>>> print(f"Integrity: {result.efficiency_index:.2f}")
Returns:

IntegrityResult with efficiency_index in [0, 1]

Parameters:
Return type:

AEIResult

prefgraph.compute_confusion_metric(session, tolerance=1e-10, method='cycles')#

Compute the confusion metric (how exploitable the user’s decisions are).

This is the tech-friendly alias for compute_mpi (Money Pump Index).

The confusion metric measures how much value could be extracted from a user making inconsistent decisions via preference cycling.

Example

>>> from prefgraph import BehaviorLog, compute_confusion_metric
>>> result = compute_confusion_metric(user_log)
>>> if result.confusion_score > 0.15:
...     alert_ux_team(user_id)
Returns:

ConfusionResult with confusion_score in [0, 1]

Parameters:
Return type:

MPIResult

prefgraph.compute_minimal_outlier_fraction(session, tolerance=1e-10, method='auto')#

Compute the minimal fraction of observations to remove to achieve consistency.

Tech-friendly alias for compute_houtman_maks_index.

Returns the smallest fraction of user sessions that must be removed to make the remaining behavior fully consistent. Useful for identifying which specific transactions are problematic.

Parameters:
Return type:

HoutmanMaksResult

prefgraph.compute_granular_integrity(session, tolerance=1e-08, efficiency_threshold=0.9)#

Compute integrity scores for each individual observation.

This is the tech-friendly alias for compute_vei (Varian’s Efficiency Index).

Unlike compute_integrity_score which gives one global score, this identifies which specific observations are problematic.

Use this to: - Find specific transactions to investigate - Identify when user behavior changed - Detect specific sessions with issues

Example

>>> from prefgraph import BehaviorLog, compute_granular_integrity
>>> result = compute_granular_integrity(user_log)
>>> for obs_idx in result.problematic_observations:
...     print(f"Investigate observation {obs_idx}")
Parameters:
Return type:

VEIResult

prefgraph.compute_test_power(session, n_simulations=1000, tolerance=1e-10, random_seed=None, store_simulation_values=True)#

Compute the statistical power of the consistency test.

This is the tech-friendly alias for compute_bronars_power.

The test power measures whether a passed consistency test is meaningful: - Power > 0.7: High discriminatory power, passing GARP is significant - Power 0.5-0.7: Moderate power, interpret with caution - Power < 0.5: Low power, even random behavior would pass

Use this for: - Validating that consistency scores are meaningful - Determining if more observations are needed - Assessing quality of price variation in data

Example

>>> from prefgraph import BehaviorLog, compute_test_power
>>> result = compute_test_power(user_log, n_simulations=500)
>>> if result.power_index < 0.5:
...     print("Warning: GARP test has low discriminatory power")
Parameters:
Return type:

BronarsPowerResult

Preference Structure Functions#

prefgraph.validate_proportional_scaling(session, tolerance=1e-10)#

Validate that user preferences scale proportionally with budget.

This is the tech-friendly alias for check_harp (HARP = Homothetic Axiom of Revealed Preference).

Proportional preferences mean the user’s relative preferences don’t change with their budget - they just scale up proportionally. This is useful for: - User segmentation (different budget levels have same relative preferences) - Demand prediction (can extrapolate to different spending levels) - Aggregating users across income levels

Example

>>> from prefgraph import BehaviorLog, validate_proportional_scaling
>>> result = validate_proportional_scaling(user_log)
>>> if result.is_consistent:
...     print("User has proportional preferences")
Parameters:
Return type:

HARPResult

prefgraph.test_income_invariance(session, tolerance=1e-10, max_cycle_length=3)#

Test if user behavior is invariant to income/budget changes.

This is the tech-friendly alias for check_quasilinearity.

Income-invariant behavior means the user’s preferences for goods don’t change with their total budget - only relative prices matter. This is useful for: - Demand modeling (simpler demand functions) - Welfare analysis (constant marginal utility of money) - Price optimization (no need to account for income effects)

Example

>>> from prefgraph import BehaviorLog, test_income_invariance
>>> result = test_income_invariance(user_log)
>>> if result.is_quasilinear:
...     print("User has constant marginal utility of money")
>>> else:
...     print(f"Income effects detected in {len(result.violations)} cycles")
Parameters:
Return type:

QuasilinearityResult

prefgraph.test_feature_independence(session, group_a, group_b, tolerance=1e-06)#

Test if two feature groups are independent (can be optimized separately).

This is the tech-friendly alias for check_separability.

Use this to determine if product categories can be priced/optimized independently without considering cross-effects.

Example

>>> from prefgraph import BehaviorLog, test_feature_independence
>>> # Test if Rides and Eats are independent for a superapp user
>>> result = test_feature_independence(user_log, group_a=[0, 1], group_b=[2, 3])
>>> if result.is_separable:
...     print("Can price independently")
Returns:

FeatureIndependenceResult with is_separable and cross_effect_strength

Parameters:
Return type:

SeparabilityResult

prefgraph.test_cross_price_effect(session, good_g, good_h, price_change_threshold=0.05, tolerance=1e-10)#

Test how changes in one item’s price affect demand for another item.

This is the tech-friendly alias for check_gross_substitutes.

Use this to understand cross-price relationships between products: - Substitutes: Price of A up → Demand for B up (users switch) - Complements: Price of A up → Demand for B down (bought together) - Independent: No clear relationship

Example

>>> from prefgraph import BehaviorLog, test_cross_price_effect
>>> result = test_cross_price_effect(user_log, good_g=0, good_h=1)
>>> if result.are_substitutes:
...     print("Users treat these as substitutes")
Parameters:
Return type:

GrossSubstitutesResult

prefgraph.compute_cross_price_matrix(session, price_change_threshold=0.05)#

Compute all pairwise cross-price relationships.

Returns an N x N matrix of relationships between all goods.

Parameters:
Return type:

SubstitutionMatrixResult

Utility Recovery#

prefgraph.fit_latent_values(session, tolerance=1e-08)#

Fit latent preference values from user behavior.

This is the tech-friendly alias for recover_utility.

Extracts latent preference values that can be used as: - Features for ML models - Inputs to counterfactual simulations - User embeddings for personalization

Example

>>> from prefgraph import BehaviorLog, fit_latent_values
>>> result = fit_latent_values(user_log)
>>> if result.converged:
...     features = result.latent_values  # Use as ML features
Returns:

LatentValueResult with latent_values array

Parameters:
Return type:

UtilityRecoveryResult

prefgraph.build_value_function(session, utility_result)#

Build a callable value function from fitted latent values.

This is the tech-friendly alias for construct_afriat_utility.

Returns a function that estimates the value/utility of any action vector.

Example

>>> from prefgraph import BehaviorLog, fit_latent_values, build_value_function
>>> result = fit_latent_values(user_log)
>>> value_fn = build_value_function(user_log, result)
>>> estimated_value = value_fn(new_action_vector)
Parameters:
  • session (BehaviorLog)

  • utility_result (UtilityRecoveryResult)

Return type:

Callable[[ndarray[tuple[Any, …], dtype[float64]]], float]

prefgraph.predict_choice(session, utility_result, new_prices, budget, n_goods=None)#

Predict what action a user will take given new costs and a resource limit.

This is the tech-friendly alias for predict_demand.

Uses the fitted latent value model to predict user behavior under new conditions (counterfactual analysis).

Example

>>> from prefgraph import BehaviorLog, fit_latent_values, predict_choice
>>> result = fit_latent_values(user_log)
>>> predicted_action = predict_choice(user_log, result, new_costs, budget)
Parameters:
Return type:

ndarray[tuple[Any, …], dtype[float64]] | None

Embedding Analysis#

prefgraph.find_preference_anchor(session, method='SLSQP', max_iterations=1000)#

Find the user’s preference anchor (ideal point) in embedding space.

This is the tech-friendly alias for find_ideal_point.

The preference anchor is the location in feature space that the user seems to prefer. Items closer to this anchor are more likely to be chosen.

Use this for: - Recommendation explainability (“You prefer items near this anchor”) - Personalization (recommend items close to anchor) - Understanding user preference structure

Example

>>> from prefgraph import EmbeddingChoiceLog, find_preference_anchor
>>> result = find_preference_anchor(user_choices)
>>> print(f"User's anchor: {result.ideal_point}")
Returns:

PreferenceAnchorResult with ideal_point and explained_variance

Parameters:
  • session (SpatialSession)

  • method (str)

  • max_iterations (int)

Return type:

IdealPointResult

prefgraph.validate_embedding_consistency(session)#

Check if user choices are consistent in embedding space.

This is the tech-friendly alias for check_euclidean_rationality.

Verifies that the user’s choices can be explained by a single preference anchor. Inconsistency suggests multiple users or erratic behavior.

Parameters:

session (SpatialSession)

Return type:

tuple[bool, list[tuple[int, int]]]

prefgraph.compute_signal_strength(session, ideal_point)#

Compute the signal strength of user preferences.

This is the tech-friendly alias for compute_preference_strength.

Higher signal strength means clearer, more consistent preferences. Low signal strength indicates noisy or random choices.

Parameters:
Return type:

ndarray[tuple[Any, …], dtype[float64]]

Risk Analysis#

prefgraph.compute_risk_profile(session, rho_bounds=(-2.0, 5.0), tolerance=1e-06)[source]#

Estimate risk profile from choices under uncertainty.

Uses Constant Relative Risk Aversion (CRRA) utility model:

u(x) = x^(1-ρ) / (1-ρ) for ρ ≠ 1 u(x) = ln(x) for ρ = 1

where ρ is the Arrow-Pratt coefficient of relative risk aversion.

This function estimates ρ using Maximum Likelihood Estimation (MLE) with a logistic choice model. This is an econometric approach; for the revealed preference axiom approach, see Chambers, Echenique, and Saito (2015).

Parameters:
  • session (RiskChoiceLog) – RiskSession with safe values, risky lotteries, and choices

  • rho_bounds (tuple[float, float]) – Search bounds for risk aversion coefficient (min, max)

  • tolerance (float) – Convergence tolerance for optimization

Returns:

RiskProfileResult with estimated risk profile

Return type:

RiskProfileResult

Example

>>> import numpy as np
>>> from prefgraph import RiskSession, compute_risk_profile
>>> # Risk-averse person: prefers $50 certain over 50/50 chance of $100/$0
>>> safe = np.array([50.0, 40.0, 30.0])
>>> outcomes = np.array([[100.0, 0.0], [100.0, 0.0], [100.0, 0.0]])
>>> probs = np.array([[0.5, 0.5], [0.5, 0.5], [0.5, 0.5]])
>>> choices = np.array([False, False, True])  # Only takes gamble at $30
>>> session = RiskSession(safe, outcomes, probs, choices)
>>> result = compute_risk_profile(session)
>>> result.risk_category
'risk_averse'
prefgraph.check_expected_utility_axioms(session)[source]#

Check if choices are consistent with Expected Utility axioms.

Tests for violations of: 1. Monotonicity: preferring more to less 2. Independence: compound lottery invariance

Parameters:

session (RiskChoiceLog) – RiskSession with choice data

Returns:

Tuple of (is_consistent, list of violation descriptions)

Return type:

tuple[bool, list[str]]

prefgraph.classify_risk_type(session)[source]#

Quick classification of decision-maker type.

  • “gambler”: Risk-seeking, prefers uncertainty

  • “investor”: Risk-averse, prefers certainty

  • “neutral”: Maximizes expected value

  • “inconsistent”: Choices don’t fit any clear pattern

Parameters:

session (RiskChoiceLog) – RiskSession with choice data

Returns:

Classification string

Return type:

Literal[‘gambler’, ‘investor’, ‘neutral’, ‘inconsistent’]

Integrability (Slutsky Conditions)#

Test whether observed demand data is consistent with integrability conditions. Based on Chambers & Echenique (2016) Chapter 6.4-6.5.

prefgraph.test_integrability(log, symmetry_tolerance=0.1, nsd_tolerance=1e-06, method='regression', compute_pvalue=True)[source]#

Test if demand data satisfies integrability conditions.

Integrability requires the Slutsky matrix to be: 1. Symmetric: S[i,j] = S[j,i] 2. Negative semi-definite: all eigenvalues <= 0

The Slutsky matrix captures substitution effects holding utility constant. If both conditions hold, demand can be derived from utility maximization.

This function uses theoretically rigorous estimation methods: - “regression”: Local polynomial regression (recommended, default) - “stone_geary”: Stone-Geary/Linear Expenditure System - “finite_diff”: Legacy finite differences

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • symmetry_tolerance (float) – Tolerance for symmetry test (relative deviation)

  • nsd_tolerance (float) – Tolerance for eigenvalue test

  • method (str) – Slutsky matrix estimation method

  • compute_pvalue (bool) – Whether to compute p-value for NSD test

Returns:

IntegrabilityResult with Slutsky conditions analysis

Return type:

IntegrabilityResult

Example

>>> from prefgraph import BehaviorLog, test_integrability
>>> result = test_integrability(user_log)
>>> if result.is_integrable:
...     print("Demand is rationalizable by utility maximization")
>>> else:
...     print(f"Failed: symmetric={result.is_symmetric}, NSD={result.is_negative_semidefinite}")

References

Chambers & Echenique (2016), Chapter 6.4-6.5 Hurwicz & Uzawa (1971), “On the Integrability of Demand Functions” Deaton & Muellbauer (1980), “Economics and Consumer Behavior”

prefgraph.compute_slutsky_matrix(log, method='regression')[source]#

Estimate the Slutsky substitution matrix from demand data.

The Slutsky matrix S[i,j] measures the substitution effect: how demand for good i changes when price of good j changes, holding utility constant.

S[i,j] = ∂x_i/∂p_j + x_j * ∂x_i/∂m

where m is income/expenditure.

This function provides multiple estimation methods: - “regression”: Local polynomial regression (recommended) - “stone_geary”: Stone-Geary/Linear Expenditure System estimation - “finite_diff”: Legacy finite differences method

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • method (str) – Estimation method - “regression”, “stone_geary”, or “finite_diff”

Returns:

N x N Slutsky matrix

Return type:

NDArray[np.float64]

References

Deaton & Muellbauer (1980), “Economics and Consumer Behavior” Chambers & Echenique (2016), Chapter 6.4

prefgraph.check_slutsky_symmetry(slutsky_matrix, tolerance=0.1)[source]#

Check if Slutsky matrix is symmetric.

Symmetry is a necessary condition for integrability: S[i,j] = S[j,i] for all i,j.

Parameters:
  • slutsky_matrix (ndarray[tuple[Any, ...], dtype[float64]]) – N x N Slutsky matrix

  • tolerance (float) – Relative tolerance for symmetry

Returns:

Tuple of (is_symmetric, violations, max_deviation)

Return type:

tuple[bool, list[tuple[int, int]], float]

prefgraph.check_slutsky_nsd(slutsky_matrix, tolerance=1e-06, compute_pvalue=True, n_simulations=1000)[source]#

Check if Slutsky matrix is negative semi-definite with statistical test.

Negative semi-definiteness requires all eigenvalues <= 0. This is a necessary condition for utility maximization.

This function provides proper statistical testing using the asymptotic distribution of the largest eigenvalue under the null hypothesis that the true Slutsky matrix is NSD.

The test statistic is: T = n * max(0, λ_max) Under H0, this follows a mixture of chi-squared distributions.

Parameters:
  • slutsky_matrix (ndarray[tuple[Any, ...], dtype[float64]]) – N x N Slutsky matrix

  • tolerance (float) – Tolerance for positive eigenvalues

  • compute_pvalue (bool) – Whether to compute Monte Carlo p-value

  • n_simulations (int) – Number of simulations for p-value computation

Returns:

Tuple of (is_nsd, eigenvalues, max_eigenvalue, p_value) p_value is None if compute_pvalue=False

Return type:

tuple[bool, ndarray[tuple[Any, …], dtype[float64]], float, float | None]

Warning

This function symmetrizes the Slutsky matrix before computing eigenvalues. If the original matrix is significantly asymmetric, this may mask problems. Check symmetry separately using check_slutsky_symmetry().

References

Lewbel (1995), “Consistent Nonparametric Hypothesis Tests” Robin & Smith (2000), “Tests of Rank”

Welfare Analysis#

Analyze welfare changes from price variations using compensating and equivalent variation. Based on Chambers & Echenique (2016) Chapter 7.3-7.4.

prefgraph.analyze_welfare_change(baseline_log, policy_log, tolerance=1e-06, method='exact')[source]#

Analyze welfare change between two scenarios.

Computes both compensating variation (CV) and equivalent variation (EV) to measure the welfare impact of a policy or price change.

CV: Amount of money to give consumer after the change to restore original utility level.

EV: Amount of money equivalent to the utility change at original prices.

This function uses theoretically rigorous methods: - “exact”: Uses Afriat utility recovery and constrained optimization - “vartia”: Uses Vartia (1983) path integral approximation - “bounds”: Uses Laspeyres/Paasche bounds (fastest but least accurate)

Parameters:
  • baseline_log (BehaviorLog) – BehaviorLog at baseline (pre-policy) prices

  • policy_log (BehaviorLog) – BehaviorLog at policy (post-change) prices

  • tolerance (float) – Numerical tolerance

  • method (str) – Computation method - “exact”, “vartia”, or “bounds”

Returns:

WelfareResult with CV, EV, and welfare direction

Return type:

WelfareResult

Example

>>> from prefgraph import BehaviorLog, analyze_welfare_change
>>> result = analyze_welfare_change(baseline_data, policy_data)
>>> print(f"Welfare direction: {result.welfare_direction}")
>>> print(f"CV: ${result.compensating_variation:.2f}")
>>> print(f"EV: ${result.equivalent_variation:.2f}")

References

Chambers & Echenique (2016), Chapter 7.3-7.4 Afriat (1967), “The Construction of Utility Functions” Vartia (1983), “Efficient Methods of Measuring Welfare Change”

prefgraph.compute_compensating_variation(baseline_log, policy_log, target_utility=None, tolerance=1e-06, method='exact')[source]#

Compute compensating variation (CV).

CV measures how much additional money the consumer would need at the new prices to achieve the old utility level.

CV > 0: Consumer needs compensation (welfare worsened) CV < 0: Consumer can afford to pay (welfare improved)

This function uses theoretically rigorous methods by default: - “exact”: Uses Afriat utility recovery and solves min{p_new @ x : U(x) >= U_baseline} - “vartia”: Uses Vartia (1983) path integral approximation - “bounds”: Uses Laspeyres bound (fastest but least accurate)

Parameters:
  • baseline_log (BehaviorLog) – BehaviorLog at baseline prices

  • policy_log (BehaviorLog) – BehaviorLog at policy prices

  • target_utility (float | None) – Deprecated parameter (kept for backward compatibility)

  • tolerance (float) – Numerical tolerance

  • method (str) – Computation method - “exact”, “vartia”, or “bounds”

Returns:

Compensating variation amount

Return type:

float

References

Chambers & Echenique (2016), Chapter 7.3-7.4 Afriat (1967), “The Construction of Utility Functions”

prefgraph.compute_equivalent_variation(baseline_log, policy_log, target_utility=None, tolerance=1e-06, method='exact')[source]#

Compute equivalent variation (EV).

EV measures how much money at baseline prices would be equivalent to the utility change caused by the policy.

EV > 0: Policy improved welfare by this amount EV < 0: Policy worsened welfare by this amount

This function uses theoretically rigorous methods by default: - “exact”: Uses Afriat utility recovery and solves for expenditure function - “vartia”: Uses Vartia (1983) path integral approximation - “bounds”: Uses Paasche bound (fastest but least accurate)

Parameters:
  • baseline_log (BehaviorLog) – BehaviorLog at baseline prices

  • policy_log (BehaviorLog) – BehaviorLog at policy prices

  • target_utility (float | None) – Deprecated parameter (kept for backward compatibility)

  • tolerance (float) – Numerical tolerance

  • method (str) – Computation method - “exact”, “vartia”, or “bounds”

Returns:

Equivalent variation amount

Return type:

float

References

Chambers & Echenique (2016), Chapter 7.3-7.4 Afriat (1967), “The Construction of Utility Functions”

prefgraph.recover_cost_function(log, target_utility=None, tolerance=1e-08)[source]#

Estimate the expenditure (cost) function from revealed preference data.

The expenditure function e(p, u) gives the minimum cost to achieve utility level u at prices p.

This is a wrapper around recover_expenditure_function() that provides a backward-compatible interface while using the rigorous Afriat approach.

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • target_utility (float | None) – Optional utility level to estimate cost for (deprecated)

  • tolerance (float) – Numerical tolerance for LP solver

Returns:

  • ‘success’: Whether Afriat utility recovery succeeded

  • ’utility_function’: Callable to evaluate utility at any bundle

  • ’expenditure_function’: Callable to compute e(p, u)

  • ’observation_utilities’: Utility at each observed bundle

  • ’observation_expenditures’: Expenditure at each observation

Return type:

Dictionary with cost function estimates including

prefgraph.compute_consumer_surplus(log, good_index, price_change)[source]#

Compute consumer surplus change for a price change in one good.

Uses the area under the demand curve approximation.

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • good_index (int) – Index of the good with price change

  • price_change (float) – Change in price (positive = increase)

Returns:

Consumer surplus change (negative if price increased)

Return type:

float

prefgraph.compute_deadweight_loss(baseline_log, policy_log, method='exact')[source]#

Estimate deadweight loss from a policy intervention.

Deadweight loss is the welfare loss not captured by transfers. It represents the economic inefficiency caused by market distortions.

For a welfare-improving policy: DWL = CV - EV (EV > CV implies inefficiency) For a welfare-worsening policy: DWL = EV - CV (CV > EV implies inefficiency)

The Harberger approximation uses: DWL ≈ |CV - EV| / 2

This function uses theoretically rigorous CV/EV computation methods.

Parameters:
  • baseline_log (BehaviorLog) – BehaviorLog at efficient prices

  • policy_log (BehaviorLog) – BehaviorLog at distorted prices

  • method (str) – CV/EV computation method - “exact”, “vartia”, or “bounds”

Returns:

Estimated deadweight loss (always non-negative)

Return type:

float

References

Harberger (1964), “The Measurement of Waste” Chambers & Echenique (2016), Chapter 7.4

Additive Separability#

Test whether preferences are additively separable across goods. Based on Chambers & Echenique (2016) Chapter 9.3.

prefgraph.test_additive_separability(log, cross_effect_threshold=0.1, price_change_threshold=0.05)[source]#

Test if preferences are additively separable.

Additive separability means U(x) = Σ u_i(x_i), implying: - Each good’s marginal utility depends only on its own quantity - Cross-price effects (holding income constant) are zero - Goods can be priced independently

This is a stronger condition than weak separability and quasilinearity.

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • cross_effect_threshold (float) – Threshold for cross-effect significance

  • price_change_threshold (float) – Minimum price change to consider

Returns:

AdditivityResult with separability analysis

Return type:

AdditivityResult

Example

>>> from prefgraph import BehaviorLog, test_additive_separability
>>> result = test_additive_separability(user_log)
>>> if result.is_additive:
...     print("Goods can be priced independently")
>>> else:
...     print(f"Cross-effects found: {result.violations}")

References

Chambers & Echenique (2016), Chapter 9.3 Gorman, W.M. (1968). “The Structure of Utility Functions”

prefgraph.identify_additive_groups(cross_effects_matrix, threshold=0.1)[source]#

Identify groups of goods that are additively separable from each other.

Uses connected components: goods i and j are in the same group if there’s a significant cross-effect between them (directly or transitively).

Parameters:
  • cross_effects_matrix (ndarray[tuple[Any, ...], dtype[float64]]) – N x N matrix of cross-price effects

  • threshold (float) – Threshold for significant cross-effect

Returns:

List of sets, each containing good indices in a separable group

Return type:

list[set[int]]

Example

>>> groups = identify_additive_groups(cross_effects)
>>> # If groups = [{0, 1}, {2, 3, 4}], then goods 0-1 and 2-4
>>> # can be priced independently from each other
prefgraph.check_no_cross_effects(log, good_i, good_j, price_change_threshold=0.05)[source]#

Test if there are cross-price effects between two goods.

For additive preferences, when p_j changes (other prices constant), x_i should not change (holding income constant).

Parameters:
  • log (BehaviorLog) – BehaviorLog with prices and quantities

  • good_i (int) – Index of quantity good

  • good_j (int) – Index of price good

  • price_change_threshold (float) – Minimum price change to consider

Returns:

Dictionary with cross-effect analysis

Return type:

dict

Compensated Demand#

Analyze substitution and income effects via Slutsky decomposition. Based on Chambers & Echenique (2016) Chapter 10.3.

prefgraph.decompose_price_effects(session, price_change_threshold=0.05, tolerance=1e-10)[source]#

Decompose price effects into substitution and income effects.

The Slutsky equation states: Total effect = Substitution effect + Income effect dx_i/dp_j = (∂x_i/∂p_j)|_u + x_j * (∂x_i/∂m)

The substitution effect measures how demand changes when price changes but utility is held constant (compensated demand).

The income effect measures how the change in purchasing power affects demand.

Parameters:
  • session (BehaviorLog) – ConsumerSession with prices and quantities

  • price_change_threshold (float) – Minimum price change to consider

  • tolerance (float) – Numerical tolerance

Returns:

CompensatedDemandResult with Slutsky decomposition

Return type:

CompensatedDemandResult

Example

>>> from prefgraph import ConsumerSession, decompose_price_effects
>>> result = decompose_price_effects(user_session)
>>> print(f"Substitution effect for good 0 from price 1: {result.substitution_effects[0,1]:.3f}")
>>> print(f"Income effect: {result.income_effects[0,1]:.3f}")
>>> print(f"Satisfies compensated law: {result.satisfies_compensated_law}")

References

Chambers & Echenique (2016), Chapter 10.3 Slutsky, E. (1915). “On the Theory of the Budget of the Consumer”

prefgraph.compute_hicksian_demand(session, target_utility=None, method='exact')[source]#

Compute Hicksian (compensated) demand via expenditure minimization.

Hicksian demand h(p, u) solves:

min_x  p @ x
s.t.   U(x) >= u
       x >= 0

This implementation recovers the Afriat utility function U(x) from the data and uses constrained optimization to solve for Hicksian demand at any price vector and utility level.

Parameters:
  • session (BehaviorLog) – ConsumerSession with prices and quantities

  • target_utility (float | None) – Utility level for computing derivatives (default: median)

  • method (str) – Computation method: - “exact”: Full Afriat recovery + constrained optimization - “approximation”: Legacy finite differences (faster but approximate)

Returns:

  • ‘success’: Whether Afriat utility recovery succeeded

  • ’hicksian_demand_fn’: Callable (prices, utility) -> quantities

  • ’hicksian_derivatives’: N x N matrix of ∂h_i/∂p_j at target utility

  • ’target_utility’: Utility level used for derivatives

  • ’utility_function’: Callable to evaluate utility at any bundle

  • ’observation_utilities’: Utility at each observed bundle

  • ’observations_used’: Number of observations (for backward compatibility)

Return type:

Dictionary containing

Example

>>> from prefgraph import ConsumerSession, compute_hicksian_demand
>>> import numpy as np
>>> P = np.array([[1.0, 2.0], [1.5, 1.5], [2.0, 1.0]])
>>> Q = np.array([[2.0, 1.0], [1.5, 1.5], [1.0, 2.0]])
>>> session = ConsumerSession(prices=P, quantities=Q)
>>> result = compute_hicksian_demand(session, method='exact')
>>> if result['success']:
...     h = result['hicksian_demand_fn']
...     print(f"h([1,1], 0.5) = {h([1, 1], 0.5)}")

References

Chambers & Echenique (2016), Chapter 10.3 Afriat (1967), “The Construction of Utility Functions”

prefgraph.check_compensated_law_of_demand(session, tolerance=1e-06)[source]#

Check if data satisfies the compensated law of demand.

The compensated law states that substitution effects are negative for own-price changes: when price increases (holding utility constant), quantity demanded decreases.

Parameters:
  • session (BehaviorLog) – ConsumerSession with prices and quantities

  • tolerance (float) – Numerical tolerance

Returns:

Dictionary with test results

Return type:

dict

prefgraph.compute_slutsky_decomposition(session, price_change_threshold=0.05, tolerance=1e-10)#

Compute Slutsky decomposition of price effects.

Alias for decompose_price_effects.

Parameters:
Return type:

CompensatedDemandResult

prefgraph.estimate_compensated_demand(session, target_utility=None, method='exact')#

Estimate compensated (Hicksian) demand.

Alias for compute_hicksian_demand.

Parameters:
Return type:

dict

General Metric Preferences#

Analyze preferences with general distance metrics beyond Euclidean. Based on Chambers & Echenique (2016) Chapter 11.3-11.4.

prefgraph.find_ideal_point_general(session, metric='L2', p=2.0, method='SLSQP', max_iterations=1000, bounds=None)[source]#

Find ideal point using a general distance metric.

Extends the Euclidean preference model to arbitrary metrics: - L1 (Manhattan): d(x,y) = Σ|x_i - y_i| - L2 (Euclidean): d(x,y) = √(Σ(x_i - y_i)²) - Linf (Chebyshev): d(x,y) = max|x_i - y_i| - Minkowski: d(x,y) = (Σ|x_i - y_i|^p)^(1/p)

Parameters:
  • session (SpatialSession) – SpatialSession with item features and choice data

  • metric (str) – Distance metric (“L1”, “L2”, “Linf”, “minkowski”)

  • p (float) – Minkowski parameter (p=1 -> L1, p=2 -> L2, p=inf -> Linf)

  • method (str) – Scipy optimization method

  • max_iterations (int) – Maximum optimization iterations

  • bounds (tuple[float, float] | None) – Optional (min, max) bounds for each dimension. If None, bounds are inferred from the data range with 10% padding.

Returns:

GeneralMetricResult with ideal point and metric analysis

Return type:

GeneralMetricResult

Example

>>> from prefgraph import SpatialSession, find_ideal_point_general
>>> result = find_ideal_point_general(session, metric="L1")
>>> print(f"Ideal point: {result.ideal_point}")
>>> print(f"Violations: {result.num_violations}")

References

Chambers & Echenique (2016), Chapter 11.3-11.4

prefgraph.determine_best_metric(session, candidates=None, method='SLSQP')[source]#

Find the distance metric that best explains the choice data.

Tries multiple metrics and selects the one with fewest violations.

Parameters:
  • session (SpatialSession) – SpatialSession with item features and choice data

  • candidates (list[str] | None) – List of metrics to try (default: [“L1”, “L2”, “Linf”])

  • method (str) – Optimization method

Returns:

GeneralMetricResult for the best metric

Return type:

GeneralMetricResult

Example

>>> from prefgraph import SpatialSession, determine_best_metric
>>> result = determine_best_metric(session)
>>> print(f"Best metric: {result.best_metric}")
>>> print(f"Violations: {result.num_violations}")
prefgraph.test_metric_rationality(session, metric='L2', p=2.0)[source]#

Test if choices are rationalizable under a given metric.

Parameters:
  • session (SpatialSession) – SpatialSession with choice data

  • metric (str) – Distance metric to use

  • p (float) – Minkowski parameter (if metric=”minkowski”)

Returns:

Tuple of (is_rational, violations)

Return type:

tuple[bool, list[tuple[int, int]]]

Stochastic Choice#

Analyze probabilistic choice data using random utility models. Based on Chambers & Echenique (2016) Chapter 13.

StochasticChoiceLog#

class prefgraph.StochasticChoiceLog(menus, choice_frequencies, item_labels=None, total_observations_per_menu=None, user_id=None, metadata=<factory>)[source]#

Bases: object

Represents stochastic/probabilistic choice data for random utility models.

This data structure supports analyzing choices where the same decision-maker may make different choices in the same situation (probabilistic choice). Based on Chapter 13 of Chambers & Echenique (2016).

Variables:
  • menus (list[frozenset[int]]) – List of T menus, each a frozenset of item indices.

  • choice_frequencies (list[dict[int, int]]) – List of T dictionaries mapping item -> choice count. Each dict shows how many times each item was chosen from that menu.

  • item_labels (list[str] | None) – Optional list of N item names for display.

  • total_observations_per_menu (list[int] | None) – Optional list of total observations per menu.

  • user_id (str | None) – Optional identifier for the user/session.

  • metadata (dict[str, Any]) – Optional dictionary for additional attributes.

Parameters:

Example

>>> # Same menu presented 100 times, user chose item 0 60 times, item 1 40 times
>>> log = StochasticChoiceLog(
...     menus=[frozenset({0, 1, 2})],
...     choice_frequencies=[{0: 60, 1: 40, 2: 0}],
... )
>>> log.get_choice_probability(0, 0)  # P(choose 0 from menu 0)
0.6
menus: list[frozenset[int]]#
choice_frequencies: list[dict[int, int]]#
item_labels: list[str] | None = None#
total_observations_per_menu: list[int] | None = None#
user_id: str | None = None#
metadata: dict[str, Any]#
property num_menus: int#

Number of distinct menus T.

property all_items: frozenset[int]#

Set of all item indices that appear in any menu.

property num_items: int#

Number of unique items N.

get_choice_probability(menu_idx, item)[source]#

Get probability of choosing item from menu at menu_idx.

Parameters:
Return type:

float

get_choice_probabilities(menu_idx)[source]#

Get all choice probabilities for a menu.

Parameters:

menu_idx (int)

Return type:

dict[int, float]

classmethod from_repeated_choices(menus, choices)[source]#

Create StochasticChoiceLog from repeated deterministic choice data.

Groups observations by menu and computes choice frequencies.

Parameters:
  • menus (list[frozenset[int]]) – List of menus (may have duplicates)

  • choices (list[int]) – List of chosen items (one per observation)

Returns:

StochasticChoiceLog with aggregated frequencies

Return type:

StochasticChoiceLog

summary()[source]#

Run comprehensive analysis and return unified summary.

This provides a Stata/statsmodels-style summary of stochastic choice analysis including RUM consistency, regularity, and transitivity tests.

Returns:

StochasticChoiceSummary with RUM, regularity, and transitivity results

Return type:

StochasticChoiceSummary

Example

>>> log = StochasticChoiceLog(menus, choice_frequencies)
>>> print(log.summary())  # Full report
__init__(menus, choice_frequencies, item_labels=None, total_observations_per_menu=None, user_id=None, metadata=<factory>)#
Parameters:
Return type:

None

prefgraph.fit_random_utility_model(log, model_type='logit', max_iterations=1000)[source]#

Fit a random utility model to stochastic choice data.

Random utility models assume the consumer has utility U_i = V_i + epsilon_i where V_i is deterministic and epsilon_i is random. Different assumptions about epsilon distribution lead to different models: - Logit: epsilon ~ Gumbel (IIA holds) - Probit: epsilon ~ Normal - Luce: probability proportional to utility

Parameters:
  • log (StochasticChoiceLog) – StochasticChoiceLog with choice frequency data

  • model_type (str) – Type of model (“logit”, “probit”, “luce”)

  • max_iterations (int) – Maximum optimization iterations

Returns:

StochasticChoiceResult with model parameters and fit statistics

Return type:

StochasticChoiceResult

Example

>>> from prefgraph import StochasticChoiceLog, fit_random_utility_model
>>> result = fit_random_utility_model(choice_data, model_type="logit")
>>> print(f"Model: {result.model_type}")
>>> print(f"Satisfies IIA: {result.satisfies_iia}")
>>> print(f"Log-likelihood: {result.log_likelihood:.2f}")

References

Chambers & Echenique (2016), Chapter 13 McFadden, D. (1974). “Conditional Logit Analysis of Qualitative Choice Behavior”

prefgraph.test_mcfadden_axioms(log)[source]#

Test McFadden’s axioms for random utility maximization.

The axioms include: 1. Regularity: P(x|A) >= P(x|B) when A ⊆ B (removing options doesn’t decrease choice probability) 2. IIA: P(x|A)/P(y|A) = P(x|B)/P(y|B) for all A,B containing x,y

Parameters:

log (StochasticChoiceLog) – StochasticChoiceLog with choice frequency data

Returns:

Dictionary with axiom test results

Return type:

dict

prefgraph.estimate_choice_probabilities(log, utilities, model_type='logit')[source]#

Estimate choice probabilities given utilities.

Parameters:
  • log (StochasticChoiceLog) – StochasticChoiceLog with menu structure

  • utilities (NDArray[np.float64]) – Array of item utilities

  • model_type (str) – Type of model

Returns:

Array of choice probabilities (flattened)

Return type:

NDArray[np.float64]

prefgraph.check_independence_irrelevant_alternatives(log, tolerance=0.1)[source]#

Test Independence of Irrelevant Alternatives (IIA).

IIA states that the relative odds of choosing x over y should not depend on what other alternatives are available: P(x|A) / P(y|A) = P(x|B) / P(y|B) for all menus A, B containing both x and y.

Parameters:
  • log (StochasticChoiceLog) – StochasticChoiceLog with choice frequency data

  • tolerance (float) – Tolerance for ratio comparison

Returns:

True if IIA approximately holds

Return type:

bool

Note

IIA is a strong condition that often fails in practice (e.g., red bus/blue bus paradox).

prefgraph.fit_luce_model(log)[source]#

Fit Luce choice model to stochastic choice data.

The Luce model (also called Bradley-Terry) assumes: P(x|A) = v(x) / Σ_{y ∈ A} v(y)

where v(x) is the “choice value” of item x.

Parameters:

log (StochasticChoiceLog) – StochasticChoiceLog with choice frequency data

Returns:

Tuple of (utilities, parameters)

Return type:

tuple[NDArray[np.float64], dict[str, float]]

Limited Attention#

Test rationality under limited attention and estimate consideration sets. Based on Chambers & Echenique (2016) Chapter 14.

prefgraph.test_attention_rationality(log, max_consideration_size=None)[source]#

Test if choices are rationalizable with limited attention.

A choice is attention-rational if there exists: 1. A preference ordering over items 2. A consideration set function (what items are noticed) Such that each choice is optimal among considered items.

This is a weaker notion than standard rationality - it allows apparent violations due to limited attention.

Parameters:
  • log (MenuChoiceLog) – MenuChoiceLog with menus and choices

  • max_consideration_size (int | None) – Maximum consideration set size (None = no limit)

Returns:

AttentionResult with consideration sets and attention analysis

Return type:

AttentionResult

Example

>>> from prefgraph import MenuChoiceLog, test_attention_rationality
>>> result = test_attention_rationality(choice_log)
>>> if result.is_attention_rational:
...     print("Choices are rationalizable with limited attention")
...     print(f"Avg consideration set size: {result.mean_consideration_size:.1f}")

Note

Complexity: This function calls validate_menu_sarp internally, which uses Floyd-Warshall with O(I³) complexity where I is the number of unique items. For large item sets, this can be slow.

References

Chambers & Echenique (2016), Chapter 14 Manzini, P. & Mariotti, M. (2014). “Stochastic Choice and Consideration Sets”

prefgraph.estimate_consideration_sets(log, method='greedy')[source]#

Estimate consideration sets for each observation.

The consideration set is the subset of menu items that the consumer actually notices/considers before making a choice.

Parameters:
  • log (MenuChoiceLog) – MenuChoiceLog with menus and choices

  • method (str) – Estimation method (“greedy”, “optimal”, “salience”)

Returns:

List of consideration sets, one per observation

Return type:

list[set[int]]

Note

The chosen item is always in the consideration set.

prefgraph.compute_salience_weights(log, consideration_sets=None)[source]#

Compute salience weights for each item.

Higher weight = more likely to be noticed/considered. Estimated from frequency of appearing in consideration sets.

Parameters:
  • log (MenuChoiceLog) – MenuChoiceLog with menus and choices

  • consideration_sets (list[set[int]] | None) – Optional pre-computed consideration sets

Returns:

Array of salience weights (one per item)

Return type:

NDArray[np.float64]

prefgraph.test_attention_filter(log, filter_function)[source]#

Test if choices are rational given a specific attention filter.

An attention filter specifies which items are considered at each observation. This tests if choices are optimal within filtered menus.

Parameters:
  • log (MenuChoiceLog) – MenuChoiceLog with menus and choices

  • filter_function (callable) – Function(menu, t) -> consideration_set

Returns:

Dictionary with test results

Return type:

dict

Production Theory#

Analyze firm behavior using revealed preference methods for production. Based on Chambers & Echenique (2016) Chapter 15.

ProductionLog#

class prefgraph.ProductionLog(input_prices, input_quantities, output_prices, output_quantities, firm_id=None, metadata=<factory>, _cost_matrix=None, _revenue_matrix=None)[source]#

Bases: object

Represents firm production data for production theory analysis.

This data structure supports analyzing firm behavior using revealed preference methods for profit maximization and cost minimization tests. Based on Chapter 15 of Chambers & Echenique (2016).

Variables:
  • input_prices (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x N_inputs matrix of input prices per observation.

  • input_quantities (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x N_inputs matrix of input quantities used.

  • output_prices (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x N_outputs matrix of output prices per observation.

  • output_quantities (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – T x N_outputs matrix of output quantities produced.

  • firm_id (str | None) – Optional identifier for the firm.

  • metadata (dict[str, Any]) – Optional dictionary for additional attributes.

Parameters:
Properties:

profit: Profit at each observation (revenue - cost) total_cost: Total input cost at each observation total_revenue: Total output revenue at each observation

Example

>>> import numpy as np
>>> # Firm uses 2 inputs to produce 1 output over 3 periods
>>> log = ProductionLog(
...     input_prices=np.array([[1.0, 2.0], [1.5, 2.0], [1.0, 2.5]]),
...     input_quantities=np.array([[10.0, 5.0], [8.0, 6.0], [12.0, 4.0]]),
...     output_prices=np.array([[10.0], [12.0], [11.0]]),
...     output_quantities=np.array([[5.0], [4.0], [6.0]]),
... )
>>> log.profit  # Revenue - Cost
array([30., 26., 46.])
input_prices: ndarray[tuple[Any, ...], dtype[float64]]#
input_quantities: ndarray[tuple[Any, ...], dtype[float64]]#
output_prices: ndarray[tuple[Any, ...], dtype[float64]]#
output_quantities: ndarray[tuple[Any, ...], dtype[float64]]#
firm_id: str | None = None#
metadata: dict[str, Any]#
property num_observations: int#

Number of observations T.

property num_inputs: int#

Number of inputs N_inputs.

property num_outputs: int#

Number of outputs N_outputs.

property total_cost: ndarray[tuple[Any, ...], dtype[float64]]#

Total input cost at each observation.

property total_revenue: ndarray[tuple[Any, ...], dtype[float64]]#

Total output revenue at each observation.

property profit: ndarray[tuple[Any, ...], dtype[float64]]#

Profit at each observation (revenue - cost).

property cost_matrix: ndarray[tuple[Any, ...], dtype[float64]]#

T x T matrix where C[i,j] = cost of using inputs j at prices i.

property revenue_matrix: ndarray[tuple[Any, ...], dtype[float64]]#

T x T matrix where R[i,j] = revenue from outputs j at prices i.

summary()[source]#

Run comprehensive analysis and return unified summary.

This provides a Stata/statsmodels-style summary of production analysis including profit maximization, cost minimization, and efficiency metrics.

Returns:

ProductionSummary with profit max, cost min, and efficiency results

Return type:

ProductionSummary

Example

>>> log = ProductionLog(input_prices, input_quantities, output_prices, output_quantities)
>>> print(log.summary())  # Full report
__init__(input_prices, input_quantities, output_prices, output_quantities, firm_id=None, metadata=<factory>, _cost_matrix=None, _revenue_matrix=None)#
Parameters:
Return type:

None

prefgraph.test_profit_maximization(log, tolerance=1e-06)[source]#

Test if firm behavior is consistent with profit maximization.

Production GARP: For observations i and j, if firm at i could have achieved the output of j at the prices of i, and the profit would have been at least as high, then the reverse should not also hold.

The test constructs a revealed preferred-at-least-as-profitable relation and checks for cycles.

Parameters:
  • log (ProductionLog) – ProductionLog with input/output prices and quantities

  • tolerance (float) – Numerical tolerance

Returns:

ProductionGARPResult with profit maximization analysis

Return type:

ProductionGARPResult

Example

>>> from prefgraph import ProductionLog, test_profit_maximization
>>> result = test_profit_maximization(firm_data)
>>> if result.is_profit_maximizing:
...     print("Firm behavior is profit-maximizing consistent")
>>> else:
...     print(f"Found {result.num_violations} violations")

References

Chambers & Echenique (2016), Chapter 15 Varian, H.R. (1984). “The Nonparametric Approach to Production Analysis”

prefgraph.check_cost_minimization(log, tolerance=1e-06)[source]#

Test if firm behavior is consistent with cost minimization.

Cost minimization is the dual of profit maximization. The firm should choose inputs that minimize cost for a given output level.

Parameters:
  • log (ProductionLog) – ProductionLog with input/output prices and quantities

  • tolerance (float) – Numerical tolerance

Returns:

Dictionary with cost minimization analysis

Return type:

dict

prefgraph.estimate_returns_to_scale(log)[source]#

Estimate returns to scale from production data.

Returns to scale indicates how output changes when all inputs are scaled proportionally: - Increasing: doubling inputs more than doubles output - Constant: doubling inputs exactly doubles output - Decreasing: doubling inputs less than doubles output

Parameters:

log (ProductionLog) – ProductionLog with input/output quantities

Returns:

One of “increasing”, “constant”, “decreasing”, or “variable”

Return type:

str

prefgraph.compute_technical_efficiency(log, method='output_oriented')[source]#

Compute technical efficiency for each observation.

Technical efficiency measures how close the firm operates to the production frontier. A score of 1.0 means fully efficient.

Parameters:
  • log (ProductionLog) – ProductionLog with input/output data

  • method (str) – “output_oriented” or “input_oriented”

Returns:

Array of efficiency scores (one per observation)

Return type:

NDArray[np.float64]

Data Generators#

prefgraph.datasets.generate_random_budgets(n_users=100000, n_obs=15, n_goods=5, functional_form='cobb_douglas', elasticity=0.5, rationality=0.7, noise_scale=0.3, price_range=(0.5, 5.0), budget_range=(10.0, 100.0), seed=42)[source]#

Generate random budget data for many users in parallel.

Each user gets random prices and quantities shaped (T, K), with demand computed from the specified functional form and perturbed by noise controlled by the rationality parameter.

Parameters:
  • n_users (int) – Number of users to generate.

  • n_obs (int | tuple[int, int]) – Observations per user. Int for fixed, (min, max) for variable.

  • n_goods (int) – Number of goods (columns in price/quantity arrays).

  • functional_form (str) – “cobb_douglas”, “ces”, or “leontief”.

  • elasticity (float) – CES elasticity of substitution sigma (only for “ces”).

  • rationality (float) – 0.0 = random quantities, 1.0 = exact utility-maximizing.

  • noise_scale (float) – Std dev of log-normal perturbation when rationality < 1.0.

  • price_range (tuple[float, float]) – (min, max) for uniform price draws.

  • budget_range (tuple[float, float]) – (min, max) for uniform budget draws.

  • seed (int) – Random seed for reproducibility.

Returns:

List of (prices, quantities) numpy array pairs, each (T, K). Directly consumable by Engine.analyze_arrays().

Return type:

list[tuple[ndarray, ndarray]]

prefgraph.datasets.generate_random_menus(n_users=100000, n_obs=10, n_items=5, menu_size=(2, 5), choice_model='logit', temperature=1.0, rationality=0.7, seed=42)[source]#

Generate random menu choice data for many users in parallel.

Each user gets a sequence of menus (subsets of items) with choices made according to the specified choice model.

Parameters:
  • n_users (int) – Number of users to generate.

  • n_obs (int | tuple[int, int]) – Observations per user. Int for fixed, (min, max) for variable.

  • n_items (int) – Total number of distinct items in the universe.

  • menu_size (int | tuple[int, int]) – Items per menu. Int for fixed, (min, max) for variable.

  • choice_model (str) – “logit”, “fixed_ranking”, or “uniform”.

  • temperature (float) – Logit softmax temperature (lower = more deterministic).

  • rationality (float) – Probability of following choice model vs random pick.

  • seed (int) – Random seed for reproducibility.

Returns:

List of (menus, choices, n_items) tuples. Directly consumable by Engine.analyze_menus().

Return type:

list[tuple[list[list[int]], list[int], int]]

prefgraph.datasets.generate_random_production(n_users=10000, n_obs=15, n_inputs=3, n_outputs=2, functional_form='cobb_douglas', rationality=0.7, noise_scale=0.3, seed=42)[source]#

Generate random production data for many firms in parallel.

Each firm gets input/output prices and quantities. The first n_inputs columns are inputs, the remaining n_outputs columns are outputs.

Parameters:
  • n_users (int) – Number of firms to generate.

  • n_obs (int | tuple[int, int]) – Observations per firm. Int for fixed, (min, max) for variable.

  • n_inputs (int) – Number of input goods.

  • n_outputs (int) – Number of output goods.

  • functional_form (str) – “cobb_douglas”, “ces”, or “leontief”.

  • rationality (float) – 0.0 = random, 1.0 = profit-maximizing.

  • noise_scale (float) – Std dev of log-normal perturbation.

  • seed (int) – Random seed for reproducibility.

Returns:

List of (prices, quantities) numpy array pairs, each T × (n_inputs + n_outputs).

Return type:

list[tuple[ndarray, ndarray]]

prefgraph.datasets.generate_random_intertemporal(n_users=10000, n_obs=10, n_periods=5, discount_factor=(0.8, 0.99), rationality=0.7, seed=42)[source]#

Generate random intertemporal choice data for many agents in parallel.

Each agent has a true discount factor delta and makes consumption allocation decisions across time periods with exponential discounting.

Parameters:
  • n_users (int) – Number of agents to generate.

  • n_obs (int | tuple[int, int]) – Observations per agent. Int for fixed, (min, max) for variable.

  • n_periods (int) – Number of time periods (columns).

  • discount_factor (float | tuple[float, float]) – True delta range. Float for fixed, (min, max) for variable.

  • rationality (float) – 0.0 = random, 1.0 = optimal discounted allocation.

  • seed (int) – Random seed for reproducibility.

Returns:

List of (prices, quantities) numpy array pairs, each (T, n_periods).

Return type:

list[tuple[ndarray, ndarray]]

Dataset Loaders#

prefgraph.datasets.load_demo(n_users=100, n_obs=15, n_goods=5, seed=42, return_panel=False)[source]#

Load a synthetic demo dataset (offline, zero setup).

Generates deterministic budget data with a mix of rational and irrational consumers for testing and demos.

The dataset contains three types of consumers: - ~40% perfectly rational (Cobb-Douglas utility maximization) - ~40% noisy rational (perturbations from optimal) - ~20% irrational (random choices)

This creates an interesting CCEI distribution for demonstrations.

Parameters:
  • n_users (int) – Number of synthetic users (default 100).

  • n_obs (int) – Observations per user (default 15).

  • n_goods (int) – Number of goods (default 5).

  • seed (int) – Random seed for reproducibility (default 42).

  • return_panel (bool) – If True, return a BehaviorPanel instead.

Returns:

List of (prices T*K, quantities T*K) tuples ready for Engine.analyze_arrays(). If return_panel=True, returns a BehaviorPanel.

Return type:

list[tuple[ndarray, ndarray]]

prefgraph.datasets.load_dunnhumby(data_dir=None, n_households=None, min_weeks=10, period=None)[source]#

Load Dunnhumby grocery dataset as a BehaviorPanel.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing transaction_data.csv and product.csv. If None, searches standard locations.

  • n_households (int | None) – Max number of households to include (None = all).

  • min_weeks (int) – Minimum active shopping weeks per household (default 10).

  • period (str | None) – Time aggregation level. None = one BehaviorLog per household across all weeks. “month” = split into monthly sub-sessions.

Returns:

BehaviorPanel with one BehaviorLog per household (or household-month).

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_open_ecommerce(data_dir=None, n_users=None, min_observations=5, top_n_categories=50)[source]#

Load Open E-Commerce (Amazon) dataset as a BehaviorPanel.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing amazon-purchases.csv.

  • n_users (int | None) – Max number of users to include (None = all).

  • min_observations (int) – Minimum active months per user (default 5).

  • top_n_categories (int) – Number of top categories to include (default 50).

Returns:

BehaviorPanel with one BehaviorLog per user.

Return type:

BehaviorPanel

prefgraph.datasets.load_uci_retail(data_dir=None, n_customers=None, min_transactions=5, top_n_products=50)[source]#

Load UCI Online Retail dataset as a BehaviorPanel.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing online_retail.xlsx.

  • n_customers (int | None) – Max number of customers to include (None = all).

  • min_transactions (int) – Minimum active months per customer (default 5).

  • top_n_products (int) – Number of top products to include (default 50).

Returns:

BehaviorPanel with one BehaviorLog per customer.

Return type:

BehaviorPanel

prefgraph.datasets.load_retailrocket(*args, **kwargs)[source]#

Lazy wrapper - defers pandas import until called.

prefgraph.datasets.load_instacart(data_dir=None, max_users=None, min_orders=10)[source]#

Load Instacart dataset as a BehaviorPanel.

Aggregates products at the aisle level (134 aisles). Uses heuristic per-aisle prices based on keyword matching of aisle names (e.g. fresh produce ~$2, meat/seafood ~$6, alcohol ~$10).

Parameters:
  • data_dir (str | Path | None) – Path to directory containing Instacart CSV files.

  • max_users (int | None) – Maximum number of users (None = all).

  • min_orders (int) – Minimum orders per user (default 10).

Returns:

BehaviorPanel with one BehaviorLog per user.

Return type:

BehaviorPanel

prefgraph.datasets.load_instacart_menu_v2(data_dir=None, max_users=50000, min_sessions=5, min_menu_size=2, min_pair_events=3)[source]#

Load Instacart as aisle-level menu choices with trailing-3 familiar menus.

Each observation is a (user, order, aisle) triple where:

  • The user reordered exactly one SKU in that aisle (choice)

  • The menu is all distinct products the user bought in that aisle across their previous 3 orders, plus the current choice

Invariants on returned logs: - Every choice is in its menu - Every menu has at least min_menu_size items - Every user has at least min_sessions observations

Parameters:
  • data_dir (str | Path | None) – Path to Instacart data directory. None uses standard locations.

  • max_users (int | None) – Cap on users returned. None returns all.

  • min_sessions (int) – Minimum valid observations per user (default 5).

  • min_menu_size (int) – Minimum menu cardinality (default 2).

  • min_pair_events (int) – Minimum events per (user, aisle) pair (default 3).

Returns:

Dict mapping “user_{id}” -> MenuChoiceLog, sorted by order_number.

Return type:

dict[str, MenuChoiceLog]

prefgraph.datasets.load_yoochoose(data_dir=None, min_sessions=5, max_users=5000, remap_items=True)[source]#

Load Yoochoose click-stream data as menu-choice observations.

Each session with a purchase becomes a menu-choice observation: items clicked = menu, purchased item = choice.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing yoochoose-clicks.dat and yoochoose-buys.dat.

  • min_sessions (int) – Minimum purchase sessions per user.

  • max_users (int | None) – Cap number of users returned (default 5000).

  • remap_items (bool) – Remap item IDs to 0..N-1 per user.

Returns:

Dict mapping session_id (str) -> MenuChoiceLog.

Return type:

dict[str, MenuChoiceLog]

prefgraph.datasets.load_olist(data_dir=None, n_customers=None, min_months=2, min_orders=3, n_categories=20)[source]#

Load Olist Brazilian E-Commerce dataset as a BehaviorPanel.

Joins orders, order items, products, and the customer identity table to build monthly budget vectors (price x quantity) across product categories per customer.

Olist anonymizes customer_id per order; the true repeat-buyer key is customer_unique_id from olist_customers_dataset.csv. ~3K customers have 2+ orders, ~250 have 3+.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing Olist CSV files. If None, searches standard locations.

  • n_customers (int | None) – Max number of customers to include (None = all).

  • min_months (int) – Minimum active months per customer (default 2).

  • min_orders (int) – Minimum number of distinct orders per customer (default 3). Most Olist customers have only 1 order.

  • n_categories (int) – Number of top product categories to use (default 20).

Returns:

BehaviorPanel with one BehaviorLog per customer (rows = months, cols = product categories).

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_m5(data_dir=None, aggregation='store', min_weeks=100, max_users=None)[source]#

Load M5 Walmart dataset as a BehaviorPanel.

Constructs weekly price-quantity panels by aggregating daily item-level sales within each store (or store-department). Quantities are total units sold per department (or category) per week. Prices are mean sell_price for that department-store-week.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing sales_train_evaluation.csv, sell_prices.csv, and calendar.csv. If None, searches standard locations.

  • aggregation (str) –

    User granularity level.

    • ”store”: 10 users (one per store), 7 goods (departments).

    • ”store_dept”: 70 users (one per store-department combination), goods are items within that department (aggregated to category). Each user has 3 goods (FOODS, HOBBIES, HOUSEHOLD).

  • min_weeks (int) – Minimum weeks with nonzero sales required per user (default 100).

  • max_users (int | None) – Optional cap on number of users returned.

Returns:

BehaviorPanel with one BehaviorLog per store (or store-dept).

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_rees46(*args, **kwargs)[source]#

Lazy wrapper - defers pandas import until called.

prefgraph.datasets.load_online_retail_ii(data_dir=None, n_customers=None, min_months=4, top_n_categories=30)[source]#

Load Online Retail II dataset as a BehaviorPanel.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing online_retail_II.csv. If None, searches standard locations.

  • n_customers (int | None) – Max number of customers to include (None = all).

  • min_months (int) – Minimum active months per customer (default 4).

  • top_n_categories (int) – Number of top products by frequency (default 30).

Returns:

BehaviorPanel with one BehaviorLog per customer. Metadata includes ‘cutoff_date’ for train/test splitting.

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_hm(data_dir=None, max_users=50000, min_periods=6, top_k_groups=20, cutoff_date='2020-06-01', time_period='month')[source]#

Load H&M Fashion dataset as a BehaviorPanel.

Reads transactions_train.csv in chunks for memory efficiency. Maps article_id to product groups (first 2 digits), aggregates to per-customer price-quantity panels.

Price construction: for purchased groups, the customer’s own average realized price is used. For unpurchased groups, prices are imputed via period-group median -> group median -> global median fallback.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing transactions_train.csv.

  • max_users (int) – Maximum number of customers (most active, default 50000).

  • min_periods (int) – Minimum active periods per customer (default 6).

  • top_k_groups (int) – Number of top product groups to keep (default 20).

  • cutoff_date (str) – ISO date for metadata (default ‘2020-06-01’).

  • time_period (str) – Aggregation period - “week”, “month” (default), or “quarter”.

Returns:

BehaviorPanel with one BehaviorLog per customer.

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_pakistan(data_dir=None, max_users=50000, min_months=5)[source]#

Load Pakistan E-Commerce dataset as a BehaviorPanel.

Filters to completed orders with positive price/quantity and non-null Customer ID. Aggregates to monthly periods: for each customer-month, quantity = total units ordered per category, price = median unit price per category that month (market-wide oracle).

All 16 product categories are used as goods.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing the CSV. If None, searches standard locations.

  • max_users (int) – Maximum number of customers to include (default 50,000).

  • min_months (int) – Minimum active months per customer (default 5).

Returns:

BehaviorPanel with one BehaviorLog per customer (rows = months, cols = product categories).

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_favorita(data_dir=None, min_weeks=50, max_stores=None)[source]#

Load Ecuador Favorita grocery dataset as a BehaviorPanel.

Each store is a “user”. Rows are weeks, columns are 33 product families. Quantities are total unit_sales per family per week. Prices are uniform ($1/unit) since individual prices are not in the dataset.

The 4.8 GB train.csv is read in 5M-row chunks to stay memory-friendly.

Parameters:
  • data_dir (str | Path | None) – Path to directory containing train.csv, items.csv, and stores.csv. If None, searches standard locations.

  • min_weeks (int) – Minimum weeks with nonzero sales required per store (default 50).

  • max_stores (int | None) – Optional cap on number of stores returned (None = all 54).

Returns:

BehaviorPanel with one BehaviorLog per store.

Raises:
Return type:

BehaviorPanel

prefgraph.datasets.load_taobao(*args, **kwargs)[source]#

Lazy wrapper - defers pandas import until called.

prefgraph.datasets.list_datasets()[source]#

List available datasets with descriptions.

Returns:

List of dicts with name, description, source, and size info.

Return type:

list[dict[str, str]]

Exceptions and Warnings#

PrefGraph provides custom exceptions that inherit from ValueError for backward compatibility.

Base Exception#

exception prefgraph.PrefGraphError[source]#

Bases: ValueError

Base exception for all PrefGraph errors.

Inherits from ValueError for backward compatibility - existing code that catches ValueError will continue to work.

Example

>>> try:
...     log = BehaviorLog(prices, quantities)
... except PrefGraphError as e:
...     print(f"PrefGraph error: {e}")
... except ValueError as e:  # Also catches PrefGraphError
...     print(f"Value error: {e}")

Data Validation Exceptions#

exception prefgraph.DataValidationError[source]#

Bases: PrefGraphError

Raised when input data fails validation checks.

This is the base class for all data-related validation errors. Use more specific subclasses when possible.

Common causes:
  • Mismatched array dimensions

  • Invalid value ranges

  • Missing or corrupted data

exception prefgraph.DimensionError[source]#

Bases: DataValidationError

Raised when array dimensions are incompatible.

Common causes:
  • cost_vectors and action_vectors have different shapes

  • Arrays are not 2D (T x N)

  • Empty arrays (T=0 or N=0)

Example

>>> prices = np.array([[1, 2, 3]])      # shape (1, 3)
>>> quantities = np.array([[1, 2]])     # shape (1, 2)
>>> BehaviorLog(prices, quantities)
DimensionError: cost_vectors shape (1, 3) does not match
action_vectors shape (1, 2)...
exception prefgraph.ValueRangeError[source]#

Bases: DataValidationError

Raised when values are outside expected ranges.

Common causes:
  • Negative or zero prices/costs

  • Negative quantities/actions

  • Probabilities outside [0, 1]

Example

>>> prices = np.array([[1, -2]])  # negative price!
>>> BehaviorLog(prices, quantities)
ValueRangeError: Found 1 non-positive costs at positions [(0, 1)]...
exception prefgraph.NaNInfError[source]#

Bases: DataValidationError

Raised when NaN or Inf values are detected in input data.

By default, PrefGraph raises this error when NaN/Inf values are found. Use nan_policy=’drop’ or nan_policy=’warn’ to handle them automatically.

Common causes:
  • Missing data encoded as NaN

  • Division by zero in preprocessing

  • Numeric overflow producing Inf

Example

>>> prices = np.array([[1, np.nan]])
>>> BehaviorLog(prices, quantities)
NaNInfError: Found 1 NaN/Inf values in 1 observations...
>>> # Solution: use nan_policy to handle automatically
>>> BehaviorLog(prices, quantities, nan_policy='drop')

Computation Exceptions#

exception prefgraph.OptimizationError[source]#

Bases: PrefGraphError

Raised when an optimization solver fails to find a solution.

This typically occurs when:
  • Data is too inconsistent for utility recovery

  • Linear programming constraints are infeasible

  • Numerical issues prevent convergence

Common causes:
  • Behavior has low integrity score (< 0.7)

  • Extreme values causing numerical instability

  • Too few observations for the operation

Suggested fixes:
  1. Check data quality first with compute_integrity_score()

  2. Filter highly inconsistent observations

  3. Scale data to avoid extreme values

exception prefgraph.NotFittedError[source]#

Bases: PrefGraphError

Raised when an operation requires a fitted model.

This occurs when calling transform or prediction methods on a PreferenceEncoder before fitting it.

Example

>>> encoder = PreferenceEncoder()
>>> encoder.transform(log)  # forgot to fit first!
NotFittedError: Encoder not fitted. Call fit() first...
exception prefgraph.InsufficientDataError[source]#

Bases: PrefGraphError

Raised when there is not enough data for the requested operation.

Some operations require minimum amounts of data:
  • At least 2 observations for preference comparisons

  • At least 3 observations for cycle detection

  • Multiple observations per group for separability testing

Example

>>> log = BehaviorLog(prices[:1], quantities[:1])  # only 1 obs
>>> compute_granular_integrity(log)
InsufficientDataError: Need at least 3 observations for VEI...

Warnings#

class prefgraph.DataQualityWarning[source]#

Bases: UserWarning

Warning for data quality issues that don’t prevent computation.

Emitted when:
  • Rows with NaN/Inf are dropped (nan_policy=’warn’)

  • Attribute matrix is rank-deficient

  • Characteristics have zero values across all products

These issues may affect results but don’t prevent computation. Address them for best results.

Example

>>> import warnings
>>> # Suppress data quality warnings
>>> warnings.filterwarnings('ignore', category=DataQualityWarning)
>>>
>>> # Or promote to errors
>>> warnings.filterwarnings('error', category=DataQualityWarning)
class prefgraph.NumericalInstabilityWarning[source]#

Bases: UserWarning

Warning for potential numerical issues in computations.

Emitted when:
  • Division involves near-zero denominators

  • Optimization converges slowly or hits iteration limit

  • Matrix operations may be ill-conditioned

Results may be less reliable when this warning appears. Consider adjusting tolerance parameters or scaling data.

Troubleshooting#

Common Errors

  • ValueRangeError: Found non-positive costs — All prices must be > 0. Check for zeros or missing data encoded as 0.

  • DimensionError: cost_vectors shape does not match — Prices and quantities must have the same shape (T x N).

  • NaNInfError: Found NaN/Inf values — Use nan_policy="drop" to automatically remove bad rows: rp.analyze(df, ..., nan_policy="drop") or BehaviorLog(..., nan_policy="drop").

  • InsufficientDataError: Must have at least 2 observations — Need T >= 2 for meaningful analysis.

  • ImportError: pandas is required — Install with pip install prefgraph[datasets] for dataset loaders.

Tips

  • For large panels, the first call may be slow due to Numba JIT compilation. Subsequent calls are fast.

  • If compute_integrity_score is slow for T > 500, the SCC-optimized path activates automatically.

  • Memory usage scales as O(T^2) per user due to the T x T revealed preference matrices.