UFC Sell-Through Project - Card Optimizer

Overview

The card optimizer builds optimal UFC fight cards using:

  1. Greedy Selection - Start with highest-scoring fights
  2. 2-opt Local Search - Iteratively improve by swapping fights

Data Structures

Fight Class

class Fight:
    def __init__(self, fight_id, fighter1, fighter2, weight_class, is_title=False):
        self.fight_id = fight_id
        self.fighter1 = fighter1  # dict with name and stats
        self.fighter2 = fighter2
        self.weight_class = weight_class
        self.is_title = is_title
        self.score = 0.0
    
    def __repr__(self):
        title_str = " [TITLE]" if self.is_title else ""
        return f"{self.fighter1['name']} vs {self.fighter2['name']} ({self.weight_class}){title_str}"

FightCard Class

class FightCard:
    def __init__(self, fights=None):
        self.fights = fights or []
        self.predicted_sellthrough = 0.0
    
    def add_fight(self, fight):
        self.fights.append(fight)
    
    def remove_fight(self, fight):
        self.fights.remove(fight)
    
    def get_all_fighters(self):
        fighters = set()
        for f in self.fights:
            fighters.add(f.fighter1['name'])
            fighters.add(f.fighter2['name'])
        return fighters
    
    def get_weight_classes(self):
        return {f.weight_class for f in self.fights}
    
    def count_title_fights(self):
        return sum(1 for f in self.fights if f.is_title)
    
    def is_valid(self, min_fights=10, max_fights=14, min_weight_classes=3):
        # Right number of fights?
        if len(self.fights) < min_fights or len(self.fights) > max_fights:
            return False
        
        # Enough variety?
        if len(self.get_weight_classes()) < min_weight_classes:
            return False
        
        # No fighter appears twice?
        fighters = []
        for f in self.fights:
            if f.fighter1['name'] in fighters or f.fighter2['name'] in fighters:
                return False
            fighters.extend([f.fighter1['name'], f.fighter2['name']])
        
        return True

Fight Scoring

def score_fight(fight, current_card_fights):
    """Score a fight based on attendance potential."""
    f1 = fight.fighter1
    f2 = fight.fighter2
    score = 0.0
    
    # Win rate - successful fighters have fans
    win_rate_1 = f1.get('win_rate', 0.5)
    win_rate_2 = f2.get('win_rate', 0.5)
    score += (win_rate_1 + win_rate_2) * 0.2
    
    # Experience - established fighters have fanbases
    exp_1 = min(f1.get('total_fights', 0) / 20, 1.0)
    exp_2 = min(f2.get('total_fights', 0) / 20, 1.0)
    score += (exp_1 + exp_2) * 0.15
    
    # Finish rate - exciting fighters
    finish_1 = f1.get('finish_rate', 0.3)
    finish_2 = f2.get('finish_rate', 0.3)
    score += (finish_1 + finish_2) * 0.15
    
    # Title fights are HUGE draws
    if fight.is_title:
        score += 0.3
    
    # Competitive matchups are more interesting
    competitiveness = 1.0 - abs(win_rate_1 - win_rate_2)
    score += competitiveness * 0.1
    
    # Bonus for new weight class (variety)
    current_weights = {f.weight_class for f in current_card_fights}
    if fight.weight_class not in current_weights:
        score += 0.1
    
    return score

Sell-Through Prediction

def calculate_card_sellthrough(card):
    """Estimate sell-through from fight scores."""
    if not card.fights:
        return 0.5
    
    total_score = sum(f.score for f in card.fights)
    
    # Base sell-through + quality bonus
    base = 0.75  # Most UFC events sell 70-80%
    bonus = min(total_score / 15, 0.25)  # Cap at 25%
    
    # Title fight bonus
    title_bonus = card.count_title_fights() * 0.02
    
    predicted = base + bonus + title_bonus
    return max(0.5, min(1.0, predicted))

Greedy Algorithm

def greedy_build_card(available_fights, min_fights=10, max_fights=14):
    """Build initial card using greedy selection."""
    
    card = FightCard()
    used_fighters = set()
    
    # Score all fights
    for fight in available_fights:
        fight.score = score_fight(fight, [])
    
    # Sort by score descending
    sorted_fights = sorted(available_fights, key=lambda f: f.score, reverse=True)
    
    # Greedily add best non-conflicting fights
    for fight in sorted_fights:
        if fight.fighter1['name'] in used_fighters:
            continue
        if fight.fighter2['name'] in used_fighters:
            continue
        
        card.add_fight(fight)
        used_fighters.add(fight.fighter1['name'])
        used_fighters.add(fight.fighter2['name'])
        
        if len(card.fights) >= max_fights:
            break
    
    card.predicted_sellthrough = calculate_card_sellthrough(card)
    return card
def two_opt_improve(card, available_fights, max_iterations=100):
    """Improve card by swapping fights."""
    
    best_card = card
    best_score = calculate_card_sellthrough(card)
    used_fighters = card.get_all_fighters()
    
    # Get unused fights
    unused_fights = [f for f in available_fights if f not in card.fights]
    unused_fights = [f for f in unused_fights 
                     if f.fighter1['name'] not in used_fighters 
                     and f.fighter2['name'] not in used_fighters]
    
    improved = True
    iteration = 0
    
    while improved and iteration < max_iterations:
        improved = False
        iteration += 1
        
        for i, current_fight in enumerate(best_card.fights):
            for new_fight in unused_fights:
                # Check if swap is valid
                test_fighters = used_fighters.copy()
                test_fighters.remove(current_fight.fighter1['name'])
                test_fighters.remove(current_fight.fighter2['name'])
                
                if (new_fight.fighter1['name'] in test_fighters or
                    new_fight.fighter2['name'] in test_fighters):
                    continue
                
                # Try the swap
                test_card = FightCard(best_card.fights.copy())
                test_card.fights[i] = new_fight
                
                if test_card.is_valid():
                    new_score = calculate_card_sellthrough(test_card)
                    
                    if new_score > best_score:
                        best_card = test_card
                        best_score = new_score
                        improved = True
                        
                        # Update used/unused lists
                        unused_fights.append(current_fight)
                        unused_fights.remove(new_fight)
                        used_fighters = best_card.get_all_fighters()
                        break
            
            if improved:
                break
    
    best_card.predicted_sellthrough = best_score
    return best_card

Running the Optimizer

def optimize_card(available_fights):
    """Full optimization pipeline."""
    
    print("Step 1: Greedy selection...")
    initial_card = greedy_build_card(available_fights)
    print(f"  Initial card: {len(initial_card.fights)} fights")
    print(f"  Initial sell-through: {initial_card.predicted_sellthrough:.1%}")
    
    print("\nStep 2: 2-opt improvement...")
    optimized_card = two_opt_improve(initial_card, available_fights)
    print(f"  Final sell-through: {optimized_card.predicted_sellthrough:.1%}")
    
    print("\nOptimized Fight Card:")
    for i, fight in enumerate(optimized_card.fights, 1):
        print(f"  {i}. {fight}")
    
    return optimized_card

Demo

python src/optimizer/card_optimizer.py

Constraints

Constraint Default Description
min_fights 10 Minimum fights on card
max_fights 14 Maximum fights on card
min_weight_classes 3 Variety requirement
No duplicate fighters - Fighter can only appear once