ProCyclingStats profile score activity

Would be possible to add this profile score to our activities? This would add a way to rate rides

1 Like

This would be awesome.


Profile score
The ā€˜PCS ProfileScore’ is mainly developed to assign points to riders for their climbing capabilities. Here we explain how it works.

After thorough data analysis we found that the number of vertical meters alone isn’t at all a good indication how hard a stage is. For example, a 10k climb at 8% gradient followed by 100k of flat road could very well end up in a bunch sprint. The same 10k climb in the final 20k of the race is probably a typical climbers stage. Furthermore, a 5k climb at 8% seems to be much more suitable for climbers vs. sprinters than a 10k 4% climb.
So we included three variables into our PCS ProfileScore formula:

Position of climb from finish
Steepness
Length of the climb

First we compute the score for each individual climb in the stage by the following formula:

([Steepness] / 2)^2 * [Length in KM]

Then we multiply this score by a factor dependent of the distance from the finish line.

Within the last N km Factor
10 1.0
25 0.8
50 0.6
75 0.4
before final 75k 0.2
Example
Case A: Flat stage with a 10k Ć  8% climb in the final 10k.
(8 / 2)^2 * 10 * 1 = 160

Case B: Flat stage with a 10k Ć  8% climb at 100k from the finish.
(8 / 2)^2 * 10 * 0.2 = 32

Case C: Stage with 2 climbs, a 10k Ć  10% climb at 40k from the finish and a 4k Ć  12% climb in the final 10k.
{(10 / 2)^2 * 10 * 0.6} + {(12 / 2)^2 * 4 * 1} = 294
ProfileScore final
Besides the profileScore for the complete race we also keep a profileScore on the final of the race. This is simply the same formula but then applied only to the last 25 kilometre of the race.
ClimbProfileScore
For climbs we use the same formula to compute the profileScore for a climb in itself. The distance to finish factor is excluded. Also, the minimum segment length for which the steepness is computed is 200 meter. This smoothes the score a bit in case of very short steep segments.
Profile icons
We make use of 5 different icons to give an indication of what kind of stage it is.
Flat
Hills, flat finish
Hills, uphill finish
Mountains, flat finish
Mountains, uphill finish

These icons are an indication of the type of stage within an event. Therefore it could be that a stage in one race with the same profile score is considered flat and in another race as hilly. If the ProfileScore is computed, this is often displayed behind the icon which is an absolute score of the stage difficulty.

1 Like

@ddddavidee I was considering becoming a supporter so this feature gets done faster, but you are a supporter and this is from 2 years ago…so probably no luck

Now with Java script and custom activity fields. All these are possible via end user intervention.

Since you know the process. You can implement it. There’s actually no need to wait. (Yeah. Needs some elbow grease and ā€œknow howā€)

2 Likes

I’d like to try.

Do you have any pointer or how to/example to understand how to do it?

Thanks in advance

You could start with this. I have put a climb detection algorithm into copilot together with the PCS description above.
This is what it responded, to create a custom field. You can use it or improve it yourself. As it is from AI, it may be wrong, I didn’t prove it.

