← Files Bigdata.comARCHIVED FILE

skills/bigdata-peer-comparables/scripts/peer_comparables.py

17.7 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

#!/usr/bin/env python3
"""
Peer Comparables Analysis

This module builds comprehensive comparable company analysis tables including:
- EV/EBITDA, EV/Revenue, and P/E multiples for each peer
- Statistical analysis (mean, median, percentiles)
- Percentile ranking for the target company
- Implied valuation range based on peer multiples

Author: Equity Analyst Skill
"""

from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
import statistics


@dataclass
class PeerData:
    """Financial data for a single peer company."""
    name: str
    ticker: str
    enterprise_value: float    # Enterprise value
    market_cap: float          # Market capitalization
    ebitda: float              # Trailing EBITDA
    revenue: float             # Trailing revenue
    net_income: float          # Trailing net income
    shares_outstanding: float  # Shares outstanding
    growth_rate: Optional[float] = None  # Revenue growth rate (optional)


@dataclass
class TargetMetrics:
    """Financial metrics for the target company."""
    name: str
    ticker: str
    enterprise_value: float
    market_cap: float
    ebitda: float
    revenue: float
    net_income: float
    shares_outstanding: float
    current_price: float
    growth_rate: Optional[float] = None


@dataclass
class ComparableOutput:
    """Output from comparable analysis."""
    peer_table: List[Dict]         # List of peer data with calculated multiples
    target_multiples: Dict         # Target company's multiples
    percentile_rankings: Dict      # Target's percentile ranking for each metric
    implied_valuations: Dict       # Implied valuations based on peer multiples
    summary_stats: Dict            # Summary statistics for peer group


def calculate_multiples(
    ev: float,
    market_cap: float,
    ebitda: float,
    revenue: float,
    net_income: float,
    shares: float
) -> Dict[str, Optional[float]]:
    """
    Calculate valuation multiples for a company.

    Args:
        ev: Enterprise value
        market_cap: Market capitalization
        ebitda: EBITDA
        revenue: Revenue
        net_income: Net income
        shares: Shares outstanding

    Returns:
        Dictionary with calculated multiples
    """
    # EV/EBITDA
    ev_ebitda = ev / ebitda if ebitda > 0 else None

    # EV/Revenue
    ev_revenue = ev / revenue if revenue > 0 else None

    # P/E (using market cap / net income, or price / EPS)
    pe_ratio = market_cap / net_income if net_income > 0 else None

    # Price per share (if shares provided)
    price = market_cap / shares if shares > 0 else None

    # EPS
    eps = net_income / shares if shares > 0 else None

    return {
        'ev_ebitda': ev_ebitda,
        'ev_revenue': ev_revenue,
        'pe_ratio': pe_ratio,
        'price': price,
        'eps': eps
    }


def calculate_percentile(value: float, distribution: List[float]) -> float:
    """
    Calculate the percentile ranking of a value within a distribution.

    Args:
        value: The value to rank
        distribution: List of values to compare against

    Returns:
        Percentile ranking (0-100)
    """
    if not distribution:
        return 50.0

    sorted_dist = sorted(distribution)
    below = sum(1 for v in sorted_dist if v < value)
    equal = sum(1 for v in sorted_dist if v == value)

    percentile = (below + 0.5 * equal) / len(sorted_dist) * 100
    return percentile


def calculate_summary_stats(values: List[float]) -> Dict[str, float]:
    """
    Calculate summary statistics for a list of values.

    Args:
        values: List of numeric values

    Returns:
        Dictionary with mean, median, min, max, 25th/75th percentiles
    """
    if not values:
        return {
            'mean': 0,
            'median': 0,
            'min': 0,
            'max': 0,
            'p25': 0,
            'p75': 0,
            'count': 0
        }

    sorted_values = sorted(values)
    n = len(sorted_values)

    # Calculate percentiles
    p25_idx = int(n * 0.25)
    p75_idx = int(n * 0.75)

    return {
        'mean': statistics.mean(values),
        'median': statistics.median(values),
        'min': min(values),
        'max': max(values),
        'p25': sorted_values[p25_idx] if n > 0 else 0,
        'p75': sorted_values[min(p75_idx, n-1)] if n > 0 else 0,
        'count': n
    }


