UFCData 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UFCData
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Obtain UFC data and functions to manipulate it
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE.md
@@ -9,4 +9,5 @@ Requires-Dist: pandas
9
9
  Requires-Dist: numpy
10
10
  Requires-Dist: glicko2
11
11
  Requires-Dist: rapidfuzz
12
+ Requires-Dist: matplotlib
12
13
  Dynamic: license-file
@@ -4,8 +4,6 @@ An open-source Python package for accessing, manipulating, and analyzing UFC dat
4
4
 
5
5
  https://pypi.org/project/UFCData/
6
6
 
7
- add helper functions like convert odds
8
-
9
7
  ## Table of Contents
10
8
 
11
9
  - [Installation](#installation)
@@ -23,13 +21,17 @@ Fighter Functions
23
21
  - [Fighter Statistic](#fighter-statistic)
24
22
 
25
23
  Helper Functions
26
- - [Odds Convert](#elo)
27
- - [Weight Convert](#elo)
28
- - [Gender Convert](#elo)
29
- - [Mirror](#elo)
24
+ - [Odds Convert](#odds-convert)
25
+ - [Weight Convert](#weight-convert)
26
+ - [Gender Convert](#gender-convert)
27
+ - [Mirror](#mirror)
30
28
 
31
29
  Model Evaluation
32
- - [Odds Baseline Example](#elo)
30
+ - [Train Test Split](#train-test-split)
31
+ - [Get Expanding Window](#get-expanding-window)
32
+ - [Plotting Accuracy](#plotting-accuracy)
33
+ - [Expanding Window Baseline Example](#expanding-window-cv-and-final-test-example)
34
+
33
35
 
34
36
  Data Sources
35
37
  - [Online Sources](#online-sources)
@@ -56,7 +58,6 @@ data = ufc.get_data()
56
58
 
57
59
  ## Obtain fighter_bio dataframe
58
60
  fighter_bio = data["fighter_bio"].copy()
59
-
60
61
  ```
61
62
 
62
63
  Note that data in here and other functions below reference data that is available prior to the data.
@@ -101,7 +102,7 @@ This is useful when the exact value stored in the dataset is unknown or when the
101
102
 
102
103
  ## Elo
103
104
 
104
- UFCData provides an Elo rating system for calculating fighter ratings based on their previous fight results. Ratings are calculated chronologically, with each fighter's rating recorded **immediately before each fight**.
105
+ UFCData provides an Elo rating system for calculating fighter ratings based on their previous fight results. Ratings are calculated chronologically, with each fighter's rating recorded immediately before each fight.
105
106
 
106
107
  This allows Elo ratings to be used as features for predictive modeling without incorporating information from the fight being predicted.
107
108
 
@@ -147,7 +148,7 @@ The resulting `fight_elo` dataframe can then be used to examine or incorporate p
147
148
 
148
149
  ### Important
149
150
 
150
- `fight_df` should be sorted from **most recent to least recent** before being passed to `get_elo()`. The function reverses the dataframe internally to process fights chronologically and returns the resulting data in the original order.
151
+ `fight_df` should be sorted from most recent to least recent before being passed to `get_elo()`. The function reverses the dataframe internally to process fights chronologically and returns the resulting data in the original order.
151
152
 
152
153
  ## Fighter History
153
154
 
@@ -182,14 +183,12 @@ data = ufc.get_data()
182
183
  fighter_bio = data["fighter_bio"].copy()
183
184
  fights_df = data["past_fights"].copy()
184
185
 
185
- # Obtain Jon Jones' UFCStats link
186
186
  jon_jones = ufc.search(
187
187
  fighter_bio,
188
188
  "Name",
189
189
  "Jon Jones"
190
190
  ).iloc[0]["Fighter Link"]
191
191
 
192
- # Get Jon Jones' fight history
193
192
  jon_jones_history = ufc.get_fighter_history(
194
193
  jon_jones,
195
194
  fighter_bio,
@@ -202,7 +201,7 @@ This function is useful for analyzing an individual fighter's career, constructi
202
201
  ## Fighter Statistic
203
202
 
204
203
 
205
- The function `get_fighter_statistic()`, transforms a fighter's UFC fight history into a time-series dataset where each row represents the fighter's statistics **as they were known before a particular fight**.
204
+ The function `get_fighter_statistic()`, transforms a fighter's UFC fight history into a time-series dataset where each row represents the fighter's statistics as they were known before a particular fight.
206
205
 
207
206
  This is designed for machine-learning applications where using information from the future would cause data leakage.
208
207
 
@@ -266,7 +265,7 @@ This includes:
266
265
 
267
266
  A DataFrame containing the same types of statistics for each previous UFC fight.
268
267
 
269
- Each row represents the fighter's information **immediately before that fight**.
268
+ Each row represents the fighter's information immediately before that fight.
270
269
 
271
270
  For example:
272
271
 
@@ -292,14 +291,12 @@ fights_df = data["past_fights"].copy()
292
291
  rounds_df = data['past_rounds'].copy()
293
292
  future_df = data['future_fights']
294
293
 
295
- # Obtain Jon Jones' UFCStats link
296
294
  jon_jones = ufc.search(
297
295
  fighter_bio,
298
296
  "Name",
299
297
  "Jon Jones"
300
298
  ).iloc[0]["Fighter Link"]
301
299
 
302
- # Get Jon Jones' statistic
303
300
  jon_jones_current, jon_jones_past = ufc.get_fighter_statistic(
304
301
  jon_jones,
305
302
  fighter_bio,
@@ -309,7 +306,294 @@ jon_jones_current, jon_jones_past = ufc.get_fighter_statistic(
309
306
 
310
307
  jon_jones_current
311
308
  ```
309
+ ## Odds Convert
310
+
311
+ The function `convert_odds()` converts American betting odds from strings into integers or floating point numbers.
312
+
313
+ ```python
314
+ convert_odds(odds, probability)
315
+ ```
316
+
317
+ ### Parameters
318
+
319
+ * `odds` — American betting odds as a string.
320
+
321
+ ### Returns
322
+
323
+ If probability=True, it returns a `float` representing the implied probability of the betting odds.
324
+
325
+ For positive American odds:
326
+
327
+ ```text
328
+ Probability = 100 / (odds + 100)
329
+ ```
330
+
331
+ For negative American odds:
332
+
333
+ ```text
334
+ Probability = -odds / (-odds + 100)
335
+ ```
336
+ If probability=False, takes the string and converts it to an integer.
337
+
338
+ ### Example
339
+
340
+ ```python
341
+ import ufcdata as ufc
342
+
343
+ data = ufc.get_data()
344
+
345
+ fights_df = data["past_fights"].copy()
346
+
347
+ fights_df["Fighter 1 Odds"] = fights_df["Fighter 1 Odds"].apply(ufc.convert_odds)
348
+ fights_df["Fighter 1 Probability"] = fights_df["Fighter 1 Odds"].apply(ufc.convert_odds, probability=True)
349
+ fights_df
350
+ ```
351
+ ## Gender Convert
352
+
353
+ The function `convert_gender()` converts gender labels into binary numeric values, with `"Male"` represented as `1` and `"Female"` represented as `0`.
354
+
355
+ ```python
356
+ convert_gender(gender)
357
+ ```
358
+
359
+ ### Parameters
360
+
361
+ * `gender` — Gender label as a string.
362
+
363
+ ### Returns
364
+
365
+ Returns `1` for `"Male"` and `0` for `"Female"`.
366
+
367
+ If the gender is missing or does not match either `"Male"` or `"Female"`, it returns `None`.
368
+
369
+ ### Example
370
+
371
+ ```python
372
+ import ufcdata as ufc
373
+
374
+ data = ufc.get_data()
375
+
376
+ fights_df = data["past_fights"].copy()
377
+ fights_df["Gender"] = fights_df["Gender"].apply(ufc.convert_gender)
378
+ fights_df
379
+ ```
380
+
381
+ ## Weight Convert
382
+
383
+ The function `convert_weight()` converts UFC weight class labels into their corresponding weight limits in pounds.
384
+
385
+ ```python
386
+ convert_weight(weight)
387
+ ```
388
+
389
+ ### Parameters
390
+
391
+ * `weight` — Weight class as a string.
392
+
393
+ ### Returns
394
+
395
+ Returns the weight limit in pounds for recognized weight classes.
396
+
397
+ | Weight Class | Weight (lb) |
398
+ | ----------------- | ----------: |
399
+ | Strawweight | 115 |
400
+ | Flyweight | 125 |
401
+ | Bantamweight | 135 |
402
+ | Featherweight | 145 |
403
+ | Lightweight | 155 |
404
+ | Welterweight | 170 |
405
+ | Middleweight | 185 |
406
+ | Light Heavyweight | 205 |
407
+ | Heavyweight | 265 |
408
+ | Super Heavyweight | 265 |
409
+
410
+ For `"Catch Weight"` and `"Open Weight"`, as well as unrecognized or missing values, the function returns `NaN`.
411
+
412
+ ### Example
413
+
414
+ ```python
415
+ import ufcdata as ufc
416
+
417
+ data = ufc.get_data()
418
+
419
+ past_fights = data["past_fights"].copy()
420
+ past_fights["Weight Class"] = past_fights["Weight Class"].apply(ufc.convert_weight)
421
+
422
+ past_fights
423
+ ```
424
+
425
+ ## Mirror
426
+
427
+ The function `mirror()` creates mirrored versions of a DataFrame by swapping corresponding pairs of columns. This is useful when analyzing UFC fights from both fighters' perspectives.
428
+
429
+ ```python
430
+ mirror(df, cols_1, cols_2, in_order=True)
431
+ ```
432
+
433
+ ### Parameters
434
+
435
+ * `df` — The DataFrame to mirror.
436
+ * `cols_1` — List of column names representing the first fighter or side.
437
+ * `cols_2` — List of column names representing the second fighter or side. Each column is swapped with the corresponding column in `cols_1`.
438
+ * `in_order` — Determines the order of the returned rows. Defaults to `True`.
439
+
440
+ If `in_order=True`, each original row is immediately followed by its mirrored row.
441
+
442
+ If `in_order=False`, all original rows are followed by all mirrored rows.
443
+
444
+ ### Returns
445
+
446
+ Returns a DataFrame containing both the original and mirrored versions of each row.
447
+
448
+ ### Example
449
+
450
+ ```python
451
+ import ufcdata as ufc
452
+
453
+ data = ufc.get_data()
454
+
455
+ fights_df = data["past_fights"].copy()
456
+ fights_df = fights_df[["Date", "Event Link", "Fighter 1", "Fighter 1 Odds","Fighter 2", "Fighter 2 Odds"]]
457
+
458
+ cols_1 = [
459
+ "Fighter 1",
460
+ "Fighter 1 Odds",
461
+ ]
462
+
463
+ cols_2 = [
464
+ "Fighter 2",
465
+ "Fighter 2 Odds",
466
+ ]
467
+
468
+ mirrored_fights = ufc.mirror(
469
+ fights_df,
470
+ cols_1,
471
+ cols_2
472
+ )
473
+
474
+ mirrored_fights
475
+ ```
476
+
477
+ Using `in_order=True` produces rows in the following order:
478
+
479
+ ```text
480
+ Original Fight 1
481
+ Mirrored Fight 1
482
+ Original Fight 2
483
+ Mirrored Fight 2
484
+ ...
485
+ ```
486
+ Using `in_order=True` produces rows in the following order:
487
+ ```text
488
+ Original Fight 1
489
+ Original Fight 2
490
+ Mirrored Fight 1
491
+ Mirrored Fight 2
492
+ ...
493
+ ```
494
+
495
+ This is useful when creating fighter-level datasets where each fight should be represented from both fighters' perspectives.
496
+
497
+ ## Train Test Split
498
+ The function `train_test_split()` splits a UFC fight DataFrame into training and test sets while preserving the chronological order of the data. Unlike `sklearn.model_selection.train_test_split()`, this function does not randomly shuffle the data.
499
+
500
+ The input `fights_df` should be ordered from newest to oldest. The most recent fights are assigned to the test set, while the older fights are assigned to the training set. Both returned DataFrames are then reordered from oldest to newest.
501
+
502
+ ```python
503
+ train_test_split(fights_df, test_size=0.2)
504
+ ```
505
+ ### Parameters
506
+
507
+ * `fights_df` — UFC fight DataFrame ordered from newest to oldest.
508
+ * `test_size` — Determines the size of the test set. If less than 1, it is interpreted as a proportion of the total number of fights. If greater than or equal to 1, it is interpreted as the exact number of fights to include in the test set. Defaults to 0.2.
509
+ ### Returns
510
+ The function returns two dataFrames. The first is a dataFrame containing the older fights used for training, ordered from oldest to newest. The second is a dataFrame containing the most recent fights used for testing, ordered from oldest to newest.
511
+
512
+ ### Example
513
+
514
+ ```python
515
+ import ufcdata as ufc
516
+
517
+ data = ufc.get_data()
518
+
519
+ past_fights = data["past_fights"].copy()
520
+ train, test = ufc.train_test_split(past_fights, 0.2)
521
+ train
522
+ ```
523
+
524
+ ## Get Expanding Window
525
+ The function `get_expanding_window()` creates multiple training and test sets for time-series cross-validation using an expanding training window.
526
+
527
+ The input `fights_df` should be ordered from oldest to newest. With each fold, the training set expands to include more historical fights, while the test set remains a fixed size.
528
+
529
+ ```python
530
+ fold_results = ufc.get_expanding_window(
531
+ fights_df,
532
+ folds=5,
533
+ test_size=500
534
+ )
535
+ ```
536
+ ### Parameters
537
+
538
+ * `fights_df` — UFC fight DataFrame ordered from oldest to newest.
539
+ * `folds` — The number of train/test folds to create.
540
+ * `test_size` — The number of fights included in each test set.
541
+
542
+ ### Returns
543
+ The function returns a dictionary containing the training and test DataFrames for each fold.
544
+ ```python
545
+ {
546
+ "Fold 1": {
547
+ "train": train_dataframe,
548
+ "test": test_dataframe
549
+ },
550
+ "Fold 2": {
551
+ "train": train_dataframe,
552
+ "test": test_dataframe
553
+ },
554
+ ...
555
+ }
556
+ ```
557
+ ### Example
558
+
559
+ ```python
560
+ import ufcdata as ufc
561
+
562
+ data = ufc.get_data()
563
+
564
+ past_fights = data["past_fights"].copy()
565
+ train, test = ufc.train_test_split(past_fights, 0.2)
566
+
567
+ folds = ufc.get_expanding_window(train, 10, 500)
568
+ folds['Fold 2']['test']
569
+ ```
570
+
571
+ ## Plotting Accuracy
572
+
573
+ The function `plot_cv` plots out a line graph showing the accuracy of each fold along with a line showing the mean.
574
+
575
+ ```python
576
+ plot_cv(cv_labels, cv_accuracies, title="Cross Validation Accuracy")
577
+ ```
578
+
579
+ ### Parameters
580
+
581
+ * `cv_labels` — The list of fold names.
582
+ * `cv_accuracies` — The list of accuracies.
583
+ * `title` — The title of the plot.
584
+
585
+ The function `plot_test` plots out a single bar graph showing the accuracy of a single test.
586
+
587
+ ```python
588
+ plot_test(accuracy, title="Test Accuracy")
589
+ ```
590
+
591
+ ### Parameters
592
+
593
+ * `accuracy` — The accuracy.
594
+ * `title` — The title of the plot.
312
595
 
596
+ ## Expanding Window Baseline Example
313
597
 
314
598
  ## Online Sources
315
599
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UFCData
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Obtain UFC data and functions to manipulate it
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE.md
@@ -9,4 +9,5 @@ Requires-Dist: pandas
9
9
  Requires-Dist: numpy
10
10
  Requires-Dist: glicko2
11
11
  Requires-Dist: rapidfuzz
12
+ Requires-Dist: matplotlib
12
13
  Dynamic: license-file
@@ -8,6 +8,7 @@ UFCData.egg-info/requires.txt
8
8
  UFCData.egg-info/top_level.txt
9
9
  ufcdata/__init__.py
10
10
  ufcdata/data.py
11
+ ufcdata/evaluation.py
11
12
  ufcdata/fighter.py
12
13
  ufcdata/helper.py
13
14
  ufcdata/ratings.py
@@ -3,3 +3,4 @@ pandas
3
3
  numpy
4
4
  glicko2
5
5
  rapidfuzz
6
+ matplotlib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "UFCData"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "Obtain UFC data and functions to manipulate it"
5
5
  requires-python = ">=3.10"
6
6
 
@@ -10,4 +10,5 @@ dependencies = [
10
10
  "numpy",
11
11
  "glicko2",
12
12
  "rapidfuzz",
13
+ "matplotlib",
13
14
  ]
@@ -2,4 +2,5 @@ from .data import get_data
2
2
  from .search import search
3
3
  from .ratings import get_elo
4
4
  from .fighter import get_fighter_history, get_fighter_statistic
5
- from .helper import convert_odds, convert_gender, convert_weight, mirror
5
+ from .helper import convert_odds, convert_gender, convert_weight, mirror
6
+ from .evaluation import train_test_split, get_expanding_window, plot_cv, plot_test
@@ -0,0 +1,192 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ def train_test_split(fights_df, test_size=0.2):
5
+ """
6
+ Split a UFC fight DataFrame into chronological training and test sets.
7
+
8
+ The input DataFrame must be ordered from the newest fight to the oldest
9
+ fight. The most recent fights are assigned to the test set, while the
10
+ remaining older fights are assigned to the training set. Both returned
11
+ DataFrames are reversed so that they are ordered from oldest to newest.
12
+
13
+ Parameters
14
+ ----------
15
+ fights_df : pandas.DataFrame
16
+ UFC fight DataFrame ordered from newest to oldest.
17
+ test_size : float or int, default=0.2
18
+ Determines the size of the test set. If less than 1, it is interpreted
19
+ as a proportion of the total number of fights. If greater than or
20
+ equal to 1, it is interpreted as the exact number of fights to include
21
+ in the test set.
22
+
23
+ Returns
24
+ -------
25
+ tuple of pandas.DataFrame
26
+ A tuple containing:
27
+
28
+ - train : pandas.DataFrame
29
+ Training data consisting of the older fights, ordered from
30
+ oldest to newest.
31
+ - test : pandas.DataFrame
32
+ Test data consisting of the most recent fights, ordered from
33
+ oldest to newest.
34
+ """
35
+ fights_df = fights_df.copy()
36
+
37
+ fights_df["Winner"] = np.select(
38
+ [
39
+ fights_df["Fighter 1 Outcome"] == "W",
40
+ fights_df["Fighter 2 Outcome"] == "W"
41
+ ],
42
+ [
43
+ fights_df["Fighter 1 Link"],
44
+ fights_df["Fighter 2 Link"]
45
+ ],
46
+ default=None
47
+ )
48
+
49
+ if test_size < 1:
50
+ n_test = int(len(fights_df) * test_size)
51
+ else:
52
+ n_test = test_size
53
+
54
+ test = fights_df[:n_test]
55
+ train = fights_df[n_test:]
56
+
57
+ test = test.iloc[::-1].reset_index(drop=True)
58
+ train = train.iloc[::-1].reset_index(drop=True)
59
+
60
+ return (train, test)
61
+
62
+ def get_expanding_window(fights_df, folds, test_size):
63
+ """
64
+ Create expanding training windows with fixed-size test sets for
65
+ time-series cross-validation.
66
+
67
+ Parameters
68
+ ----------
69
+ fights_df : pandas.DataFrame
70
+ UFC fight DataFrame ordered chronologically from oldest to newest.
71
+ folds : int
72
+ Number of train/test folds to create.
73
+ test_size : int
74
+ Number of fights included in each test set.
75
+
76
+ Returns
77
+ -------
78
+ dict
79
+ Dictionary containing the training and test DataFrames for each fold.
80
+ Each fold is stored using the key ``"Fold 1"``, ``"Fold 2"``, etc.,
81
+ with the following structure:
82
+
83
+ {
84
+ "train": train_dataframe,
85
+ "test": test_dataframe
86
+ }
87
+
88
+ Notes
89
+ -----
90
+ The training window expands with each fold while the test window remains
91
+ fixed at `test_size` rows.
92
+
93
+ For example, the folds follow the structure:
94
+
95
+ Fold 1: [Train] [Test]
96
+ Fold 2: [------ Train ------] [Test]
97
+ Fold 3: [------------ Train ------------] [Test]
98
+
99
+ This approach is suitable for time-series cross-validation because each
100
+ training set only contains observations that occur before its
101
+ corresponding test set.
102
+ """
103
+ fold_results = {}
104
+
105
+ train_sizes = np.linspace(
106
+ test_size,
107
+ len(fights_df) - test_size,
108
+ folds,
109
+ dtype=int
110
+ )
111
+
112
+ for i, train_size in enumerate(train_sizes, start=1):
113
+ train = fights_df.iloc[:train_size].reset_index(drop=True)
114
+ test = fights_df.iloc[train_size:train_size + test_size].reset_index(drop=True)
115
+
116
+ fold_results[f"Fold {i}"] = {
117
+ "train": train,
118
+ "test": test
119
+ }
120
+
121
+ return fold_results
122
+
123
+ def plot_cv(cv_labels, cv_accuracies, title="Cross Validation Accuracy"):
124
+ plt.figure(figsize=(10, 5))
125
+
126
+ plt.plot(
127
+ cv_labels,
128
+ cv_accuracies,
129
+ color="#666666",
130
+ linestyle="-",
131
+ linewidth=2,
132
+ marker="o",
133
+ markersize=7,
134
+ markerfacecolor="#D20A0A", # UFC red dots
135
+ markeredgecolor="#FFFFFF",
136
+ markeredgewidth=1.5,
137
+ label="Fold Accuracy"
138
+ )
139
+
140
+
141
+ # Mean accuracy across folds
142
+ mean_accuracy = np.mean(cv_accuracies)
143
+
144
+ plt.axhline(
145
+ mean_accuracy,
146
+ color="#D20A0A",
147
+ linestyle="--",
148
+ linewidth=2,
149
+ label=f"Mean Accuracy: {mean_accuracy:.1%}"
150
+ )
151
+
152
+
153
+ plt.xlabel("Fold")
154
+ plt.ylabel("Accuracy")
155
+ plt.title(title)
156
+ # plt.ylim(0, 1)
157
+
158
+ plt.xticks(rotation=45)
159
+ plt.grid(True, alpha=0.3)
160
+ plt.legend()
161
+
162
+ plt.tight_layout()
163
+ plt.show()
164
+
165
+ def plot_test(accuracy, title="Test Accuracy"):
166
+ plt.figure(figsize=(6, 5))
167
+
168
+ plt.bar(
169
+ 0,
170
+ accuracy,
171
+ color="#D20A0A",
172
+ width=0.25
173
+ )
174
+
175
+ plt.ylabel("Accuracy")
176
+ plt.title(title)
177
+ plt.ylim(0, 1)
178
+
179
+ plt.xlim(-0.5, 0.5)
180
+ plt.xticks([0], ["Test Set"])
181
+
182
+ plt.text(
183
+ 0,
184
+ accuracy + 0.02,
185
+ f"{accuracy:.1%}",
186
+ ha="center",
187
+ fontweight="bold",
188
+ color="#444444"
189
+ )
190
+
191
+ plt.tight_layout()
192
+ plt.show()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes