Files

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])