commit 99daa55e682e04c6ca36d3fcb530647f56b549e7 Author: Thies Lennart Alff Date: Thu Jul 27 14:58:21 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e4080e0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gc_wrapper"] + path = gc_wrapper + url = https://github.com/rb83421/GoldenCheetah_Python_Chart_Wrapper.git diff --git a/gc_wrapper b/gc_wrapper new file mode 160000 index 0000000..7b81e28 --- /dev/null +++ b/gc_wrapper @@ -0,0 +1 @@ +Subproject commit 7b81e28aeab748a47d9ea0bc8fd5d7b45e88ac74 diff --git a/hrv_trends.py b/hrv_trends.py new file mode 100644 index 0000000..be4000f --- /dev/null +++ b/hrv_trends.py @@ -0,0 +1,221 @@ +import os +import tempfile +import pandas as pd +import plotly +from plotly.subplots import make_subplots +from plotly.graph_objs import Scatter, Layout, Bar, Figure + +try: + from gc_wrapper.GC_Wrapper import GC_wrapper as GC +except ImportError: + pass +BASELINE_DAYS = 7 +NORMALRANGE_DAYS = 60 +TRAINING_STRESS = 'BikeStress' + +HRV = 'RECOVERY_POINTS' +HR = 'HR' +HRV_BASE = 'HrvBaseline' +HR_BASE = 'HrBaseline' +HRV_NORMAL = 'HrvNormal' +HRV_STD = 'HrvStdDev' +HRV_CEILING = 'HrvNormalCeiling' +HRV_FLOOR = 'HrvNormalFloor' +HRV_CV = 'HrvCv' +HRV_VAR_BASE = 'HrvVarBase' + + + +def get_hrv_measures(): + history = pd.DataFrame(GC.seasonMeasures( + all=True, group='Hrv')).query(f'{HRV} != 0.0') + selection = pd.DataFrame(GC.seasonMeasures( + all=False, group='Hrv')).query(f'{HRV} != 0.0') + return history, selection + + +def compute_hrv_trends(df: pd.DataFrame): + df[HRV_BASE] = df[HRV].rolling(window=BASELINE_DAYS, + min_periods=BASELINE_DAYS // 2).mean() + df[HRV_NORMAL] = df[HRV].rolling(window=NORMALRANGE_DAYS, + min_periods=NORMALRANGE_DAYS // 2).mean() + df[HRV_STD] = df[HRV].rolling(window=NORMALRANGE_DAYS, + min_periods=NORMALRANGE_DAYS // 2).std() + df[HRV_CEILING] = df[HRV_NORMAL] + 0.75 * df[HRV_STD] + df[HRV_FLOOR] = df[HRV_NORMAL] - 0.75 * df[HRV_STD] + + df[HR_BASE] = df[HR].rolling(window=BASELINE_DAYS, + min_periods=BASELINE_DAYS // 2).mean() + df[HRV_VAR_BASE] = df[HRV].rolling(window=BASELINE_DAYS, min_periods=BASELINE_DAYS // 2).std() + df[HRV_CV] = df[HRV_VAR_BASE] / df[HRV_BASE] * 100.0 + return df + + +def get_pmc_data(t1): + df = pd.DataFrame(GC.seasonPmc(all=True, metric=TRAINING_STRESS)) + tmp = pd.DataFrame(GC.seasonPmc(all=False, metric=TRAINING_STRESS)) + t0 = tmp.iloc[0]['date'] + return crop(df, t0, t1) + + +def get_stress(): + df = pd.DataFrame(GC.seasonMetrics())[['date', TRAINING_STRESS]] + data = df.groupby('date')[TRAINING_STRESS].sum() + df = pd.DataFrame({'date': data.index, TRAINING_STRESS: data.values}) + return df + + +def get_rpe(): + df = pd.DataFrame(GC.seasonMetrics())[['date', 'RPE']] + # df = df.loc[df['RPE'].dtype.kind in 'iufc'] + # df = df.query('RPE != 0.0') + df['RPE'] = df['RPE'].replace('', '0').astype(int) + data = df.groupby('date', as_index=False).mean() + return data + + +def crop(df: pd.DataFrame, t0, t1): + df = df.loc[df['date'] >= t0] + return df.loc[df['date'] <= t1] + + +try: + os.remove(tempfile.gettempdir() + "/hrv_trends.html") +except: + pass + +history, selection = get_hrv_measures() +compute_hrv_trends(history) +t0 = selection.iloc[0]['date'] +t1 = selection.iloc[-1]['date'] +hrv_data = crop(history, t0, t1) + +rpe = get_rpe() +stress = get_stress() +pmc_data = get_pmc_data(stress.iloc[-1]['date']) + +lts_trace = Scatter(x=pmc_data['date'], + y=pmc_data['lts'], + name='Fitness (LTS)', + fill='tozeroy', + line=dict(color='#1f77b4')) +sts_trace = Scatter(x=pmc_data['date'], + y=pmc_data['sts'], + name='Fatigue (STS)', + fill='tozeroy') + +sb_trace = Scatter(x=pmc_data['date'], y=pmc_data['sb'], name='Form (TSB)') + +hr_trace = Scatter(x=hrv_data['date'], + y=hrv_data[HR_BASE], + mode='lines', + name=f'{BASELINE_DAYS}-day HR Baseline') + +hrv_ceiling_trace = Scatter(x=hrv_data['date'], + y=hrv_data[HRV_CEILING], + fill=None, + mode='lines', + name=f"{NORMALRANGE_DAYS}-day Top Normal", + line=dict(color='#b3f6d1', )) +hrv_floor_trace = Scatter(x=hrv_data['date'], + y=hrv_data[HRV_FLOOR], + fill='tonexty', + mode='lines', + name=f'{NORMALRANGE_DAYS}-day Bottom Normal', + line=dict(color='#b3f6d1', )) + +hrv_base_trace = Scatter(x=hrv_data['date'], + y=hrv_data[HRV_BASE], + mode='lines', + name=f"{BASELINE_DAYS}-day Baseline", + line=dict(color='red', )) + +hrv_trace = Bar(x=hrv_data['date'], + y=hrv_data[HRV], + name="Recovery Points", + marker=dict(color='lightgray', )) + +hrv_cv_trace = Scatter(x=hrv_data['date'], + y=hrv_data[HRV_CV], + name='CV', + mode='lines', + line=dict(color='black')) + +# session_rpe_trace = Bar(x=rpe['date'], +# y=rpe['Session_RPE'], +# name='RPE', +# marker=dict(color='lightgray', )) +rpe_trace = Bar(x=rpe['date'], + y=rpe['RPE'], + name='RPE', + marker=dict(color='red', )) + +layout = Layout(template='none', + paper_bgcolor='rgba(0,0,0,0)', + plot_bgcolor='rgba(0,0,0,0)', + yaxis=dict(title="HRV Recovery Points", range=[5.5, 10]), + yaxis2=dict(title="Coeffient of Variation", range=[0, 20]), + yaxis3=dict(title=f'{TRAINING_STRESS}'), + yaxis4=dict(title='HRV (ln(RMSSD)'), + yaxis5=dict(title='HR (BPM)'), + legend=dict(orientation="h")) + +fig = make_subplots( + 4, + 1, + subplot_titles=[ + 'HRV', + 'Stress', + 'HRV vs. HR', + 'RPE', + ], + specs=[ + [dict(secondary_y=False)], + [dict(secondary_y=False)], + [dict(secondary_y=False)], + [dict(secondary_y=True)], + # [dict(secondary_y=True)], + ], + # horizontal_spacing=0.2, + # vertical_spacing=0.2 +) +fig.add_trace(hrv_trace, 1, 1) +fig.add_trace(hrv_ceiling_trace, 1, 1) +fig.add_trace(hrv_floor_trace, 1, 1) +fig.add_trace(hrv_base_trace, 1, 1) +# fig.add_trace(sb_trace, 1, 1, secondary_y=True) +fig.add_trace(hrv_cv_trace, 2, 1) +fig.add_trace(sts_trace, 3, 1) +fig.add_trace(lts_trace, 3, 1) +fig.add_trace(hr_trace, 4, 1, secondary_y=True) +fig.add_trace(hrv_base_trace, 4, 1) +# fig.add_trace(session_rpe_trace, 2, 2) +# fig.add_trace(rpe_trace, 5, 1) +fig.update_layout(layout) +fig.update_layout( + dict( + yaxis=dict(showline=True, mirror=True, ticks='inside'), + yaxis2=dict(showline=True, mirror=True, ticks='inside'), + yaxis3=dict(showline=True, mirror=True, ticks='inside'), + yaxis4=dict(showline=True, mirror=True, ticks='inside'), + yaxis5=dict(showline=True, mirror=True, ticks='inside'), + yaxis6=dict(showline=True, mirror=True, ticks='inside'), + yaxis7=dict(showline=True, mirror=True, ticks='inside'), + yaxis8=dict(showline=True, mirror=True, ticks='inside'), + xaxis=dict(showline=True, mirror=True, ticks='inside'), + xaxis2=dict(showline=True, mirror=True, ticks='inside'), + xaxis3=dict(showline=True, mirror=True, ticks='inside'), + xaxis4=dict(showline=True, mirror=True, ticks='inside'), + xaxis5=dict(showline=True, mirror=True, ticks='inside'), + xaxis6=dict(showline=True, mirror=True, ticks='inside'), + xaxis7=dict(showline=True, mirror=True, ticks='inside'), + xaxis8=dict(showline=True, mirror=True, ticks='inside'), + )) +fig.update_xaxes(showline=True, linewidth=1, linecolor='black', mirror=True) +fig.update_yaxes(showline=True, linewidth=1, linecolor='black', mirror=True) + +f = plotly.offline.plot(fig, + auto_open=False, + filename=tempfile.gettempdir() + "/temp-plot.html") + +GC.webpage("file://" + f)