Skip to main content

The intensity-duration modelling toolkit for endurance sports. Scikit-learn compatible.

Project description

Silhouette

The intensity-duration modelling toolkit for endurance sports. Scikit-learn compatible.

Try the interactive playground 🚀

Models

Power (cycling)

Model Parameters
TwoParamCriticalPowerRegressor CP, W'
ThreeParamCriticalPowerRegressor CP, W', P_max
OmniDomainPowerRegressor CP, W', P_max, a, tcp_max
ExpPowerRegressor CP, P_max, tau
MinimalPowerPowerRegressor ⚠️ experimental MAP, MAP duration, gamma_l, gamma_s
FPCAPowerRegressor FPC1, FPC2, FPC3

Speed (running)

Model Parameters
TwoParamCriticalSpeedRegressor CS, D'
ThreeParamCriticalSpeedRegressor CS, D', S_max
ExpSpeedRegressor ⚠️ experimental CS, S_max, tau
OmniDomainSpeedRegressor ⚠️ experimental CS, D', S_max, a, tcp_max
MinimalPowerSpeedRegressor MAS, MAS duration, gamma_l, gamma_s

Installation

uv add silhouette

Or with pip:

pip install silhouette

Quick start

Power models (cycling)

import numpy as np
from silhouette import OmniDomainPowerRegressor

durations = np.array([5, 10, 30, 60, 120, 300, 600, 1200, 1800, 3600])
power = np.array([1050, 850, 600, 480, 400, 340, 310, 290, 275, 255])

reg = OmniDomainPowerRegressor()
reg.fit(durations.reshape(-1, 1), power)

reg.cp_       # critical power (W)
reg.p_max_    # peak power (W)
reg.w_prime_  # anaerobic work capacity (J)

reg.predict(np.array([[300]]))  # predicted power at 5 minutes

All parametric models share the same interface. Swap OmniDomainPowerRegressor for TwoParamCriticalPowerRegressor or ThreeParamCriticalPowerRegressor and the code works the same way.

Speed models (running)

from silhouette import TwoParamCriticalSpeedRegressor

durations = np.array([120, 180, 300, 600, 900])
speed = np.array([5.8, 5.4, 5.0, 4.6, 4.4])

reg = TwoParamCriticalSpeedRegressor()
reg.fit(durations.reshape(-1, 1), speed)

reg.cs_       # critical speed (m/s)
reg.d_prime_  # distance capacity above CS (m)

Speed models use the same formulas as their power counterparts, with domain-appropriate parameter names, bounds, and defaults.

FPCA model

from silhouette import FPCAPowerRegressor

reg = FPCAPowerRegressor.from_model()
reg.fit(durations.reshape(-1, 1), power)

reg.fpc1_     # overall power level
reg.fpc2_     # sprint vs endurance bias
reg.fpc3_     # mid-duration specialization

reg.predict(np.array([[300]]))
reg.percentiles()  # {"fpc1": 72.3, "fpc2": 34.1, "fpc3": 55.8}
reg.z_scores()     # {"fpc1": 0.87, "fpc2": -0.41, "fpc3": 0.14}

Known parameters

When parameters are already known, use curve directly without fitting:

from silhouette import TwoParamCriticalPowerRegressor, TwoParamCriticalSpeedRegressor

t = np.arange(1, 3601)
power = TwoParamCriticalPowerRegressor.curve(t, cp=250, w_prime=20_000)
speed = TwoParamCriticalSpeedRegressor.curve(t, cs=4, d_prime=200)

Duration range

Each model is designed for a specific duration range:

Model Recommended range
Two-parameter 2-15 min
Three-parameter up to 15 min
Exponential up to 15 min
Omni-domain any
Minimal power 1 min+
FPCA any

A warning is issued when data falls outside the recommended range. Use duration_range to restrict which data points are used for fitting:

reg = TwoParamCriticalPowerRegressor(duration_range=(120, 900))
reg.fit(X, power)  # only uses data between 2 and 15 minutes

