mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
import numpy as np
|
|
import pandas as pd
|
|
from procesing.pricers.base import PricingFunction
|
|
|
|
|
|
def session_features_to_demand(session_features: pd.DataFrame) -> float:
|
|
"""
|
|
Map session behavioral features to demand proxy.
|
|
THIS is the critical θ̂ → D transformation for rule-based pricing.
|
|
|
|
Logic:
|
|
- High velocity → agent behavior → price up (revenue recovery)
|
|
- High cart ratio → purchase intent → price up
|
|
- Low activity → discount to convert
|
|
|
|
Returns: demand proxy score (0-20 range, higher = more demand)
|
|
"""
|
|
if session_features.empty:
|
|
return 1.0
|
|
|
|
feat = session_features.iloc[0] if len(session_features) > 0 else {}
|
|
|
|
velocity = feat.get('interaction_velocity', 0)
|
|
cart_ratio = feat.get('cart_to_view_ratio', 0)
|
|
item_views = feat.get('item_views', 0)
|
|
cart_adds = feat.get('cart_adds', 0)
|
|
|
|
# baseline demand
|
|
demand = 1.0
|
|
|
|
# agent detection: high velocity → treat as high "demand" to price up
|
|
if velocity > 2.0:
|
|
demand += 10.0 # strong agent signal
|
|
|
|
# conversion intent: cart interaction → price up
|
|
if cart_ratio > 0.1 or cart_adds > 0:
|
|
demand += 5.0
|
|
|
|
# browsing depth: many views → interest signal
|
|
if item_views > 3:
|
|
demand += min(item_views, 5.0)
|
|
|
|
return min(demand, 20.0) # cap at 20
|
|
|
|
|
|
class StaticPricer(PricingFunction):
|
|
"""Static pricing: always return fixed base prices"""
|
|
|
|
def __init__(self, base_prices: np.ndarray = None):
|
|
self.base_prices = base_prices
|
|
|
|
def fit(self, historical_data: pd.DataFrame):
|
|
"""Extract base prices from historical data"""
|
|
if 'base_price' in historical_data.columns:
|
|
self.base_prices = historical_data['base_price'].values
|
|
elif 'price' in historical_data.columns:
|
|
self.base_prices = historical_data['price'].values
|
|
else:
|
|
raise ValueError("historical_data must contain 'base_price' or 'price' column")
|
|
return self
|
|
|
|
def predict(self, state_space) -> np.ndarray:
|
|
"""Return static base prices regardless of state"""
|
|
if self.base_prices is None:
|
|
raise ValueError("Must call fit() or provide base_prices in constructor")
|
|
return self.base_prices.copy()
|
|
|
|
def _get_features(self, state_space=None) -> np.ndarray:
|
|
"""Static pricer uses no features, returns empty array"""
|
|
n = len(self.base_prices) if self.base_prices is not None else 0
|
|
return np.zeros((n, 0))
|
|
|
|
|
|
class RandomPricer(PricingFunction):
|
|
"""Random pricing within bounds (for baseline comparison)"""
|
|
|
|
def __init__(self, price_min: float = 50.0, price_max: float = 500.0, seed: int = None):
|
|
self.price_min = price_min
|
|
self.price_max = price_max
|
|
self.seed = seed
|
|
self.n_products = None
|
|
self.rng = np.random.default_rng(seed)
|
|
|
|
def fit(self, historical_data: pd.DataFrame):
|
|
"""Learn number of products"""
|
|
self.n_products = len(historical_data)
|
|
return self
|
|
|
|
def predict(self, state_space) -> np.ndarray:
|
|
"""Generate random prices"""
|
|
if self.n_products is None:
|
|
self.n_products = len(state_space.demand)
|
|
return self.rng.uniform(self.price_min, self.price_max, size=self.n_products)
|
|
|
|
def _get_features(self, state_space=None) -> np.ndarray:
|
|
"""Random pricer uses no features"""
|
|
n = self.n_products if self.n_products else 0
|
|
return np.zeros((n, 0))
|
|
|
|
|
|
class SimpleSurgePricer(PricingFunction):
|
|
"""
|
|
Rule-based surge pricer adjusting prices via demand thresholds.
|
|
Logic: if demand > high_threshold -> surge, if demand < low_threshold -> discount.
|
|
Simpler and more controllable than curve fitting approaches.
|
|
"""
|
|
|
|
def __init__(self,
|
|
base_prices: np.ndarray = None,
|
|
high_threshold: int = 10,
|
|
low_threshold: int = 2,
|
|
surge_multiplier: float = 1.2,
|
|
discount_multiplier: float = 0.9):
|
|
self.base_prices = base_prices
|
|
self.high_threshold = high_threshold
|
|
self.low_threshold = low_threshold
|
|
self.surge_multiplier = surge_multiplier
|
|
self.discount_multiplier = discount_multiplier
|
|
|
|
def fit(self, market_data: pd.DataFrame):
|
|
"""Extract base prices from product catalog or historical averages"""
|
|
self.base_prices = market_data['base_price'].to_numpy() if 'base_price' in market_data.columns else market_data['price'].values
|
|
return self
|
|
|
|
def predict(self, state_space) -> np.ndarray:
|
|
"""
|
|
Adjust prices based on current demand using surge rules.
|
|
state_space.demand: demand proxy per product (from session features)
|
|
state_space.prices: base prices
|
|
"""
|
|
demand = np.asarray(state_space.demand) if state_space and hasattr(state_space, 'demand') else np.array([0])
|
|
base = np.asarray(state_space.prices) if state_space and hasattr(state_space, 'prices') else self.base_prices
|
|
|
|
if base is None:
|
|
base = np.ones(len(demand)) * 99.99
|
|
|
|
# ensure float dtype to allow multiplication by float multipliers
|
|
new_prices = base.astype(np.float64).copy()
|
|
high_mask = demand >= self.high_threshold
|
|
new_prices[high_mask] *= self.surge_multiplier
|
|
|
|
low_mask = demand <= self.low_threshold
|
|
new_prices[low_mask] *= self.discount_multiplier
|
|
|
|
return new_prices
|
|
|
|
def _get_features(self, state_space=None) -> np.ndarray:
|
|
"""Extract demand and base price features for each product"""
|
|
if state_space is None:
|
|
n = len(self.base_prices) if self.base_prices is not None else 0
|
|
return np.zeros((n, 2))
|
|
|
|
demand = np.asarray(state_space.demand) if hasattr(state_space, 'demand') else np.array([0])
|
|
base = np.asarray(state_space.prices) if hasattr(state_space, 'prices') else self.base_prices
|
|
if base is None:
|
|
base = np.ones(len(demand)) * 99.99
|
|
|
|
return np.column_stack([demand, base])
|