def build_comp_table(
    target: TargetMetrics,
    peers: List[PeerData]
) -> ComparableOutput:
    """
    Build a comprehensive comparable company analysis.

    Args:
        target: Target company metrics
        peers: List of peer company data

    Returns:
        ComparableOutput with full analysis
    """
    # Calculate multiples for all peers
    peer_table = []
    ev_ebitda_values = []
    ev_revenue_values = []
    pe_values = []

    for peer in peers:
        multiples = calculate_multiples(
            ev=peer.enterprise_value,
            market_cap=peer.market_cap,
            ebitda=peer.ebitda,
            revenue=peer.revenue,
            net_income=peer.net_income,
            shares=peer.shares_outstanding
        )

        peer_entry = {
            'name': peer.name,
            'ticker': peer.ticker,
            'ev': peer.enterprise_value,
            'market_cap': peer.market_cap,
            'ebitda': peer.ebitda,
            'revenue': peer.revenue,
            'net_income': peer.net_income,
            'ev_ebitda': multiples['ev_ebitda'],
            'ev_revenue': multiples['ev_revenue'],
            'pe_ratio': multiples['pe_ratio'],
            'growth_rate': peer.growth_rate
        }
        peer_table.append(peer_entry)

        # Collect valid multiples for statistics
        if multiples['ev_ebitda'] is not None:
            ev_ebitda_values.append(multiples['ev_ebitda'])
        if multiples['ev_revenue'] is not None:
            ev_revenue_values.append(multiples['ev_revenue'])
        if multiples['pe_ratio'] is not None:
            pe_values.append(multiples['pe_ratio'])

    # Calculate target multiples
    target_multiples = calculate_multiples(
        ev=target.enterprise_value,
        market_cap=target.market_cap,
        ebitda=target.ebitda,
        revenue=target.revenue,
        net_income=target.net_income,
        shares=target.shares_outstanding
    )
    target_multiples['name'] = target.name
    target_multiples['ticker'] = target.ticker
    target_multiples['current_price'] = target.current_price

    # Calculate percentile rankings for target
    percentile_rankings = {}
    if target_multiples['ev_ebitda'] is not None and ev_ebitda_values:
        percentile_rankings['ev_ebitda'] = calculate_percentile(
            target_multiples['ev_ebitda'], ev_ebitda_values
        )
    if target_multiples['ev_revenue'] is not None and ev_revenue_values:
        percentile_rankings['ev_revenue'] = calculate_percentile(
            target_multiples['ev_revenue'], ev_revenue_values
        )
    if target_multiples['pe_ratio'] is not None and pe_values:
        percentile_rankings['pe_ratio'] = calculate_percentile(
            target_multiples['pe_ratio'], pe_values
        )

    # Calculate summary statistics
    summary_stats = {
        'ev_ebitda': calculate_summary_stats(ev_ebitda_values),
        'ev_revenue': calculate_summary_stats(ev_revenue_values),
        'pe_ratio': calculate_summary_stats(pe_values)
    }

    # Calculate implied valuations for target
    implied_valuations = calculate_implied_valuations(
        target=target,
        summary_stats=summary_stats
    )

    return ComparableOutput(
        peer_table=peer_table,
        target_multiples=target_multiples,
        percentile_rankings=percentile_rankings,
        implied_valuations=implied_valuations,
        summary_stats=summary_stats
    )


def calculate_implied_valuations(
    target: TargetMetrics,
    summary_stats: Dict
) -> Dict:
    """
    Calculate implied valuations for target based on peer multiples.

    Args:
        target: Target company metrics
        summary_stats: Peer group summary statistics

    Returns:
        Dictionary with implied valuations
    """
    implied = {}

    # EV/EBITDA implied valuation
    if summary_stats['ev_ebitda']['count'] > 0 and target.ebitda > 0:
        ev_ebitda_stats = summary_stats['ev_ebitda']
        implied_ev_low = target.ebitda * ev_ebitda_stats['p25']
        implied_ev_median = target.ebitda * ev_ebitda_stats['median']
        implied_ev_high = target.ebitda * ev_ebitda_stats['p75']

        # Convert to equity value (EV - Net Debt = Equity)
        net_debt = target.enterprise_value - target.market_cap

        implied['ev_ebitda'] = {
            'low': (implied_ev_low - net_debt) / target.shares_outstanding,
            'median': (implied_ev_median - net_debt) / target.shares_outstanding,
            'high': (implied_ev_high - net_debt) / target.shares_outstanding,
            'multiple_low': ev_ebitda_stats['p25'],
            'multiple_median': ev_ebitda_stats['median'],
            'multiple_high': ev_ebitda_stats['p75']
        }

    # EV/Revenue implied valuation
    if summary_stats['ev_revenue']['count'] > 0 and target.revenue > 0:
        ev_rev_stats = summary_stats['ev_revenue']
        implied_ev_low = target.revenue * ev_rev_stats['p25']
        implied_ev_median = target.revenue * ev_rev_stats['median']
        implied_ev_high = target.revenue * ev_rev_stats['p75']

        net_debt = target.enterprise_value - target.market_cap

        implied['ev_revenue'] = {
            'low': (implied_ev_low - net_debt) / target.shares_outstanding,
            'median': (implied_ev_median - net_debt) / target.shares_outstanding,
            'high': (implied_ev_high - net_debt) / target.shares_outstanding,
            'multiple_low': ev_rev_stats['p25'],
            'multiple_median': ev_rev_stats['median'],
            'multiple_high': ev_rev_stats['p75']
        }

    # P/E implied valuation
    if summary_stats['pe_ratio']['count'] > 0 and target.net_income > 0:
        pe_stats = summary_stats['pe_ratio']
        eps = target.net_income / target.shares_outstanding

        implied['pe_ratio'] = {
            'low': eps * pe_stats['p25'],
            'median': eps * pe_stats['median'],
            'high': eps * pe_stats['p75'],
            'multiple_low': pe_stats['p25'],
            'multiple_median': pe_stats['median'],
            'multiple_high': pe_stats['p75']
        }

    return implied