reg.predict(X)           # predict still works at any duration
reg.duration_mask_       # boolean mask of which points were used

Custom bounds

reg = OmniDomainPowerRegressor(
    bounds={"cp": (200, 400), "p_max": (800, 1500)},
    initial_params={"cp": 280},
)

Fitting methods

The two-parameter models support an alternative fitting method that minimizes error in work/distance space instead of power/speed space:

reg = TwoParamCriticalPowerRegressor(fitting="work_duration")
reg.fit(X, power)

This linearizes the model to W = W' + CP·t and fits via OLS, giving more weight to longer durations. The default (fitting="nonlinear") minimizes error in power space.

Time to exhaustion

The inverse of the power-duration curve: given a power, how long can it be sustained?

# On a fitted model
tte = reg.predict_inverse(np.array([250, 300, 350]))

# With known parameters
tte = TwoParamCriticalPowerRegressor.curve_inverse(350, cp=250, w_prime=20_000)

Plotting

Install with plotting support:

uv add silhouette[plotting]

Plot data with fitted models (sklearn Display pattern):

from silhouette.plotting import PowerDurationDisplay

# Single model
display = PowerDurationDisplay.from_estimator(reg, durations.reshape(-1, 1), power)

# Compare models
display = PowerDurationDisplay.from_estimators(
    [reg_2p, reg_omni], durations.reshape(-1, 1), power,
)

Power-Duration Models

FPCA mode of variance:

from silhouette.plotting import ModeOfVarianceDisplay

display = ModeOfVarianceDisplay.from_estimator(fpca_reg)

Mode of Variance

Minimal power model (normalized coordinates with reference band):

from silhouette.plotting import MinimalPowerDisplay

display = MinimalPowerDisplay.from_estimator(reg_minimal, durations.reshape(-1, 1), power)

Minimal Power Model

References

  • Monod, H., & Scherrer, J. (1965). The work capacity of a synergic muscular group. Ergonomics, 8(3), 329-338.
  • Hopkins, W. G., Edmond, I. M., Hamilton, B. H., Macfarlane, D. J., & Ross, B. H. (1989). Relation between power and endurance for treadmill running of short duration. Ergonomics, 32(12), 1565-1571.
  • Morton, R. H. (1996). A 3-parameter critical power model. Ergonomics, 39(4), 611-619.
  • Mulligan, M., Adam, G., & Emig, T. (2018). A minimal power model for human running performance. PloS one, 13(11), e0206645.
  • Puchowicz, M. J., Baker, J., & Clarke, D. C. (2020). Development and field validation of an omni-domain power-duration model. Journal of Sports Sciences, 38(7), 801-813.
  • Puchowicz, M. J., & Skiba, P. F. (2025). Functional Data Analysis of the Power-Duration Relationship in Cyclists. International Journal of Sports Physiology and Performance, 1(aop), 1-10.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

silhouette-0.5.0.tar.gz (69.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

silhouette-0.5.0-py3-none-any.whl (73.3 kB view details)

Uploaded Python 3

File details

Details for the file silhouette-0.5.0.tar.gz.

File metadata

  • Download URL: silhouette-0.5.0.tar.gz
  • Upload date:
  • Size: 69.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for silhouette-0.5.0.tar.gz
Algorithm Hash digest
SHA256 959c336d5e3e6c8a76385f4e7601b09ddfe0480ca8ab25b5708104084cafab69
MD5 d06b9d161722b7507543bbf0da96ff28
BLAKE2b-256 dbf8a9878c14923262aa0c97823354fd19bc8c646356b41ad62247eeded3202a

See more details on using hashes here.

File details

Details for the file silhouette-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: silhouette-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 73.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for silhouette-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8e6c4c48828e14d338880d034ccb70533fd71715698ec85bfc00fb467e1dc558
MD5 e3f5c2632a4d2ea8b1fb3ec1a6006e1e
BLAKE2b-256 ab556661d4ac34d6617ef97f15c482f0bfd462398f145deceb2419d6e65de39a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page