# coding: utf-8
""" Metrics to asses the performance of a cycling ride.
Functions named as ``*_score`` return a scalar value to maximize: the higher
the better.
Function named as ``*_error`` or ``*_loss`` return a scalar value to minimize:
the lower the better.
"""
# Authors: Guillaume Lemaitre <g.lemaitre58@gmail.com>
# Cedric Lemaitre
# License: BSD 3 clause
from __future__ import division
import numpy as np
TS_SCALE_GRAPPE = dict([('I1', 2.), ('I2', 2.5), ('I3', 3.),
('I4', 3.5), ('I5', 4.5), ('I6', 7.),
('I7', 11.)])
ESIE_SCALE_GRAPPE = dict([('I1', (.3, .5)), ('I2', (.5, .6)),
('I3', (.6, .75)), ('I4', (.75, .85)),
('I5', (.85, 1.)), ('I6', (1., 1.80)),
('I7', (1.8, 3.))])
def mpa2ftp(mpa):
"""Convert the maximum power aerobic into the functional threshold power.
Parameters
----------
mpa : float
Maximum power aerobic.
Return:
-------
ftp : float
Functional threshold power.
Examples
--------
>>> from skcycling.metrics import mpa2ftp
>>> print(mpa2ftp(400)) # doctest: +ELLIPSIS
304...
"""
return 0.76 * mpa
def ftp2mpa(ftp):
"""Convert the functional threshold power into the maximum threshold power.
Parameters
----------
ftp : float
Functional threshold power.
Return:
-------
mpa : float
Maximum power aerobic.
Examples
--------
>>> from skcycling.metrics import ftp2mpa
>>> print(ftp2mpa(304)) # doctest: +ELLIPSIS
400...
"""
return ftp / 0.76
[docs]def normalized_power_score(activity_power, mpa, window_width=30):
"""Normalized power®.
The normalized power is an average power computing a smoothed power input
and rejecting the low power intensities.
Read more in the :ref:`User Guide <metrics>`.
Parameters
----------
activity_power : Series
A Series containing the power data from an activity.
mpa : float
Maximum power aerobic. Use :func:`metrics.ftp2mpa` if you use the
functional threshold power metric.
window_width : int, optional
The width of the window used to smooth the power data before to compute
the normalized power. The default width is 30 samples.
Returns
-------
score : float
Normalized power score.
References
----------
.. [1] Allen, H., and A. Coggan. "Training and racing with a power
meter." VeloPress, 2012.
Examples
--------
>>> from skcycling.datasets import load_fit
>>> from skcycling.io import bikeread
>>> from skcycling.metrics import normalized_power_score
>>> ride = bikeread(load_fit()[0])
>>> mpa = 400
>>> np = normalized_power_score(ride['power'], mpa)
>>> print('Normalized power {:.2f} W'.format(np))
Normalized power 218.49 W
"""
smooth_activity = (activity_power.rolling(window_width, center=True)
.mean().dropna())
# removing value < I1-ESIE, i.e. 30 % MPA
smooth_activity = smooth_activity[
smooth_activity > ESIE_SCALE_GRAPPE['I1'][0] * mpa]
return (smooth_activity ** 4).mean() ** (1 / 4)
[docs]def intensity_factor_score(activity_power, mpa):
"""Intensity factor®.
The intensity factor® is the ratio of the normalized power® over the
functional threshold power. Note that all our computation consider the
maximum power aerobic for consistency. If you only have the functional
threshold power, use :func:`metrics.ftp2mpa`.
Read more in the :ref:`User Guide <metrics>`.
Parameters
----------
activity_power : Series
A Series containing the power data from an activity.
mpa : float
Maximum power aerobic. Use :func:`metrics.ftp2mpa` if you use the
functional threshold power metric.
Returns
-------
score: float
Intensity factor.
References
----------
.. [1] Allen, H., and A. Coggan. "Training and racing with a power
meter." VeloPress, 2012.
Examples
--------
>>> from skcycling.datasets import load_fit
>>> from skcycling.io import bikeread
>>> from skcycling.metrics import intensity_factor_score
>>> ride = bikeread(load_fit()[0])
>>> mpa = 400
>>> if_score = intensity_factor_score(ride['power'], mpa)
>>> print('Intensity factor {:.2f} W'.format(if_score))
Intensity factor 0.72 W
"""
ftp = mpa2ftp(mpa)
return normalized_power_score(activity_power, mpa) / ftp
[docs]def training_stress_score(activity_power, mpa):
"""Training stress score®.
The training stress score® corresponds to the intensity factor® normalized
by the time of the activity. You can use the function
:func:`metrics.ftp2mpa` if you are using the functional threshold metric.
Read more in the :ref:`User Guide <metrics>`.
Parameters
----------
activity_power : Series
A Series containing the power data from an activity.
mpa : float
Maximum power aerobic. Use :func:`metrics.ftp2mpa` if you use the
functional threshold power metric.
Returns
-------
score: float
Training stress score.
References
----------
.. [1] Allen, H., and A. Coggan. "Training and racing with a power
meter." VeloPress, 2012.
Examples
--------
>>> from skcycling.datasets import load_fit
>>> from skcycling.io import bikeread
>>> from skcycling.metrics import training_stress_score
>>> ride = bikeread(load_fit()[0])
>>> mpa = 400
>>> ts_score = training_stress_score(ride['power'], mpa)
>>> print('Training stress score {:.2f}'.format(ts_score))
Training stress score 32.38
"""
activity_power = activity_power.resample('1S').mean()
if_score = intensity_factor_score(activity_power, mpa)
return (activity_power.size * if_score ** 2) / 3600 * 100
[docs]def training_load_score(activity_power, mpa):
"""Training load score.
Grappe et al. proposes to compute the load of an activity by a weighted sum
of the time spend in the different ESIE zones.
Read more in the :ref:`User Guide <metrics>`.
Parameters
----------
activity_power : Series
A Series containing the power data from an activity.
mpa : float
Maximum power aerobic. Use :func:`metrics.ftp2mpa` if you use the
functional threshold power metric.
Returns
-------
tls_score: float
Training load score.
References
----------
.. [1] Grappe, F. "Cyclisme et optimisation de la performance: science
et méthodologie de l'entraînement." De Boeck Supérieur, 2009.
Examples
--------
>>> from skcycling.datasets import load_fit
>>> from skcycling.io import bikeread
>>> from skcycling.metrics import training_load_score
>>> ride = bikeread(load_fit()[0])
>>> mpa = 400
>>> tl_score = training_load_score(ride['power'], mpa)
>>> print('Training load score {:.2f}'.format(tl_score))
Training load score 74.90
"""
tls_score = 0.
activity_power = activity_power.resample('1S').mean()
for key in TS_SCALE_GRAPPE.keys():
power_samples = activity_power[
np.bitwise_and(activity_power >= ESIE_SCALE_GRAPPE[key][0] * mpa,
activity_power < ESIE_SCALE_GRAPPE[key][1] * mpa)]
tls_score += power_samples.size / 60 * TS_SCALE_GRAPPE[key]
return tls_score