← back to Handbag Auth Nextjs

analytics/generate_sample_data.py

310 lines

#!/usr/bin/env python3
"""
Generate realistic sample auction data for testing analytics
Creates historical data with price trends, seasonality, and brand variations
"""

import sqlite3
import random
from datetime import datetime, timedelta
import os

# Configuration
PROJECT_DIR = '/root/Projects/handbag-auth-nextjs'
DB_PATH = os.path.join(PROJECT_DIR, 'data', 'auction-history', 'auctions.db')

# Brand configurations with realistic price ranges and trends
BRAND_CONFIG = {
    "Hermes Birkin": {
        "base_price": 12000,
        "volatility": 0.3,
        "trend": 0.15,  # 15% annual appreciation
        "auction_houses": ["Christie's", "Sotheby's", "Heritage Auctions"],
        "frequency": 50  # relative frequency
    },
    "Hermes Kelly": {
        "base_price": 10000,
        "volatility": 0.25,
        "trend": 0.12,
        "auction_houses": ["Christie's", "Sotheby's", "Heritage Auctions"],
        "frequency": 45
    },
    "Chanel Classic Flap": {
        "base_price": 5000,
        "volatility": 0.2,
        "trend": 0.10,
        "auction_houses": ["Christie's", "Sotheby's", "Heritage Auctions", "Bonhams"],
        "frequency": 80
    },
    "Chanel Boy": {
        "base_price": 3500,
        "volatility": 0.25,
        "trend": 0.08,
        "auction_houses": ["Christie's", "Sotheby's", "Heritage Auctions", "Bonhams"],
        "frequency": 60
    },
    "Chanel 19": {
        "base_price": 3000,
        "volatility": 0.3,
        "trend": 0.05,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 40
    },
    "Louis Vuitton Neverfull": {
        "base_price": 800,
        "volatility": 0.15,
        "trend": 0.03,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 100
    },
    "Louis Vuitton Speedy": {
        "base_price": 700,
        "volatility": 0.15,
        "trend": 0.03,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 90
    },
    "Louis Vuitton Pochette": {
        "base_price": 600,
        "volatility": 0.2,
        "trend": 0.02,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 70
    },
    "Dior Lady Dior": {
        "base_price": 3000,
        "volatility": 0.2,
        "trend": 0.07,
        "auction_houses": ["Christie's", "Sotheby's", "Heritage Auctions"],
        "frequency": 55
    },
    "Dior Saddle": {
        "base_price": 2500,
        "volatility": 0.25,
        "trend": 0.12,  # Trending up due to revival
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 65
    },
    "Gucci Dionysus": {
        "base_price": 1500,
        "volatility": 0.2,
        "trend": 0.05,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 70
    },
    "Gucci Marmont": {
        "base_price": 1200,
        "volatility": 0.2,
        "trend": 0.04,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 75
    },
    "Prada Galleria": {
        "base_price": 1800,
        "volatility": 0.18,
        "trend": 0.03,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 50
    },
    "Prada Re-Edition": {
        "base_price": 900,
        "volatility": 0.25,
        "trend": 0.08,  # Popular vintage piece
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 60
    },
    "Bottega Veneta Cassette": {
        "base_price": 2000,
        "volatility": 0.3,
        "trend": 0.10,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 45
    },
    "Bottega Veneta Jodie": {
        "base_price": 1800,
        "volatility": 0.3,
        "trend": 0.09,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 40
    },
    "Celine Luggage": {
        "base_price": 2200,
        "volatility": 0.2,
        "trend": 0.04,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 50
    },
    "Celine Triomphe": {
        "base_price": 1500,
        "volatility": 0.2,
        "trend": 0.06,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 45
    },
    "Saint Laurent Kate": {
        "base_price": 1200,
        "volatility": 0.2,
        "trend": 0.05,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 55
    },
    "Balenciaga City": {
        "base_price": 1000,
        "volatility": 0.25,
        "trend": 0.06,
        "auction_houses": ["Heritage Auctions", "Bonhams", "LiveAuctioneers"],
        "frequency": 60
    },
    "Fendi Baguette": {
        "base_price": 1500,
        "volatility": 0.2,
        "trend": 0.07,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 50
    },
    "Goyard St Louis": {
        "base_price": 1800,
        "volatility": 0.25,
        "trend": 0.05,
        "auction_houses": ["Christie's", "Heritage Auctions", "Bonhams"],
        "frequency": 40
    }
}

