Skip to content

Analyzing beatmaps and replays

abraker95 edited this page Sep 8, 2021 · 8 revisions

In this tutorial we will explore the gui, and how it's possible to analyze beatmap and replay data. No programming experience is needed for the first half of this tutorial.


The GUI

The gui is split into 4 main sections: Browsing, Spatial Viewing, Temporal Viewing, and Analysis & Controls.

  • Browsing - Has a file browser, Collections browser, and an Online browser. File browser allows you to browse local maps and replays. It is possible to load the maps and replays by dragging them inside the gui. Collections browser (incomplete) allows to load collection.db files to browser local maps sorted by collections. Online browser (incomplete) provides an online listing of beatmaps and replays.
  • Spatial Viewing - Also known as the map display. Displays all the notes as well any other visible layers
  • Temporal Viewing - Also known as the timeline display. Displays all the notes spread in time as well as player hits (incomplete)
  • Analysis & Controls - Allows to enable/disable layers, browse loaded replay data, and analyze data view Console

It is possible to resize the views and even collapse them so they are not in the way. For example, after loading the map it's possible to collapse the browser view so it doesn't clutter the screen.

Loading the data

For this example we will use the map Leaf - I (Maddy)[Terror].osu, which is provided in the unit_tests/maps folder. You can open it by drag and dropping the file in the view itself. After it is opened you will notice there

Moving the yellow timeline cursor on the bottom we will see notes appear in the map display. It is possible to zoom in and out of the timeline by hold-dragging the right mouse button.

Various layers (as available) can be enabled/disabled in the controls on right. By default only Hitobject outlines and Hitobject aimpoints are provided, and more can be added (Tutorial pending).

We can load replays in a similar fashion as we did with the map. We will use the Lead - I (Maddy) [Terror] replay_0.osr file provided in unit_tests/replays folder. Moving the timeline cursor on the bottom again, we will notice that there are orange and cyan streaks in the map viewer now. Orange streaks represent left button press and cyan streaks represent right button press. We will also notice that there is a new layer group called "replay" in which firebat9's username is displayed. By default, there are 2 layers provided: Replay cursor (which if you look closely in the map viewer, there is a small green cursor present), and Replay hold (which are the visible streaks). If we go into the Options > Std Settings menu it is possible to change visual settings such as the color of the cursor and streaks, cursor size, among other things.

Exploring the data

In the replays tab we can see more detailed information about the replay(s) loaded. If there are multiple replays loaded, it is possible to sort the replays by the present column (acc, combo, score, etc) by clicking on the respective column's title. Right clicking on a replay will reveal more options:

  • Set only visible - Disables all other replay layers
  • Locate replay - Opens folder with replay selected (incomplete)
  • Copy replay code to clipboard - Copies the code needed to access the replay in the Embedded Console
  • Copy score code to clipboard - Copies the code needed to access the replay's score data in the Embedded Console
  • Graphs - Plots replay data

Let's plot a graph of cursor velocity. If we right click on the replay go to Graphs > Create cursor velocity graph, a new graph will be created in the Graphs tab. Much like in the timeline, the graph has a movable yellow cursor that reflect the viewed time in the map display. Like the timeline, you can right click drag to zoom in/out. If you right click on the graph, more options will be shown. The important ones are:

  • X axis - set x-axis range
  • Y axis - set y-axis range
  • export - export data to a *.csv, *.png, and other formats

Analyzing the data

Now we are going to get into the more advanced stuff. A background in programming is recommended.

The list of graphing options we saw is not the complete list of what is available, and are provided for convenience in the gui. The is a many, many functions available for analysis, and many more that can be implemented. Analysis files are located in the analysis folder. For each gamemode there are 6 analysis files which group various types of analysis that can be done:

  • map_data.py - Basic functions to work with map data
  • map_metrics.py - Analysis on map data
  • replay_data.py - Basic function to work with replay data
  • replay_metrics.py - Analysis on relay data
  • score_data.py - Analysis on per-replay score data
  • score_metrics.py - Analysis on bulk-replay score data

Each file has a function that provides an entry point to deal with data. The following are functions that get data needed to deal with analysis:

  • map_data.py - map_data = StdMapData.get_map_data(get_beatmap().hitobjects)
  • map_metrics.py - map_data
  • replay_data.py - replay_data = get_replays()[n], where n is the nth replay. It is best to just get the replay code via option found by right clicking on the replay in the replay tab.
  • replay_metrics.py - replay_data
  • score_data.py - score_data = get_score_data(replay_data, map_data)
  • score_metrics.py - mass_score_data = get_per_hitobject_score_data([ score_data, ... ])

The data returned by those functions is a pandas data type with annotated columns. Each column is accessible as you would access a dictionary. For example, consider map_data is the following:

                        time      x      y  type  object
hitobject aimpoint                                      
0         0            557.0  300.0  324.0   1.0     1.0
          1            558.0  300.0  324.0   3.0     1.0
1         0            693.0  300.0  196.0   1.0     1.0
          1            694.0  300.0  196.0   3.0     1.0
2         0            829.0  410.0  260.0   1.0     2.0
                     ...    ...    ...   ...     ...
652       1         150952.0  460.0  112.0   3.0     2.0
653       0         151059.0  256.0  216.0   1.0     1.0
          1         151060.0  256.0  216.0   3.0     1.0
655       0         153059.0  256.0  192.0   1.0     1.0
          1         153060.0  256.0  192.0   3.0     1.0

To access the time column, simply do map_data['time']. The two columns on the left, hitobject and aimpoint are index columns. Data can be indexed in various ways:

  • map_data['time'][:, 0] - gives the time of first aimpoint of each hit object (if it exists)
  • map_data['time'][:, 1] - gives the time of second aimpoint of each hit object (if it exists)
  • map_data[['x', 'y']] - gives just the x and y columns
  • map_data.max(level=0) - gives all data for last aimpoint of each hitobject

For more info, refer to pandas indexing and selecting data page. Advanced data selection might require google searches including the term "multiindex"

Visualizing the data

For this section of the tutorial, we will visualize the player's hit and aim offsets.

To start, let's go into the console tab and get the map, replay, and score:

map_data = StdMapData.get_map_data(get_beatmap().hitobjects)
replay_data = StdReplayData.get_replay_data(get_replays()[0].play_data)
score_data = StdScoreData.get_score_data(replay_data, map_data)

Now lets separate hit offset data and aim offset data into their own parts:

hitoffset_data_t = score_data['replay_t'] - score_data['map_t']
aimoffset_data_x = score_data['replay_x'] - score_data['max_x']
aimoffset_data_y = score_data['replay_y'] - score_data['map_y']

Using matpltlib, we can graph the hitoffset data as a histogram plot:

import matplotlib.pyplot as plt
plt.hist(hitoffset_data, 20)
plt.xlabel('offset from 0 ms')
plt.ylabel('number of hits')
plt.show()

Now we can plot the points as a scatter plot:

import matplotlib
import matplotlib.pyplot as plt

# Draw a circle
cs = get_beatmap().difficulty.cs
cs_px = (109 - 9*cs)/2

fig, ax = plt.subplots()
ax.add_artist(matplotlib.patches.Circle((0, 0), radius=cs_px, color='#8080FE', fill=False))

# Draw points
plt.scatter(aimoffset_data_x, aimoffset_data_y, s=1)
plt.title('Aim offsets')

plt.xlim(-cs_px*1.5, cs_px*1.5)
plt.ylim(-cs_px*1.5, cs_px*1.5)

plt.show()