Visualization of UFC Fighter Rankings over Time

GitHub: https://github.com/cinhui/ufc-ranking-chart
Technologies used: Python, D3.js, Javascript

Motivation

The purpose of this project was to create a data-driven animation showing the changes in UFC fighter rankings over time. The latest UFC 249 Pay-Per-View event was on May 9th, 2020 and featured title matches for the UFC Lightweight and Bantamweight champions. For the purpose of the visualizations, only the rankings for the lightweight and bantamweight divisions will be displayed.

The choice of visualization for this was to use a single stacked column of equally sized rectangular bars. The top bar would be the champion and the bars below would be each subsequently ranked fighter. The bars with the fighter’s names would enter, leave, and move up or down along the stacked column as their rankings change over time.

Data

The Ultimate Fighting Championship rankings was first introduced in February 2013. The votes are generated by a panel of media members. The media members are asked to vote for who they feel are the top fighters in the UFC by weight class and pound-for-pound. The fighters have to be in active status the UFC in order to receive votes and not facing any disciplinary suspensions of nine months or longer. A fighter may appear in more than one weight division at a time. The champion and interim champion can be voted on for the pound-for-pound rankings, but are not voted on in their weight class since they are already considered to be in the top of their division.

The rankings data was obtained from the following sources.

The dataset contains the official rankings published by UFC since February 2003 for each weight class. The champion is ranked at 0 and the top 10-15 fighters are listed by their rank. Initially, only the top 10 fighters were ranked. Since December 30, 2013, the rankings have included 15 fighters.

The rankings data used in these charts are up-to-date as of UFC Fight Night: Lee vs. Olivera on March 14, 2020.

Data Processing

Initial data exploratory analysis and data manipulation was done using Python. The accompanied Jupyter notebook can be found in the project GitHub repository. Below is a general overview of the data manipulation and preparation steps.

The dataset contains rankings from February 4rd, 2013 through March 14, 2020. The weight class and divisions ranked include:

['Pound-for-Pound', 'Flyweight', 'Bantamweight', 'Featherweight', 'Lightweight', 'Welterweight', 'Middleweight', 'Light Heavyweight', 'Heavyweight', "Women's Bantamweight", "Women's Strawweight", "Women's Featherweight", "Women's Flyweight"]

There were a total of 228 dates of rankings. A sequence of time slices were generated to represent each date. Time slice 0 represents the first set of rankings from February 2013, while time slice 227 represents the rankings from March 14, 2020.

To keep the size of the stacked bar charts consistent over time, only the champion and the 10 top ranked fighters would be included the in chart for each time slice.

A subset of the data containing just the rankings for the lightweight division was extracted from the data. There were 44 unique fighters identified and after truncating the list, there were 31 lightweight fighters.

Number of fighters: 31

Similarly, a subset of the data with the bantamweight division rankings was extracted. The total dataset had 52 unique fighters, which was reduced to 34 bantamweight fighters.

Number of fighters: 34

Between 2019-03-26 to 2019-05-13, there was no Bantamweight Champion. The missing values for the fighter column are replaced with “Vacant” to signify that there was no champion at that time.

For each fighter, the reversed rankings was computed. Instead of flipping the y-axis, the reversed ranking is used. Therefore when plotting the stacked bar chart, the rankings increase as we move up the y-axis, as demonstrated by the diagram below.

The visualizations were created using D3.js and Javascript. The chart shows the changes in rankings over time as fighters enter, leave, or climb the column.

The fighter bars were color-coded into three groups: active in UFC roster, fighting outside of the UFC, and inactive/retired, based on their status as of March 14th, 2020.

Animated Single Stacked Column Chart

This chart shows the UFC’s lightweight (155 lbs.) rankings over time. The pseudocode for creating the animation is presented below.

Screen shot of Lightweight Rankings.
// Load data files
// sequence.csv contains a numbered list of the dates where rankings were given, representing each time slice.
// weightclass-rankings.csv is a table where the rows are the fighters and the columns are their reversed ranking values for each time since.
// weightclass-fighters.json contains the color coding for each fighter.

Promise.all([
     d3.csv("sequence.csv"),
     d3.csv(weightclass+"-rankings.csv"),
     d3.json("weightclass+"-fighters.json"),
... 

const sequenceStart = 0;
const sequenceEnd = sequenceArray.length;

// Build initial svg layout
...
...

// Start animation
let ticker = d3.interval(e => { 

     // Call the function to obtain the rankings for the current sequence (time slice)
     sequenceValue = computeDataSlice();

     // Update labels and bars
     ...

     // Increment sequence until the end
     sequence++; 
     if(sequence> sequenceEnd) 
     ticker.stop();
}, delayDuration);

Single Stacked Column Chart with Interaction

This visualization shows the UFC’s bantamweight (135 lbs.) rankings over time. This interactive version allows the user to play/pause the animation and also toggle the slider to view the rankings on a specific date of interest. The column chart is updated using an event listener synced with the slider. When the user moves the slider, the chart is updated with the time slice corresponding to the point on the slider.

Screen shot of Bantamweight Rankings.

The code for this version is similar to the prior version, except a button and slider are added, and an interval timer is only initiated when the Start button is clicked. Here, the timer is initiated every time the button is clicked when displaying “Start” or “Resume”. When the timer starts, it calls the step function which updates the slider and the chart. The timer is cleared when the button is clicked while displaying “Pause” and the chart is unchanged. The pseudocode for the slider and play button is presented below.

// Add slider 
...
slider.append("line")
     .attr("class", "track")
     .attr("x1", xslider.range()[0])
     .attr("x2", xslider.range()[1])
     ...
     .call(d3.drag()
           .on("start.interrupt", function() { slider.interrupt(); })
            // This function is performed when the slider is moved
           .on("start drag", function() {
                 currentValue = d3.event.x;
                 currentValue = d3.max([0,currentValue]);
                 updateSlider(xslider.invert(currentValue));
                 updateChart(xslider.invert(currentValue));
            })
     );

...
...

var playButton = d3.select("#button");
playButton .on("click", function() { 
     var button = d3.select(this); 
     if (button.text() == "Pause") { 
          moving = false; 
          clearInterval(timer); 
          button.text("Resume"); 
      } else { 
          moving = true; 
          timer = setInterval(step, delayDuration);        
          button.text("Pause"); 
      } 
}) 

function step() { 
     updateSlider(...);
     updateChart(....); 
     // increment currentValue 
     if (currentValue > targetValue) { 
          moving = false; 
          currentValue = 0; 
          clearInterval(timer); 
          playButton.text("Start"); 
     } 
}

Full source code is available in the GitHub repository.

Related Links

Here are two videos that uses these data driven charts.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.