{

// Konfigurationsvariablen
const SECTION_LENGTH = 50;
const MIN_SEGMENTS = 4;
const MIN_GRADE = 2;
const MAX_GRADE_END = 1;
const MAX_GAP_SEGMENTS = 5;
const MIN_DIFFICULTY = 25;

// Funktion zur Sicherung der Datenstrƶme
function getStreamData(streamName) {
    const stream = icu.streams.get(streamName);
    return stream && stream.data ? stream.data.map(value => value ?? 0) : Array(icu.streams.get("time").data.length).fill(0);
}

// Datenstrƶme abrufen
const altitude = getStreamData("fixed_altitude");
const distance = getStreamData("distance");
const distanceKm = distance.map(d => d / 1000);
const grade = getStreamData("grade_smooth");
const time = getStreamData("time");

// Distanz bis zum Ziel ermitteln
const totalDistance = distance[distance.length - 1];

// Anfangswerte des Hƶhenprofils korrigieren
const firstNonZeroAltitude = altitude.find(value => value !== 0);
if (firstNonZeroAltitude !== undefined) {
    for (let i = 0; i < altitude.length; i++) {
        if (altitude[i] === 0) {
            altitude[i] = firstNonZeroAltitude;
        } else {
            break;
        }
    }
}

// Abschnittsbildung
let sections = [];
let currentStartIndex = 0;

for (let i = 1; i < distance.length; i++) {
    const distDiff = distance[i] - distance[currentStartIndex];
    if (distDiff >= SECTION_LENGTH || i === distance.length - 1) {
        const altDiff = altitude[i] - altitude[currentStartIndex];
        const sectionGrade = (altDiff / distDiff) * 100;

        sections.push({
            sectionIndex: sections.length,
            startIndex: currentStartIndex,
            endIndex: i,
            distance: distDiff,
            elevation: altDiff,
            grade: sectionGrade
        });

        currentStartIndex = i;
    }
}

// Identifikation der Anstiege
let climbs = [];
let currentClimb = { startSegment: null, endSegment: null };

sections.forEach((section, index) => {
    if (currentClimb.startSegment === null && section.grade >= MIN_GRADE) {
        currentClimb.startSegment = section.sectionIndex;
    } else if (currentClimb.startSegment !== null && (section.grade < MAX_GRADE_END || index === sections.length - 1)) {
        currentClimb.endSegment = section.sectionIndex - 1;
        if (currentClimb.endSegment - currentClimb.startSegment >= MIN_SEGMENTS) {
            climbs.push({ ...currentClimb });
        }
        currentClimb = { startSegment: null, endSegment: null };
    }
});

// Zusammenfassen nahegelegener Anstiege
for (let i = 1; i < climbs.length; i++) {
    const prevClimb = climbs[i - 1];
    const currentClimb = climbs[i];
    const gapSegments = currentClimb.startSegment - prevClimb.endSegment;

    if (gapSegments <= MAX_GAP_SEGMENTS) {
        prevClimb.endSegment = currentClimb.endSegment;
        climbs.splice(i, 1);
        i--;
    }
}

// PCS-Berechnung
function computeProfileScore(climbs) {
    let totalScore = 0;

    climbs.forEach(climb => {
        const startIndex = sections[climb.startSegment].startIndex;
        const endIndex = sections[climb.endSegment].endIndex;
        const distToFinish = (totalDistance - distance[endIndex])/1000;

        const climbSections = sections.slice(climb.startSegment, climb.endSegment);
        const lengthKm = climbSections.reduce((sum, section) => sum + section.distance, 0) / 1000;
        const avgGrade = climbSections.reduce((sum, section) => sum + section.elevation, 0) / lengthKm / 1000 * 100;
        const baseScore = Math.pow(avgGrade / 2, 2) * lengthKm;

        let distanceFactor = 0.2;
        if (distToFinish <= 10) {
            distanceFactor = 1.0;
        } else if (distToFinish <= 25) {
            distanceFactor = 0.8;
        } else if (distToFinish <= 50) {
            distanceFactor = 0.6;
        } else if (distToFinish <= 75) {
            distanceFactor = 0.4;
        }

        totalScore += baseScore * distanceFactor;
    });

    return totalScore;
}

// PCS ProfileScore berechnen
pcs = computeProfileScore(climbs);
console.log("PCS ProfileScore:",pcs);

pcs;
}

2 Likes

I’d like to see all stuff I want implemented but of course it cannot happen.
Still, I think the app/website is very good a d deserves some (of my) support.

It seemes to work!!! That’s nice !
I’d tr to modify it to have a score computed per interval and a global one !

(at the moment a global score is shown on all intervals)

Can you get it for all your rides in the past?

Would you show the steps?

Thanks!

I have to click on the PCS Score field (that I’ve added) to trigger the computation for the ā€œoldā€ rides, let’s see with the next one if it will be auto-computed

Custom Activity fields are computed at import.
To calculate them for existing activities, use List View - Edit - Analyse and uncheck all boxes to keep former edits. That will calculate the Custom fields for your selection.

2 Likes

If you dont mind you could make the activity stream public so others can use it as well :slight_smile:

1 Like

Tell me more about how to do it and I’ll execute! :sweat_smile:

Is it possible to add a custom field as a column in the List View?

Because you’ve said it worked, I made those public. I’ve changed it, so that it works for intervals (search under fields) and as a custom field (serach under custom):
image

Yes, click in the tab Data → Fields and tick your custom interval field.

2 Likes

Thanks a lot !

I meant in the Activities → List View, like in the following pic
image

Sure, you can add it to the columns:

it does not appear on my side ! :smiling_face_with_tear:

If you have it as ā€œCustomā€ Field (not as Interval ā€œFieldā€), then it will appear under columns:

2 Likes