Distribution des zones d'allure
strava_pace_zonesAnalyze your weekly running distance by pace zones (Recovery to Anaerobic) and verify adherence to the 80/20 rule. Optionally input your lactate threshold pace for accurate zone calculation.
Instructions
Analyse la répartition des kilomètres par zone d'allure (Récupération, Facile, Aérobie, Seuil, VO2max, Anaérobie). Aide à vérifier la règle 80/20 (80% en zones basses).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| weeks | No | Nombre de semaines à analyser | |
| threshold_pace_min_km | No | Allure au seuil lactique au format 'M:SS' par km (ex: '4:30'). Si absent, estimée depuis les activités récentes. |
Implementation Reference
- src/analytics/analysisTools.ts:126-193 (registration)Registration of the 'strava_pace_zones' tool via server.registerTool() inside registerAnalysisTools().
server.registerTool( "strava_pace_zones", { title: "Distribution des zones d'allure", description: "Analyse la répartition des kilomètres par zone d'allure (Récupération, Facile, Aérobie, " + "Seuil, VO2max, Anaérobie). Aide à vérifier la règle 80/20 (80% en zones basses).", inputSchema: z.object({ weeks: z.number().int().min(1).max(26).default(8).describe("Nombre de semaines à analyser"), threshold_pace_min_km: z .string() .optional() .describe( "Allure au seuil lactique au format 'M:SS' par km (ex: '4:30'). " + "Si absent, estimée depuis les activités récentes." ), }), }, async ({ weeks, threshold_pace_min_km }) => { const afterEpoch = Math.floor(Date.now() / 1000) - weeks * 7 * 86400; const activities = await listAllActivities(afterEpoch); const runs = activities.filter((a) => a.type === "Run" && !a.trainer && a.distance > 500); // Determine threshold speed let thresholdSpeedMps: number; if (threshold_pace_min_km) { const [min, sec] = threshold_pace_min_km.split(":").map(Number); const secPerKm = min * 60 + (sec ?? 0); thresholdSpeedMps = 1000 / secPerKm; } else { // Estimate: top 10% of average speeds as proxy for threshold const speeds = runs.map((r) => r.average_speed).sort((a, b) => b - a); const top10 = speeds.slice(0, Math.max(1, Math.floor(speeds.length * 0.1))); const avgTopSpeed = top10.reduce((s, v) => s + v, 0) / top10.length; thresholdSpeedMps = avgTopSpeed * 0.9; // Threshold ~90% of best effort } const distribution = computePaceZoneDistribution(runs, thresholdSpeedMps); const thresholdPaceStr = formatPaceFromSecPerKm(speedToSecPerKm(thresholdSpeedMps)); const easyAndBelow = distribution .filter((z) => ["Recovery", "Easy", "Aerobic"].includes(z.zone)) .reduce((s, z) => s + z.percentage, 0); return { content: [ { type: "text", text: JSON.stringify( { periode: `${weeks} dernières semaines`, allure_seuil_estimee: `${thresholdPaceStr} /km`, zones: distribution, regle_80_20: { zones_basses_pct: easyAndBelow, zones_hautes_pct: 100 - easyAndBelow, objectif: "80% zones basses, 20% zones hautes", statut: easyAndBelow >= 75 ? "✓ Bonne polarisation" : "⚠ Trop de zones hautes", }, }, null, 2 ), }, ], }; } ); - src/analytics/analysisTools.ts:144-193 (handler)The async handler function for strava_pace_zones: fetches activities, determines threshold pace, calls computePaceZoneDistribution, and returns zones + 80/20 rule analysis.
async ({ weeks, threshold_pace_min_km }) => { const afterEpoch = Math.floor(Date.now() / 1000) - weeks * 7 * 86400; const activities = await listAllActivities(afterEpoch); const runs = activities.filter((a) => a.type === "Run" && !a.trainer && a.distance > 500); // Determine threshold speed let thresholdSpeedMps: number; if (threshold_pace_min_km) { const [min, sec] = threshold_pace_min_km.split(":").map(Number); const secPerKm = min * 60 + (sec ?? 0); thresholdSpeedMps = 1000 / secPerKm; } else { // Estimate: top 10% of average speeds as proxy for threshold const speeds = runs.map((r) => r.average_speed).sort((a, b) => b - a); const top10 = speeds.slice(0, Math.max(1, Math.floor(speeds.length * 0.1))); const avgTopSpeed = top10.reduce((s, v) => s + v, 0) / top10.length; thresholdSpeedMps = avgTopSpeed * 0.9; // Threshold ~90% of best effort } const distribution = computePaceZoneDistribution(runs, thresholdSpeedMps); const thresholdPaceStr = formatPaceFromSecPerKm(speedToSecPerKm(thresholdSpeedMps)); const easyAndBelow = distribution .filter((z) => ["Recovery", "Easy", "Aerobic"].includes(z.zone)) .reduce((s, z) => s + z.percentage, 0); return { content: [ { type: "text", text: JSON.stringify( { periode: `${weeks} dernières semaines`, allure_seuil_estimee: `${thresholdPaceStr} /km`, zones: distribution, regle_80_20: { zones_basses_pct: easyAndBelow, zones_hautes_pct: 100 - easyAndBelow, objectif: "80% zones basses, 20% zones hautes", statut: easyAndBelow >= 75 ? "✓ Bonne polarisation" : "⚠ Trop de zones hautes", }, }, null, 2 ), }, ], }; } ); - Input schema for strava_pace_zones: weeks (1-26, default 8) and optional threshold_pace_min_km string.
inputSchema: z.object({ weeks: z.number().int().min(1).max(26).default(8).describe("Nombre de semaines à analyser"), threshold_pace_min_km: z .string() .optional() .describe( "Allure au seuil lactique au format 'M:SS' par km (ex: '4:30'). " + "Si absent, estimée depuis les activités récentes." ), }), - src/analytics/metrics.ts:104-127 (helper)computePaceZoneDistribution: core helper that classifies runs into pace zones relative to threshold speed and returns distance/percentage per zone.
export function computePaceZoneDistribution( activities: StravaActivity[], thresholdSpeedMps: number ): PaceZoneDistribution[] { const runs = activities.filter((a) => a.type === "Run" && !a.trainer && a.distance > 0); const zoneDistances = new Map<PaceZone, number>(); for (const run of runs) { const zone = classifyPaceZone(run.average_speed, thresholdSpeedMps); zoneDistances.set(zone, (zoneDistances.get(zone) ?? 0) + run.distance); } const totalM = Array.from(zoneDistances.values()).reduce((s, v) => s + v, 0); const zones: PaceZone[] = ["Recovery", "Easy", "Aerobic", "Threshold", "VO2Max", "Anaerobic"]; return zones.map((zone) => { const distanceM = zoneDistances.get(zone) ?? 0; return { zone, distanceKm: metersToKm(distanceM), percentage: totalM > 0 ? Math.round((distanceM / totalM) * 100) : 0, }; }); } - src/analytics/metrics.ts:91-102 (helper)classifyPaceZone: classifies a single speed into a PaceZone (Recovery/Easy/Aerobic/Threshold/VO2Max/Anaerobic) based on ratio to threshold speed.
export function classifyPaceZone( speedMps: number, thresholdSpeedMps: number ): PaceZone { const ratio = speedMps / thresholdSpeedMps; if (ratio < 0.74) return "Recovery"; if (ratio < 0.84) return "Easy"; if (ratio < 0.91) return "Aerobic"; if (ratio < 0.97) return "Threshold"; if (ratio < 1.05) return "VO2Max"; return "Anaerobic"; }