betting_combat.rounds.features.fighters
Group 1: every fighter’s as-of profile before every fight, from strictly earlier dates.
Port of the research ufc/dataset/fighters.py; the numbers must stay identical.
One row per (fight_id, fighter_id). The rule that matters most: a feature for a fight on date d uses only fights on dates < d, and every shrinkage prior (the league average a rate is pulled toward) is also computed from dates < d.
Feature blocks (FighterHistory.BLOCKS):
experience n_fights, cage_min, days_since_last, age, height_in, reach_in, title_share
results win_rate, finish_win_rate, ko_win_rate, sub_win_rate, dec_rate,
got_ko_rate, got_sub_rate
momentum mom_h2 exponentially weighted sum of +1 win / -1 loss,
half-life 2 fights, whole career (a streak compounds)
finish_mom_h2 same weights on “won by finish” (1/0)
durability_mom_h2 same weights on “got finished” (1/0)
(half-life 2 locked 2026-09-27 after the feature study compared 1, 2, 3, 5)
survival past_r1..past_r4 share of earlier fights (scheduled long enough) that
went past round k; avg_fight_min
output sig_pm, sig_abs_pm, sig_acc, sig_def, td_per15, td_acc, td_def, ctrl_share,
ctrl_abs_share, kd_per15, kd_abs_per15, sub_per15
variance perf_sd SD of per-fight (sig landed - absorbed) per minute
quality opp_win_rate mean as-of win rate of the opponents he has faced
judging judged_round_share share of judge-rounds won in earlier decisions
(MMADecisions; NaN where none)
patterns fade, r1_finish_share, rounds_won_share, slow_start_share, southpaw,
division_move (see round_patterns and division_move)
Draws / no contests / DQs / overturned fights stay in a fighter’s history for experience, minutes and stats (they happened), but count as neither win nor loss.
Classes
FighterHistory
build
build() -> pd.DataFramedivision_move
division_move(h: pd.DataFrame) -> np.ndarray+1 moving up a division vs his previous fight, -1 moving down, 0 same / unknown.
expanding_sd
expanding_sd(h: pd.DataFrame, col: str, min_n: int = 2) -> pd.Seriesfights_long
fights_long() -> pd.DataFrameOne row per fighter per bout: outcome from his side, his fight totals and his opponent’s (absorbed).
fill_physicals
fill_physicals(P: pd.DataFrame) -> pd.DataFrameUFCStats leaves reach (and some birth dates) blank mostly for fighters whose UFC careers were short, i.e. who lost and were cut. Blank reach therefore encodes the future: fighters with no reach won 16% of fights against a market price of 42%. So a blank never survives as a blank. Every fill is as of the fight date, from rows on strictly earlier dates only (research fixed 2026-09-30; before, the fills used the whole year’s and the whole history’s rows, i.e. later fights): height, age the median over fighter-rows in the 365 days before the date (all earlier rows while that window holds fewer than 20; the same date’s median only for the first card, which has no earlier rows) reach least squares reach ~ height over the fighters with both known who had fought before the date (the first 30 such fighters, by first date and fighter_id, while fewer have), rounded to the inch like real values Nothing downstream can tell a filled value by its missingness.
judged_share
judged_share(h: pd.DataFrame) -> np.ndarrayShare of judge-rounds won in earlier fights that went to the judges, shrunk to 0.5 with 3 pseudo judge-rounds. NaN before a fighter’s first scored fight.
league_prior
league_prior(h: pd.DataFrame, num: str, den: str) -> pd.SeriesLeague-wide num/den over all fights on strictly earlier dates (the shrinkage target).
momentum
momentum(h: pd.DataFrame) -> pd.DataFrameExponentially weighted sums over the fighter’s earlier fights, most recent weight 1: M = s_1 + lams_2 + lam^2s_3 + …, lam = 0.5 ** (1 / half_life).
opponent_quality
opponent_quality(h: pd.DataFrame, P: pd.DataFrame) -> np.ndarrayMean, over the fighter’s earlier fights, of the opponent’s as-of win rate then.
prior_sums
prior_sums(h: pd.DataFrame, cols: list[str]) -> pd.DataFramePer row: the sum of cols over the fighter’s fights on strictly earlier dates.
round_patterns
round_patterns(h: pd.DataFrame) -> pd.DataFrameHow a fighter’s fights unfold round by round, from his earlier fights:
fade mean (sig. strike diff per round in rounds 2+) minus (round 1), over earlier fights that reached round 2; > 0 builds, < 0 fades r1_finish_share share of his finish wins that came in round 1 (shrunk to league) rounds_won_share share of rounds fought that he won by the stats rule (shrunk to 0.5) slow_start_share share of earlier fights where he lost round 1 by the rule but won