def calculate_price(brand_config, days_ago, is_estimate=False):
    """Calculate realistic price with trend, seasonality, and noise"""
    base = brand_config['base_price']
    trend = brand_config['trend']
    volatility = brand_config['volatility']

    # Time-based trend (annual appreciation)
    years = days_ago / 365.0
    trend_factor = 1 - (trend * years)  # Older items sold for less

    # Seasonal variation (luxury auctions peak in Nov/Dec and May/June)
    date = datetime.now() - timedelta(days=days_ago)
    month = date.month
    seasonal_boost = 0
    if month in [11, 12]:  # Holiday season
        seasonal_boost = 0.05
    elif month in [5, 6]:  # Spring auction season
        seasonal_boost = 0.03

    # Random variation
    noise = random.gauss(0, volatility)

    # Calculate price
    price = base * trend_factor * (1 + seasonal_boost + noise)

    # Estimates are typically 10-30% higher than hammer price
    if is_estimate:
        estimate_markup = random.uniform(1.1, 1.3)
        price *= estimate_markup

    return max(100, round(price, 2))

def generate_auction_data(num_auctions=1000, days_back=365):
    """Generate realistic auction data"""
    auctions = []

    # Create weighted brand list based on frequency
    brand_list = []
    for brand, config in BRAND_CONFIG.items():
        brand_list.extend([brand] * config['frequency'])

    for i in range(num_auctions):
        # Select random brand
        brand = random.choice(brand_list)
        config = BRAND_CONFIG[brand]

        # Random date in the past
        days_ago = random.randint(0, days_back)
        end_date = datetime.now() - timedelta(days=days_ago)

        # Random auction house based on brand preferences
        auction_house = random.choice(config['auction_houses'])

        # Generate prices
        hammer_price = calculate_price(config, days_ago, is_estimate=False)
        estimate_low = calculate_price(config, days_ago, is_estimate=True)
        estimate_high = estimate_low * random.uniform(1.15, 1.35)

        # Sometimes the item sells below estimate (good deal!)
        # Sometimes it exceeds estimate (hot item)
        if random.random() < 0.6:  # 60% sell within or below estimate
            hammer_price = estimate_low * random.uniform(0.7, 1.0)
        else:  # 40% exceed low estimate
            hammer_price = estimate_low * random.uniform(1.0, 1.3)

        # Generate auction details
        auction = {
            'auction_id': f"AUC{i+1:06d}",
            'title': f"{brand} - {random.choice(['Vintage', 'Classic', 'Rare', 'Limited Edition', 'Designer'])} - {random.choice(['Black', 'Brown', 'Tan', 'Navy', 'Red', 'Burgundy'])}",
            'auction_house': auction_house,
            'current_price': round(hammer_price, 2),
            'estimate_low': round(estimate_low, 2),
            'estimate_high': round(estimate_high, 2),
            'end_date': end_date.strftime('%Y-%m-%d'),
            'lot_number': f"LOT{random.randint(100, 999)}",
            'url': f"https://example.com/auction/{i+1}",
            'search_term': brand,
            'created_at': end_date.strftime('%Y-%m-%d %H:%M:%S')
        }

        auctions.append(auction)

    return auctions

def insert_sample_data(auctions):
    """Insert sample data into database"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()

    # Clear existing data
    cursor.execute("DELETE FROM auctions")

    # Insert sample data
    for auction in auctions:
        cursor.execute("""
            INSERT INTO auctions
            (auction_id, title, auction_house, current_price, estimate_low,
             estimate_high, end_date, lot_number, url, search_term, created_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            auction['auction_id'],
            auction['title'],
            auction['auction_house'],
            auction['current_price'],
            auction['estimate_low'],
            auction['estimate_high'],
            auction['end_date'],
            auction['lot_number'],
            auction['url'],
            auction['search_term'],
            auction['created_at']
        ))

    conn.commit()
    conn.close()

    print(f"Inserted {len(auctions)} sample auctions")

def main():
    print("Generating sample auction data...")

    # Generate 2000 auctions over the past 2 years
    auctions = generate_auction_data(num_auctions=2000, days_back=730)

    print(f"Generated {len(auctions)} auctions")
    print(f"Date range: {min(a['end_date'] for a in auctions)} to {max(a['end_date'] for a in auctions)}")
    print(f"Price range: ${min(a['current_price'] for a in auctions):.2f} to ${max(a['current_price'] for a in auctions):.2f}")

    # Insert into database
    insert_sample_data(auctions)

    print("Sample data generation complete!")
    print(f"Database: {DB_PATH}")

if __name__ == "__main__":
    main()