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.
- Returns:
pandas DataFrame (default) or list of result objects.
- Return type:
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:
objectAnalyzes 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'}#
- analyze_arrays(users, data_type='budget')[source]#
Analyze users from a list of array pairs.
- Parameters:
- Return type:
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
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)
- 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:
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_pathis set).- Return type:
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:
objectResult 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 toNone(notFalse/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)
ccei (float)
mpi (float)
is_harp (bool | None)
hm_consistent (int | None)
hm_total (int | None)
utility_success (bool | None)
vei_mean (float)
vei_min (float)
vei_exact_mean (float)
vei_exact_min (float)
max_scc (int)
compute_time_us (int)
vei_std (float)
vei_q25 (float)
vei_q75 (float)
vei_exact_std (float)
vei_exact_q25 (float)
vei_exact_q75 (float)
n_scc (int)
harp_severity (float)
scc_mean_size (float)
r_density (float)
r_out_degree_std (float)
degree_gini (float)
ew_mean (float)
ew_std (float)
ew_skew (float)
- __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:
is_garp (bool)
n_violations (int)
ccei (float)
mpi (float)
is_harp (bool | None)
hm_consistent (int | None)
hm_total (int | None)
utility_success (bool | None)
vei_mean (float)
vei_min (float)
vei_exact_mean (float)
vei_exact_min (float)
max_scc (int)
compute_time_us (int)
vei_std (float)
vei_q25 (float)
vei_q75 (float)
vei_exact_std (float)
vei_exact_q25 (float)
vei_exact_q75 (float)
n_scc (int)
harp_severity (float)
scc_mean_size (float)
r_density (float)
r_out_degree_std (float)
degree_gini (float)
ew_mean (float)
ew_std (float)
ew_skew (float)
- Return type:
None
High-Level Classes#
BehavioralAuditor#
- class prefgraph.BehavioralAuditor(precision=1e-06)[source]#
Bases:
objectValidates 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:
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:
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:
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:
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")
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:
Example
>>> if auditor.validate_menu_history(menu_log): ... print("Menu choices are consistent")
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 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:
Example
>>> score = auditor.get_menu_efficiency_score(menu_log) >>> if score < 0.9: ... print("Some inconsistent choices detected")
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}")
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:
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:
Example
>>> auditor = BehavioralAuditor() >>> summary = auditor.summary(user_log) >>> print(summary.summary()) # Print formatted report >>> summary.score() # Get aggregate score
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:
objectComprehensive audit report for user behavior.
- Variables:
- Parameters:
PreferenceEncoder#
- class prefgraph.PreferenceEncoder(precision=1e-08)[source]#
Bases:
objectEncodes 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:
Example
>>> encoder = PreferenceEncoder().fit(user_log)
- 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:
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:
- 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:
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:
- Returns:
Predicted action vector, or None if prediction failed
- Raises:
ValueError – If not fitted
- Return type:
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
- 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:
ResultDisplayMixinUnified 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)
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#
- aei_result: AEIResult#
- mpi_result: MPIResult#
- score()[source]#
Return aggregate scikit-learn style score in [0, 1].
Combines AEI and (1 - MPI) with equal weighting.
- Return type:
- 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:
- 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:
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)
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:
ResultDisplayMixinAggregate 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]#
- classmethod from_summaries(user_summaries, period_map=None)[source]#
Build PanelSummary from per-user BehavioralSummary results.
- Parameters:
- Return type:
- __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)#
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:
objectRepresents 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:
cost_vectors (ndarray[tuple[Any, ...], dtype[float64]] | None)
action_vectors (ndarray[tuple[Any, ...], dtype[float64]] | None)
user_id (str | None)
nan_policy (Literal['raise', 'warn', 'drop'])
quantities (ndarray[tuple[Any, ...], dtype[float64]] | None)
session_id (str | None)
_expenditure_matrix (ndarray[tuple[Any, ...], dtype[float64]] | None)
- 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)
- 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 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).
- 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:
- Returns:
BehaviorLog instance
- Return type:
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:
- 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:
- 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:
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:
cost_vectors (ndarray[tuple[Any, ...], dtype[float64]] | None)
action_vectors (ndarray[tuple[Any, ...], dtype[float64]] | None)
user_id (str | None)
nan_policy (Literal['raise', 'warn', 'drop'])
quantities (ndarray[tuple[Any, ...], dtype[float64]] | None)
session_id (str | None)
_expenditure_matrix (ndarray[tuple[Any, ...], dtype[float64]] | None)
- Return type:
None
BehaviorPanel#
- class prefgraph.BehaviorPanel(_logs, metadata=<factory>, _period_map=None)[source]#
Bases:
objectMulti-user panel of BehaviorLog objects.
Holds a collection of BehaviorLog objects indexed by user_id, supporting iteration, filtering, and aggregate analysis.
- Variables:
- 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())
- 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:
- classmethod from_dict(logs)[source]#
Create panel from a dict of user_id -> BehaviorLog.
- Parameters:
- Return type:
- 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:
- get_period(period)[source]#
Return panel filtered to a single period.
- Parameters:
period (str)
- Return type:
- analyze_user(user_id)[source]#
Run full behavioral analysis on a single user.
- Parameters:
user_id (str)
- Return type:
- summary(include_warp=True, include_sarp=True, include_power=False)[source]#
Run analysis on all users and return aggregate PanelSummary.
- Parameters:
- Return type:
- filter(predicate)[source]#
Return a new panel containing only logs that satisfy predicate.
- Parameters:
predicate (Callable[['BehaviorLog'], bool])
- Return type:
RiskChoiceLog#
- class prefgraph.RiskChoiceLog(safe_values, risky_outcomes, risky_probabilities, choices, session_id=None, metadata=<factory>)[source]#
Bases:
objectRepresents 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)
- 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).
- 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:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
- Return type:
WARPResult
- prefgraph.validate_sarp(session, tolerance=1e-10)#
Validate SARP (no indifferent preference cycles).
- Parameters:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
max_iterations (int)
method (str)
- 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:
session (BehaviorLog)
tolerance (float)
method (str)
- 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:
session (BehaviorLog)
tolerance (float)
method (str)
- 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:
session (BehaviorLog)
tolerance (float)
efficiency_threshold (float)
- 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:
session (BehaviorLog)
n_simulations (int)
tolerance (float)
random_seed (int | None)
store_simulation_values (bool)
- 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:
session (BehaviorLog)
tolerance (float)
- 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:
session (BehaviorLog)
tolerance (float)
max_cycle_length (int)
- 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")
- 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:
session (BehaviorLog)
good_g (int)
good_h (int)
price_change_threshold (float)
tolerance (float)
- 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:
session (BehaviorLog)
price_change_threshold (float)
- 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:
session (BehaviorLog)
tolerance (float)
- 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)
- 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)
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}")
- 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.
- 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.
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:
- 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.
- 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:
- 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:
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:
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:
- 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|/ 2This 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:
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:
- Returns:
List of sets, each containing good indices in a separable group
- Return type:
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:
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:
- 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:
session (BehaviorLog)
price_change_threshold (float)
tolerance (float)
- Return type:
CompensatedDemandResult
- prefgraph.estimate_compensated_demand(session, target_utility=None, method='exact')#
Estimate compensated (Hicksian) demand.
Alias for compute_hicksian_demand.
- Parameters:
session (BehaviorLog)
target_utility (float | None)
method (str)
- Return type:
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:
- 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}")
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:
objectRepresents 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
Number of distinct menus T.
- get_choice_probability(menu_idx, item)[source]#
Get probability of choosing item from menu at menu_idx.
- classmethod from_repeated_choices(menus, choices)[source]#
Create StochasticChoiceLog from repeated deterministic choice data.
Groups observations by menu and computes choice frequencies.
- 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>)#
- 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:
- 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:
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:
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:
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:
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:
objectRepresents 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.])
- 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)#
- 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:
- 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:
- 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:
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:
- 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:
- 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:
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:
- Returns:
List of (prices T*K, quantities T*K) tuples ready for Engine.analyze_arrays(). If return_panel=True, returns a BehaviorPanel.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
- Return type:
- 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:
- Returns:
BehaviorPanel with one BehaviorLog per user.
- Return type:
- 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:
- Returns:
BehaviorPanel with one BehaviorLog per customer.
- Return type:
- 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:
- Returns:
BehaviorPanel with one BehaviorLog per user.
- Return type:
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:
- 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:
- Returns:
Dict mapping session_id (str) -> MenuChoiceLog.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
ValueError – If aggregation is not “store” or “store_dept”.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
ValueError – If time_period is invalid.
- Return type:
- 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:
- Returns:
BehaviorPanel with one BehaviorLog per customer (rows = months, cols = product categories).
- Raises:
FileNotFoundError – If data file cannot be found.
ImportError – If pandas is not installed.
- Return type:
- 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:
FileNotFoundError – If data files cannot be found.
ImportError – If pandas is not installed.
- Return type:
Exceptions and Warnings#
PrefGraph provides custom exceptions that inherit from ValueError for
backward compatibility.
Base Exception#
- exception prefgraph.PrefGraphError[source]#
Bases:
ValueErrorBase 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:
PrefGraphErrorRaised 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:
DataValidationErrorRaised 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:
DataValidationErrorRaised 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:
DataValidationErrorRaised 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:
PrefGraphErrorRaised 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:
Check data quality first with compute_integrity_score()
Filter highly inconsistent observations
Scale data to avoid extreme values
- exception prefgraph.NotFittedError[source]#
Bases:
PrefGraphErrorRaised 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:
PrefGraphErrorRaised 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:
UserWarningWarning 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:
UserWarningWarning 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— Usenan_policy="drop"to automatically remove bad rows:rp.analyze(df, ..., nan_policy="drop")orBehaviorLog(..., nan_policy="drop").InsufficientDataError: Must have at least 2 observations— Need T >= 2 for meaningful analysis.ImportError: pandas is required— Install withpip 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_scoreis 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.