def format_comp_table(output: ComparableOutput) -> str:
    """
    Format the comparable analysis as a readable table.

    Args:
        output: ComparableOutput from build_comp_table

    Returns:
        Formatted string table
    """
    lines = []

    # Header
    lines.append("=" * 100)
    lines.append("COMPARABLE COMPANY ANALYSIS")
    lines.append("=" * 100)

    # Peer table
    lines.append("\n1. PEER MULTIPLES")
    lines.append("-" * 100)

    # Table header
    header = f"{'Company':<20} {'Ticker':<8} {'EV ($M)':<12} {'EV/EBITDA':<12} {'EV/Revenue':<12} {'P/E':<10}"
    lines.append(header)
    lines.append("-" * 100)

    # Peer rows
    for peer in output.peer_table:
        ev_ebitda_str = f"{peer['ev_ebitda']:.1f}x" if peer['ev_ebitda'] else "N/A"
        ev_rev_str = f"{peer['ev_revenue']:.2f}x" if peer['ev_revenue'] else "N/A"
        pe_str = f"{peer['pe_ratio']:.1f}x" if peer['pe_ratio'] else "N/A"

        row = f"{peer['name']:<20} {peer['ticker']:<8} {peer['ev']:>10,.0f} {ev_ebitda_str:>12} {ev_rev_str:>12} {pe_str:>10}"
        lines.append(row)

    lines.append("-" * 100)

    # Summary statistics
    lines.append("\n2. PEER GROUP STATISTICS")
    lines.append("-" * 60)

    for metric, label in [('ev_ebitda', 'EV/EBITDA'), ('ev_revenue', 'EV/Revenue'), ('pe_ratio', 'P/E')]:
        stats = output.summary_stats[metric]
        if stats['count'] > 0:
            lines.append(f"\n{label}:")
            lines.append(f"  Mean:   {stats['mean']:.2f}x    Median: {stats['median']:.2f}x")
            lines.append(f"  Min:    {stats['min']:.2f}x    Max:    {stats['max']:.2f}x")
            lines.append(f"  25th %: {stats['p25']:.2f}x    75th %: {stats['p75']:.2f}x")

    # Target analysis
    lines.append("\n" + "=" * 100)
    lines.append("3. TARGET COMPANY ANALYSIS")
    lines.append("-" * 60)

    tm = output.target_multiples
    lines.append(f"\n{tm['name']} ({tm['ticker']})")
    lines.append(f"Current Price: ${tm['current_price']:.2f}")
    lines.append(f"\nTarget Multiples:")

    if tm['ev_ebitda']:
        pct = output.percentile_rankings.get('ev_ebitda', 'N/A')
        pct_str = f"{pct:.0f}th" if isinstance(pct, float) else pct
        lines.append(f"  EV/EBITDA:  {tm['ev_ebitda']:.1f}x  (Percentile: {pct_str})")

    if tm['ev_revenue']:
        pct = output.percentile_rankings.get('ev_revenue', 'N/A')
        pct_str = f"{pct:.0f}th" if isinstance(pct, float) else pct
        lines.append(f"  EV/Revenue: {tm['ev_revenue']:.2f}x  (Percentile: {pct_str})")

    if tm['pe_ratio']:
        pct = output.percentile_rankings.get('pe_ratio', 'N/A')
        pct_str = f"{pct:.0f}th" if isinstance(pct, float) else pct
        lines.append(f"  P/E Ratio:  {tm['pe_ratio']:.1f}x  (Percentile: {pct_str})")

    # Implied valuations
    lines.append("\n4. IMPLIED VALUATION RANGE")
    lines.append("-" * 60)

    for method, label in [('ev_ebitda', 'EV/EBITDA'), ('ev_revenue', 'EV/Revenue'), ('pe_ratio', 'P/E')]:
        if method in output.implied_valuations:
            iv = output.implied_valuations[method]
            lines.append(f"\nBased on {label}:")
            lines.append(f"  Low (25th %):    ${iv['low']:.2f}  ({iv['multiple_low']:.2f}x)")
            lines.append(f"  Median:          ${iv['median']:.2f}  ({iv['multiple_median']:.2f}x)")
            lines.append(f"  High (75th %):   ${iv['high']:.2f}  ({iv['multiple_high']:.2f}x)")

    # Calculate blended range
    all_lows = []
    all_medians = []
    all_highs = []
    for method in ['ev_ebitda', 'ev_revenue', 'pe_ratio']:
        if method in output.implied_valuations:
            all_lows.append(output.implied_valuations[method]['low'])
            all_medians.append(output.implied_valuations[method]['median'])
            all_highs.append(output.implied_valuations[method]['high'])

    if all_lows:
        lines.append("\n" + "-" * 60)
        lines.append("BLENDED VALUATION RANGE:")
        lines.append(f"  Low:     ${min(all_lows):.2f}")
        lines.append(f"  Median:  ${statistics.median(all_medians):.2f}")
        lines.append(f"  High:    ${max(all_highs):.2f}")
        lines.append(f"\n  Current: ${tm['current_price']:.2f}")

        median_implied = statistics.median(all_medians)
        upside = (median_implied / tm['current_price'] - 1) * 100
        lines.append(f"  Upside to Median: {upside:+.1f}%")

    lines.append("\n" + "=" * 100)
    return "\n".join(lines)


