| """ |
| First-Price Auction Simulator for RTB Bidding Benchmark |
| |
| Simulates a stream of first-price auctions with: |
| - Impression features (from Criteo_x4 for CTR) |
| - Click labels (ground truth from Criteo) |
| - Synthetic market prices (competing bids) conditioned on features |
| - Full information feedback (observes all competing bids) |
| |
| Used to evaluate bidding algorithms under controlled conditions. |
| """ |
| import numpy as np |
| from collections import defaultdict |
|
|
|
|
| class FirstPriceAuctionSimulator: |
| """ |
| Simulates repeated first-price auctions. |
| |
| In a first-price auction: |
| - Each bidder submits a sealed bid |
| - Highest bidder wins and pays their bid |
| - Losing bidders pay nothing |
| |
| We simulate the "advertiser" (our bidder) vs. a pool of competing bidders. |
| The maximum competing bid d_t follows a distribution conditioned on features. |
| """ |
| |
| def __init__( |
| self, |
| features, |
| pctr_true, |
| click_labels, |
| value_per_click=50.0, |
| market_price_config=None, |
| seed=42 |
| ): |
| """ |
| Args: |
| features: (N, D) impression feature matrix |
| pctr_true: (N,) true or predicted CTR values |
| click_labels: (N,) binary click labels |
| value_per_click: Value of each click in currency |
| market_price_config: Dict controlling price generation |
| seed: Random seed |
| """ |
| self.features = features |
| self.pctr_true = pctr_true |
| self.click_labels = click_labels |
| self.value_per_click = value_per_click |
| self.N = len(features) |
| self.rng = np.random.RandomState(seed) |
| |
| |
| self.market_prices = self._generate_market_prices(market_price_config or {}) |
| |
| |
| self.order = self.rng.permutation(self.N) |
| self.position = 0 |
| |
| def _generate_market_prices(self, config): |
| """ |
| Generate synthetic market prices (max competing bids). |
| |
| Prices are conditioned on features: higher-CTR impressions tend to |
| have higher competition (more bidders want them). |
| |
| Default: log-normal distribution with mean correlated to pCTR. |
| """ |
| base_mean = config.get('base_mean', 20.0) |
| ctr_correlation = config.get('ctr_correlation', 10.0) |
| noise_std = config.get('noise_std', 0.6) |
| price_multiplier = config.get('price_multiplier', 1.0) |
| |
| N = self.N |
| |
| |
| mean_prices = base_mean + ctr_correlation * self.pctr_true |
| mean_prices = np.clip(mean_prices, 1.0, 200.0) |
| |
| |
| prices = self.rng.lognormal( |
| mean=np.log(mean_prices), |
| sigma=noise_std, |
| size=N |
| ) |
| |
| |
| prices = prices * price_multiplier |
| |
| |
| if self.features.shape[1] > 1: |
| |
| feature_effect = ( |
| 0.02 * self.features[:, 0] + |
| 0.01 * self.features[:, 1] |
| ) |
| prices = prices * np.exp(feature_effect * 0.1) |
| |
| return np.clip(prices, 0.5, 500.0) |
| |
| def reset(self): |
| """Reset simulator to beginning of auction sequence.""" |
| self.position = 0 |
| self.order = self.rng.permutation(self.N) |
| |
| def next_auction(self): |
| """ |
| Return next auction: (features, pctr, true_click, market_price). |
| Returns None when exhausted. |
| """ |
| if self.position >= self.N: |
| return None |
| |
| idx = self.order[self.position] |
| self.position += 1 |
| |
| return ( |
| self.features[idx], |
| self.pctr_true[idx], |
| int(self.click_labels[idx]), |
| self.market_prices[idx] |
| ) |
| |
| def simulate_bid(self, bid): |
| """ |
| Simulate the outcome of placing a bid in the current auction. |
| |
| Must be called after next_auction(). |
| |
| Args: |
| bid: Bid price |
| |
| Returns: |
| (won, cost, click_occurred) |
| """ |
| _, _, click, market_price = ( |
| self.features[self.order[self.position - 1]], |
| self.pctr_true[self.order[self.position - 1]], |
| int(self.click_labels[self.order[self.position - 1]]), |
| self.market_prices[self.order[self.position - 1]] |
| ) |
| |
| won = bid >= market_price |
| cost = bid if won else 0.0 |
| click_occurred = click if won else 0 |
| |
| return won, cost, click_occurred, market_price |
| |
| def run_algorithm(self, algo, ctr_predictor=None): |
| """ |
| Run a bidding algorithm through the full auction sequence. |
| |
| Args: |
| algo: Bidding algorithm instance with .bid(pctr) and .update(won, cost, pctr, d_t) |
| ctr_predictor: Optional CTRPredictor. If None, uses self.pctr_true. |
| |
| Returns: |
| results dict with metrics |
| """ |
| self.reset() |
| |
| |
| if hasattr(algo, 'set_budget'): |
| algo.set_budget(algo.B if hasattr(algo, 'B') else 5000) |
| |
| metrics = defaultdict(list) |
| total_clicks = 0 |
| |
| while True: |
| auction = self.next_auction() |
| if auction is None: |
| break |
| |
| features, _, true_click, market_price = auction |
| |
| |
| if ctr_predictor is not None: |
| |
| pctr = ctr_predictor.predict_single({ |
| f'I{i+1}': features[i] for i in range(13) |
| } | { |
| f'C{i+1}': features[13+i] for i in range(26) |
| }) |
| else: |
| pctr = self.pctr_true[self.order[self.position - 1]] |
| |
| |
| bid = algo.bid(pctr, features) |
| bid = np.clip(bid, 0, algo.remaining_budget if hasattr(algo, 'remaining_budget') else float('inf')) |
| |
| |
| won, cost, click, d_t = self.simulate_bid(bid) |
| |
| if won: |
| total_clicks += click |
| |
| |
| algo.update(won, cost, pctr, d_t) |
| |
| |
| metrics['bids'].append(bid) |
| metrics['won'].append(int(won)) |
| metrics['cost'].append(cost) |
| metrics['pctr'].append(pctr) |
| metrics['market_price'].append(market_price) |
| |
| |
| results = algo.get_stats() |
| results.update({ |
| 'total_clicks': total_clicks, |
| 'total_impressions': len(metrics['bids']), |
| 'total_wins': sum(metrics['won']), |
| 'total_spent': sum(metrics['cost']), |
| 'ctr': total_clicks / max(sum(metrics['won']), 1), |
| 'budget_used_frac': sum(metrics['cost']) / algo.B if hasattr(algo, 'B') else 0, |
| 'cpc': sum(metrics['cost']) / max(total_clicks, 1), |
| 'avg_bid': np.mean(metrics['bids']), |
| 'win_rate': sum(metrics['won']) / max(len(metrics['won']), 1), |
| 'avg_market_price': np.mean(metrics['market_price']), |
| }) |
| |
| return results |
| |
| def run_comparison(self, algorithms, ctr_predictor=None): |
| """ |
| Run multiple algorithms on the same auction sequence and compare. |
| |
| Args: |
| algorithms: Dict of {name: algorithm_instance} |
| ctr_predictor: Shared CTR predictor |
| |
| Returns: |
| Dict of {name: results_dict} |
| """ |
| all_results = {} |
| |
| for name, algo in algorithms.items(): |
| print(f"\nRunning {name}...") |
| results = self.run_algorithm(algo, ctr_predictor) |
| all_results[name] = results |
| |
| print(f" Clicks: {results['total_clicks']}") |
| print(f" Spend: {results['total_spent']:.2f}") |
| print(f" Budget used: {results.get('budget_used_frac', 0):.1%}") |
| print(f" CPC: {results.get('cpc', 0):.2f}") |
| |
| return all_results |
|
|