lastScore Results Data#
Accessing stage and overal results and timing data.
This is probably the data you’re most interested in, so let’s get stuck into it.
The data is defined at the category and stage level and includes stages results and timing data at each lastscore, as well as at stage end, as well as overall timing and results classification data.
# Load in the required packages
import pandas as pd
from jupyterlite_simple_cors_proxy import furl, xurl
# Generate the API URL pattern
dakar_api_template = "https://www.dakar.live.worldrallyraidchampionship.com/api/{path}"
# Define the year
YEAR = 2025
# Define the category
CATEGORY = "A"
# Define the stage
STAGE = 1
# Define the API path to the stage resource
# Use a Python f-string to instantiate variable values directly
lastscore_path = f"lastScore-{YEAR}-{CATEGORY}-{STAGE}"
# Define the URL
lastscore_url = dakar_api_template.format(path=lastscore_path)
# Preview the path and the URL
lastscore_path, lastscore_url
('lastScore-2025-A-1',
'https://www.dakar.live.worldrallyraidchampionship.com/api/lastScore-2025-A-1')
The JSON object returned by the live data service for the lastScore results feeds is quite a deepy nested data structure that returns a complex dataframe is parsed directly using the read_json() function:
# Load in data
# Use furl() to handle CORS issues in Jupyterlite
_lastscore_df = pd.read_json(furl(lastscore_url))
_lastscore_df.head(2)
| team | dss | bonif | cg | cs | ce | _bind | _updatedAt | _id | fsh | wd | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | {'bib': 427, 'clazz': '9a68ed3c41c5c7a1642df5d... | {'position': 130, 'absolute': 40650000, 'inter... | {'total': 0, 'wp': []} | {'01216': {'position': [128, 128], 'absolute':... | {'01207': {'position': [129, 129], 'absolute':... | {'position': [125, 125], 'absolute': [23353000... | lastScore-2025-A-1 | 1737386238243 | lastScore-2025-A-1-427 | NaN | NaN |
| 1 | {'bib': 634, 'clazz': 'ec2f26ebeb14824160c7204... | {'position': 171, 'absolute': 41910000, 'inter... | {'total': 0, 'wp': []} | {'01216': {'position': [194, 194], 'absolute':... | {'01207': {'position': [179, 179], 'absolute':... | {'position': [196, 197], 'absolute': [11880000... | lastScore-2025-A-1 | 1737386237448 | lastScore-2025-A-1-634 | NaN | NaN |
We can get a much better behaved data structure if we parse the JSON directly using the pd.json_normalize() function:
import requests
pd.json_normalize(requests.get(lastscore_url).json()).head()
| _bind | _updatedAt | _id | team.bib | team.clazz | team.brand | team.model | team.competitors | team.vehicle | team.vehicleImg | ... | ce.relative | ce.bonus | cg.01ASS.penality | cs.01ASS.penality | ce.penality | cg.01227.position | cg.01227.absolute | cg.01227.relative | fsh | wd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1 | 1737386238243 | lastScore-2025-A-1-427 | 427 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | [{'name': 'B. LEPIETRE', 'firstName': 'BENOIT'... | BTR | https://img.aso.fr/core_app/img-motorSports-da... | ... | [6845000, 6845000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | lastScore-2025-A-1 | 1737386237448 | lastScore-2025-A-1-634 | 634 | ec2f26ebeb14824160c7204618a5780d | DAF | FAV 85 MX | [{'name': 'J. ESTEVE ORO', 'firstName': 'JORDI... | TIBAU TEAM | https://img.aso.fr/core_app/img-motorSports-da... | ... | [102292000, 102292000] | 0 | 79200000.0 | 79200000.0 | 79200000.0 | NaN | NaN | NaN | NaN | NaN |
| 2 | lastScore-2025-A-1 | 1737386237841 | lastScore-2025-A-1-330 | 330 | a0a6386a4b9a61b73b036a50966345c0 | TAURUS | T3 MAX | [{'name': 'A. ALKUWARI', 'firstName': 'AHMED F... | NASSER RACING | https://img.aso.fr/core_app/img-motorSports-da... | ... | [1672000, 1672000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | lastScore-2025-A-1 | 1737386238131 | lastScore-2025-A-1-243 | 243 | f00d7ec8d2d96e9cf11aa515109376cf | MD | OPTIMUS | [{'name': 'P. THOMASSE', 'firstName': 'PASCAL'... | MD RALLYE SPORT | https://img.aso.fr/core_app/img-motorSports-da... | ... | [2160000, 2160000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 4 | lastScore-2025-A-1 | 1737386238128 | lastScore-2025-A-1-404 | 404 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | [{'name': 'F. LOPEZ CONTARDO', 'firstName': 'F... | CAN-AM FACTORY TEAM | https://img.aso.fr/core_app/img-motorSports-da... | ... | [1510000, 1510000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 88 columns
# Rather than re-download the JSON data using requests,
# (remembering to use xurl() for JupyterLite CORS handling)
# we can convert the dataframe back to JSON
_lastscore_json = _lastscore_df.to_dict('records')
# Now we can use the more powerful json_normalize() function
# to generate a dataframe from the data
_results_df = pd.json_normalize(_lastscore_json)
_results_df.head()
| _bind | _updatedAt | _id | fsh | wd | team.bib | team.clazz | team.brand | team.model | team.competitors | ... | ce.position | ce.absolute | ce.relative | ce.bonus | cg.01ASS.penality | cs.01ASS.penality | ce.penality | cg.01227.position | cg.01227.absolute | cg.01227.relative | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1 | 1737386238243 | lastScore-2025-A-1-427 | NaN | NaN | 427 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | [{'name': 'B. LEPIETRE', 'firstName': 'BENOIT'... | ... | [125, 125] | [23353000, 23353000] | [6845000, 6845000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | lastScore-2025-A-1 | 1737386237448 | lastScore-2025-A-1-634 | NaN | NaN | 634 | ec2f26ebeb14824160c7204618a5780d | DAF | FAV 85 MX | [{'name': 'J. ESTEVE ORO', 'firstName': 'JORDI... | ... | [196, 197] | [118800000, 118800000] | [102292000, 102292000] | 0 | 79200000.0 | 79200000.0 | 79200000.0 | NaN | NaN | NaN |
| 2 | lastScore-2025-A-1 | 1737386237841 | lastScore-2025-A-1-330 | NaN | NaN | 330 | a0a6386a4b9a61b73b036a50966345c0 | TAURUS | T3 MAX | [{'name': 'A. ALKUWARI', 'firstName': 'AHMED F... | ... | [43, 43] | [18180000, 18180000] | [1672000, 1672000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | lastScore-2025-A-1 | 1737386238131 | lastScore-2025-A-1-243 | NaN | NaN | 243 | f00d7ec8d2d96e9cf11aa515109376cf | MD | OPTIMUS | [{'name': 'P. THOMASSE', 'firstName': 'PASCAL'... | ... | [50, 50] | [18668000, 18668000] | [2160000, 2160000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 4 | lastScore-2025-A-1 | 1737386238128 | lastScore-2025-A-1-404 | NaN | NaN | 404 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | [{'name': 'F. LOPEZ CONTARDO', 'firstName': 'F... | ... | [34, 34] | [18018000, 18018000] | [1510000, 1510000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 88 columns
Inspection of the dataframe column names shows how the .json_normalize() function flattened nested objects into and created flattened, dotted column names that reveal the original nested object data paths.
_results_df.columns
Index(['_bind', '_updatedAt', '_id', 'fsh', 'wd', 'team.bib', 'team.clazz',
'team.brand', 'team.model', 'team.competitors', 'team.vehicle',
'team.vehicleImg', 'team.w2rc', 'dss.position', 'dss.absolute',
'dss.real', 'bonif.total', 'bonif.wp', 'cg.01216.position',
'cg.01216.absolute', 'cg.01216.relative', 'cg.01218.position',
'cg.01218.absolute', 'cg.01218.relative', 'cg.01224.position',
'cg.01224.absolute', 'cg.01224.relative', 'cg.01207.position',
'cg.01207.absolute', 'cg.01207.relative', 'cg.01233.position',
'cg.01233.absolute', 'cg.01233.relative', 'cg.01230.position',
'cg.01230.absolute', 'cg.01230.relative', 'cg.01222.position',
'cg.01222.absolute', 'cg.01222.relative', 'cg.01220.position',
'cg.01220.absolute', 'cg.01220.relative', 'cg.01ASS.position',
'cg.01ASS.absolute', 'cg.01ASS.relative', 'cg.01ASS.bonus',
'cg.01ASS.stagePenalty', 'cs.01207.position', 'cs.01207.absolute',
'cs.01207.relative', 'cs.01218.position', 'cs.01218.absolute',
'cs.01218.relative', 'cs.01220.position', 'cs.01220.absolute',
'cs.01220.relative', 'cs.01216.position', 'cs.01216.absolute',
'cs.01216.relative', 'cs.01227.position', 'cs.01227.absolute',
'cs.01227.relative', 'cs.01230.position', 'cs.01230.absolute',
'cs.01230.relative', 'cs.01222.position', 'cs.01222.absolute',
'cs.01222.relative', 'cs.01233.position', 'cs.01233.absolute',
'cs.01233.relative', 'cs.01224.position', 'cs.01224.absolute',
'cs.01224.relative', 'cs.01ASS.position', 'cs.01ASS.absolute',
'cs.01ASS.relative', 'cs.01ASS.bonus', 'ce.position', 'ce.absolute',
'ce.relative', 'ce.bonus', 'cg.01ASS.penality', 'cs.01ASS.penality',
'ce.penality', 'cg.01227.position', 'cg.01227.absolute',
'cg.01227.relative'],
dtype='object')
We might also notice that as well as the cg and cs results fields, which are annotated with what we might assume to be waypoint identifiers, the dataframe includes team and crew information which is not directly associated with a particular stage, but is rather more generic. There are also stage start (dss) and stage end (ce) indications.
It therefore makes sense to extract this data from the data frame, so that we might store it separately (we might also assume that the crew and team information will be fixed throughout the rally? Or are crew changes allowed during the rally?)
The following function
import pandas as pd
from typing import Tuple
def normalize_team_competitors(df: pd.DataFrame, year: int = 2025) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
"""
Transform a DataFrame containing nested competitor lists into three slighly more normalized DataFrames.
Args:
df (pd.DataFrame): Input DataFrame with columns including 'team.bib', 'team.model', and 'team.competitors' (list of dicts)
Returns:
Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: (teams_df, competitors_df, results_df)
- teams_df: DataFrame with team information
- competitors_df: DataFrame with competitor information
- results_df: DataFrame of results;
"""
# Create competitors DataFrame using explode and json_normalize
competitors_df = (
df[['team.bib', 'team.competitors']]
.explode('team.competitors')
.reset_index(drop=True)
)
# Normalize the dictionary contents and combine with team.bib
competitors_df = pd.concat([
competitors_df['team.bib'],
pd.json_normalize(competitors_df['team.competitors'])
], axis=1)
competitors_df["year"] = year
# Create teams DataFrame by dropping the competitors column
teams_df = df.drop('team.competitors', axis=1)
team_cols = [c for c in teams_df.columns if c.startswith(
"team")]
teams_df = teams_df[team_cols]
team_cols.remove("team.bib")
team_cols.append("team.competitors")
return teams_df, competitors_df, df.drop(team_cols, axis=1)
Applying this function splits out the team, competitor, and results data.
teams_df, competitors_df, _results = normalize_team_competitors(_results_df)
# Preview the team data
teams_df.head()
| team.bib | team.clazz | team.brand | team.model | team.vehicle | team.vehicleImg | team.w2rc | |
|---|---|---|---|---|---|---|---|
| 0 | 427 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | BTR | https://img.aso.fr/core_app/img-motorSports-da... | False |
| 1 | 634 | ec2f26ebeb14824160c7204618a5780d | DAF | FAV 85 MX | TIBAU TEAM | https://img.aso.fr/core_app/img-motorSports-da... | False |
| 2 | 330 | a0a6386a4b9a61b73b036a50966345c0 | TAURUS | T3 MAX | NASSER RACING | https://img.aso.fr/core_app/img-motorSports-da... | False |
| 3 | 243 | f00d7ec8d2d96e9cf11aa515109376cf | MD | OPTIMUS | MD RALLYE SPORT | https://img.aso.fr/core_app/img-motorSports-da... | False |
| 4 | 404 | 9a68ed3c41c5c7a1642df5d93458baa6 | BRP | CAN-AM MAVERICK R | CAN-AM FACTORY TEAM | https://img.aso.fr/core_app/img-motorSports-da... | False |
# Preview the competitors data
competitors_df.head()
| team.bib | name | firstName | lastName | role | gender | nationality | profil | profil_sm | podium | aid | year | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 427 | B. LEPIETRE | BENOIT | LEPIETRE | P | m | fra | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | 110add16-df63-4b90-9494-bc3db7d02662 | 2025 |
| 1 | 427 | R. RELMY-MADINSKA | RODRIGUE | RELMY-MADINSKA | C | m | fra | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | 63cb271c-1c74-4f07-82b4-c3df3c425aa7 | 2025 |
| 2 | 634 | J. ESTEVE ORO | JORDI | ESTEVE ORO | P | m | esp | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | 36106f23-d545-47b9-8565-983cce7550d3 | 2025 |
| 3 | 634 | F. PARDO | FRANCESC | PARDO | C | m | esp | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | 43c1ce16-ccee-4ac2-ae16-b89e82a43183 | 2025 |
| 4 | 634 | J. PUJOL FORNOS | JORDI | PUJOL FORNOS | M | m | esp | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | https://img.aso.fr/core_app/img-motorSports-da... | 7700b503-68b6-4aca-a63a-96047071c2e9 | 2025 |
# Preview the results data
_results.head()
| _bind | _updatedAt | _id | fsh | wd | team.bib | dss.position | dss.absolute | dss.real | bonif.total | ... | ce.position | ce.absolute | ce.relative | ce.bonus | cg.01ASS.penality | cs.01ASS.penality | ce.penality | cg.01227.position | cg.01227.absolute | cg.01227.relative | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1 | 1737386238243 | lastScore-2025-A-1-427 | NaN | NaN | 427 | 130 | 40650000 | True | 0 | ... | [125, 125] | [23353000, 23353000] | [6845000, 6845000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | lastScore-2025-A-1 | 1737386237448 | lastScore-2025-A-1-634 | NaN | NaN | 634 | 171 | 41910000 | True | 0 | ... | [196, 197] | [118800000, 118800000] | [102292000, 102292000] | 0 | 79200000.0 | 79200000.0 | 79200000.0 | NaN | NaN | NaN |
| 2 | lastScore-2025-A-1 | 1737386237841 | lastScore-2025-A-1-330 | NaN | NaN | 330 | 55 | 38370000 | True | 0 | ... | [43, 43] | [18180000, 18180000] | [1672000, 1672000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | lastScore-2025-A-1 | 1737386238131 | lastScore-2025-A-1-243 | NaN | NaN | 243 | 67 | 38730000 | True | 0 | ... | [50, 50] | [18668000, 18668000] | [2160000, 2160000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
| 4 | lastScore-2025-A-1 | 1737386238128 | lastScore-2025-A-1-404 | NaN | NaN | 404 | 72 | 38880000 | True | 0 | ... | [34, 34] | [18018000, 18018000] | [1510000, 1510000] | 0 | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 81 columns
The results dataframe still looks rather cluttered. Inspection of the data, and comparison back with the parent webiste, suggests the following interpretation for some of the results data columns:
ce: end of stage statusdss: stage start status
For waypoints:
cg: overall status at waypointcs: stage status at waypoint
Ideally, we would like to be able retrieve different collections of results data in a natural way.
For example, we might want to query:
the end of stage positions for each crew;
the positions of each crew at a particular waypoint;
time differences or gap to leader at stage end, or at a particular waypoint;
the position of a particular crew at each waypoint.
Having the data in a particular form can often make it easier — or harder — to make these sorts of query.
id_column = "_id"
point_cols = [col for col in _results.columns if col.startswith(('cg', 'cs'))]
# Melt only the point-specific columns
melted = _results[[id_column, "team.bib", *point_cols]
].melt(id_vars=[id_column, "team.bib"]).dropna()
melted = melted[melted['variable'].str.contains('position|absolute|relative')]
melted["type"] = melted["variable"].str.split('.').str[0]
melted["waypoint"] = melted["variable"].str.extract(r'\.([^\.]+)\.')
melted["metric"] = melted["variable"].str.split('.').str[-1]
melted[['value_0', 'value_1']] = pd.DataFrame(
melted['value'].tolist(),
index=melted.index
)
melted.drop(columns=["variable", "value"], inplace=True)
melted
| _id | team.bib | type | waypoint | metric | value_0 | value_1 | |
|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | cg | 01216 | position | 128 | 128 |
| 1 | lastScore-2025-A-1-634 | 634 | cg | 01216 | position | 194 | 194 |
| 2 | lastScore-2025-A-1-330 | 330 | cg | 01216 | position | 53 | 53 |
| 3 | lastScore-2025-A-1-243 | 243 | cg | 01216 | position | 60 | 60 |
| 4 | lastScore-2025-A-1-404 | 404 | cg | 01216 | position | 65 | 65 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 12569 | lastScore-2025-A-1-501 | 501 | cg | 01227 | position | 1 | 1 |
| 12680 | lastScore-2025-A-1-500 | 500 | cg | 01227 | absolute | 13622000 | 13622000 |
| 12770 | lastScore-2025-A-1-501 | 501 | cg | 01227 | absolute | 13510000 | 13510000 |
| 12881 | lastScore-2025-A-1-500 | 500 | cg | 01227 | relative | 112000 | 112000 |
| 12971 | lastScore-2025-A-1-501 | 501 | cg | 01227 | relative | 0 | 0 |
11142 rows × 7 columns
The absolute and relative times are both in milliseconds, so we can divide those down to provide a more natural time unit of seconds.
# We want the metric values as ints, but we go via a float
melted[["value_0", "value_1"]] = melted[["value_0", "value_1"]].astype(float)
melted.loc[melted["metric"].isin(["absolute", "relative"]), ["value_0", "value_1"]] /= 1000
melted[["value_0", "value_1"]] = melted[["value_0", "value_1"]].astype(int)
melted
| _id | team.bib | type | waypoint | metric | value_0 | value_1 | |
|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | cg | 01216 | position | 128 | 128 |
| 1 | lastScore-2025-A-1-634 | 634 | cg | 01216 | position | 194 | 194 |
| 2 | lastScore-2025-A-1-330 | 330 | cg | 01216 | position | 53 | 53 |
| 3 | lastScore-2025-A-1-243 | 243 | cg | 01216 | position | 60 | 60 |
| 4 | lastScore-2025-A-1-404 | 404 | cg | 01216 | position | 65 | 65 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 12569 | lastScore-2025-A-1-501 | 501 | cg | 01227 | position | 1 | 1 |
| 12680 | lastScore-2025-A-1-500 | 500 | cg | 01227 | absolute | 13622 | 13622 |
| 12770 | lastScore-2025-A-1-501 | 501 | cg | 01227 | absolute | 13510 | 13510 |
| 12881 | lastScore-2025-A-1-500 | 500 | cg | 01227 | relative | 112 | 112 |
| 12971 | lastScore-2025-A-1-501 | 501 | cg | 01227 | relative | 0 | 0 |
11142 rows × 7 columns
The category and stage indicators are currently embedded in the index value, which has the form lastScore-YEAR-CATEGORY-STAGE-BIB. We could unpack these values to make it easier to structure searches around those conditions.
melted[['year', 'category', 'stage']] = melted['_id'].str.extract(
r'lastScore-(\d{4})-([A-Z])-([\d]+)')
melted.head()
| _id | team.bib | type | waypoint | metric | value_0 | value_1 | year | category | stage | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | cg | 01216 | position | 128 | 128 | 2025 | A | 1 |
| 1 | lastScore-2025-A-1-634 | 634 | cg | 01216 | position | 194 | 194 | 2025 | A | 1 |
| 2 | lastScore-2025-A-1-330 | 330 | cg | 01216 | position | 53 | 53 | 2025 | A | 1 |
| 3 | lastScore-2025-A-1-243 | 243 | cg | 01216 | position | 60 | 60 | 2025 | A | 1 |
| 4 | lastScore-2025-A-1-404 | 404 | cg | 01216 | position | 65 | 65 | 2025 | A | 1 |
We also need to handle the stage start (dss) and end (ce) data.
ce_cols = [col for col in _results.columns if col.startswith('ce')]
melted_ce = _results[[id_column, "team.bib", *ce_cols]].copy()
melted_ce['ce.bonus'] = melted_ce['ce.bonus'].astype(object)
melted_ce.loc[:, "ce.bonus"] = melted_ce['ce.bonus'].apply(lambda x: [x, x])
melted_ce = melted_ce.melt(id_vars=[id_column, "team.bib"]).dropna()
melted_ce = melted_ce[melted_ce['variable'].str.contains('position|absolute|relative`|bonus')]
melted_ce["metric"] = melted_ce["variable"].str.split('.').str[-1]
melted_ce[['value_0', 'value_1']] = pd.DataFrame(
melted_ce['value'].tolist(),
index=melted_ce.index
)
melted_ce.drop(columns=["variable", "value"], inplace=True)
melted_ce[["value_0", "value_1"]] = melted_ce[[
"value_0", "value_1"]].astype(float)
melted_ce.loc[melted_ce["metric"].isin(["absolute", "relative"]), [
"value_0", "value_1"]] /= 1000
melted_ce[["value_0", "value_1"]] = melted_ce[[
"value_0", "value_1"]].astype(int)
melted_ce
| _id | team.bib | metric | value_0 | value_1 | |
|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | position | 125 | 125 |
| 1 | lastScore-2025-A-1-634 | 634 | position | 196 | 197 |
| 2 | lastScore-2025-A-1-330 | 330 | position | 43 | 43 |
| 3 | lastScore-2025-A-1-243 | 243 | position | 50 | 50 |
| 4 | lastScore-2025-A-1-404 | 404 | position | 34 | 34 |
| ... | ... | ... | ... | ... | ... |
| 799 | lastScore-2025-A-1-212 | 212 | bonus | 0 | 0 |
| 800 | lastScore-2025-A-1-208 | 208 | bonus | 0 | 0 |
| 801 | lastScore-2025-A-1-313 | 313 | bonus | 0 | 0 |
| 802 | lastScore-2025-A-1-628 | 628 | bonus | 0 | 0 |
| 803 | lastScore-2025-A-1-318 | 318 | bonus | 0 | 0 |
603 rows × 5 columns
dss_cols = [col for col in _results.columns if col.startswith('dss')]
melted_dss = _results[[id_column, "team.bib", *dss_cols]].copy()
melted_dss = melted_dss.melt(id_vars=[id_column, "team.bib"]).dropna()
melted_dss = melted_dss[melted_dss['variable'].str.contains(
'position|absolute')]
melted_dss["metric"] = melted_dss["variable"].str.split('.').str[-1]
melted_dss.drop(columns=["variable"], inplace=True)
melted_dss["value_0"] = melted_dss["value"].astype(float)
melted_dss.loc[melted_dss["metric"].isin(["absolute"]), [
"value_0"]] /= 1000
melted_dss["value_0"] = melted_dss[
"value_0"].astype(int)
melted_dss["value_1"] = melted_dss[
"value_0"]
melted_dss.drop(columns="value", inplace=True)
melted_dss
| _id | team.bib | metric | value_0 | value_1 | |
|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | position | 130 | 130 |
| 1 | lastScore-2025-A-1-634 | 634 | position | 171 | 171 |
| 2 | lastScore-2025-A-1-330 | 330 | position | 55 | 55 |
| 3 | lastScore-2025-A-1-243 | 243 | position | 67 | 67 |
| 4 | lastScore-2025-A-1-404 | 404 | position | 72 | 72 |
| ... | ... | ... | ... | ... | ... |
| 397 | lastScore-2025-A-1-212 | 212 | absolute | 37020 | 37020 |
| 398 | lastScore-2025-A-1-208 | 208 | absolute | 34620 | 34620 |
| 399 | lastScore-2025-A-1-313 | 313 | absolute | 39240 | 39240 |
| 400 | lastScore-2025-A-1-628 | 628 | absolute | 42630 | 42630 |
| 401 | lastScore-2025-A-1-318 | 318 | absolute | 40500 | 40500 |
402 rows × 5 columns
melted_ce = pd.concat([melted_ce, melted_dss], ignore_index=True)
melted_ce[['year', 'category', 'stage']] = melted_ce['_id'].str.extract(
r'lastScore-(\d{4})-([A-Z])-([\d]+)')
melted_ce
| _id | team.bib | metric | value_0 | value_1 | year | category | stage | |
|---|---|---|---|---|---|---|---|---|
| 0 | lastScore-2025-A-1-427 | 427 | position | 125 | 125 | 2025 | A | 1 |
| 1 | lastScore-2025-A-1-634 | 634 | position | 196 | 197 | 2025 | A | 1 |
| 2 | lastScore-2025-A-1-330 | 330 | position | 43 | 43 | 2025 | A | 1 |
| 3 | lastScore-2025-A-1-243 | 243 | position | 50 | 50 | 2025 | A | 1 |
| 4 | lastScore-2025-A-1-404 | 404 | position | 34 | 34 | 2025 | A | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1000 | lastScore-2025-A-1-212 | 212 | absolute | 37020 | 37020 | 2025 | A | 1 |
| 1001 | lastScore-2025-A-1-208 | 208 | absolute | 34620 | 34620 | 2025 | A | 1 |
| 1002 | lastScore-2025-A-1-313 | 313 | absolute | 39240 | 39240 | 2025 | A | 1 |
| 1003 | lastScore-2025-A-1-628 | 628 | absolute | 42630 | 42630 | 2025 | A | 1 |
| 1004 | lastScore-2025-A-1-318 | 318 | absolute | 40500 | 40500 | 2025 | A | 1 |
1005 rows × 8 columns