if __name__ == "__main__":
    # Example: SaaS company comparable analysis
    print("=" * 100)
    print("PEER COMPARABLES ANALYSIS - EXAMPLE")
    print("=" * 100)

    # Define target company
    target = TargetMetrics(
        name="Target SaaS Corp",
        ticker="TSAS",
        enterprise_value=5000,      # $5B EV
        market_cap=5500,            # $5.5B market cap (net cash position)
        ebitda=400,                 # $400M EBITDA
        revenue=1200,               # $1.2B revenue
        net_income=250,             # $250M net income
        shares_outstanding=100,     # 100M shares
        current_price=55.00,        # $55 per share
        growth_rate=0.25            # 25% growth
    )

    # Define peer group
    peers = [
        PeerData(
            name="Cloud Leader Inc",
            ticker="CLDI",
            enterprise_value=25000,
            market_cap=28000,
            ebitda=2000,
            revenue=8000,
            net_income=1200,
            shares_outstanding=200,
            growth_rate=0.20
        ),
        PeerData(
            name="Software Giant Co",
            ticker="SFTG",
            enterprise_value=15000,
            market_cap=16000,
            ebitda=1500,
            revenue=5000,
            net_income=900,
            shares_outstanding=150,
            growth_rate=0.15
        ),
        PeerData(
            name="Fast Growth Tech",
            ticker="FGTH",
            enterprise_value=8000,
            market_cap=8500,
            ebitda=500,
            revenue=1800,
            net_income=200,
            shares_outstanding=80,
            growth_rate=0.35
        ),
        PeerData(
            name="Enterprise SaaS Ltd",
            ticker="ESAS",
            enterprise_value=6000,
            market_cap=6200,
            ebitda=550,
            revenue=1500,
            net_income=300,
            shares_outstanding=120,
            growth_rate=0.22
        ),
        PeerData(
            name="Data Platform Corp",
            ticker="DPLT",
            enterprise_value=4500,
            market_cap=4800,
            ebitda=350,
            revenue=1000,
            net_income=180,
            shares_outstanding=90,
            growth_rate=0.28
        ),
        PeerData(
            name="Subscription Software",
            ticker="SUBS",
            enterprise_value=3500,
            market_cap=3700,
            ebitda=300,
            revenue=900,
            net_income=150,
            shares_outstanding=75,
            growth_rate=0.18
        )
    ]

    # Run analysis
    result = build_comp_table(target, peers)

    # Print formatted report
    print(format_comp_table(result))

    # Show raw output
    print("\n" + "=" * 100)
    print("RAW OUTPUT DATA")
    print("-" * 50)

    print("\nTarget Percentile Rankings:")
    for metric, percentile in result.percentile_rankings.items():
        print(f"  {metric}: {percentile:.1f}th percentile")

    print("\nImplied Share Prices:")
    for method, values in result.implied_valuations.items():
        print(f"  {method}:")
        print(f"    Low: ${values['low']:.2f}, Median: ${values['median']:.2f}, High: ${values['high']:.2f}")

SHA-256: b324f28a7eee3ec566ab9fbb85928ce67278d4033bb010516ade6cb342c3fb6a