diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 1f6a27ba..844693e7 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import from rowers.models import ( Workout, Team, CalcAgePerformance,C2WorldClassAgePerformance, + User ) import pytz @@ -325,10 +326,13 @@ def workout_summary_to_df( startdate=datetime.datetime(1970,1,1), enddate=timezone.now()+timezone.timedelta(days=1)): - ws = Workout.objects.filter(user=rower).order_by("startdatetime") + ws = Workout.objects.filter( + user=rower,date__gte=startdate,date__lte=enddate + ).order_by("startdatetime") types = [] names = [] + ids = [] startdatetimes = [] timezones = [] distances = [] @@ -339,12 +343,19 @@ def workout_summary_to_df( notes = [] tcx_links = [] csv_links = [] + workout_links = [] + goldstandards = [] + goldstandarddurations = [] rscores = [] + hrtss = [] trimps = [] + rankingpieces = [] + boattypes = [] for w in ws: types.append(w.workouttype) names.append(w.name) + ids.append(encoder.encode_hex(w.id)) startdatetimes.append(w.startdatetime) timezones.append(w.timezone) distances.append(w.distance) @@ -352,6 +363,7 @@ def workout_summary_to_df( weightcategories.append(w.weightcategory) adaptivetypes.append(w.adaptiveclass) weightvalues.append(w.weightvalue) + boattypes.append(w.boattype) notes.append(w.notes) tcx_link = SITE_URL+'/rowers/workout/{id}/emailtcx'.format( id=encoder.encode_hex(w.id) @@ -361,25 +373,41 @@ def workout_summary_to_df( id=encoder.encode_hex(w.id) ) csv_links.append(csv_link) + workout_link = SITE_URL+'/rowers/workout/{id}/'.format( + id=encoder.encode_hex(w.id) + ) + workout_links.append(workout_link) trimps.append(workout_trimp(w)[0]) rscore = workout_rscore(w) rscores.append(int(rscore[0])) + hrtss.append(int(w.hrtss)) + goldstandard,goldstandardduration = workout_goldmedalstandard(w) + goldstandards.append(int(goldstandard)) + goldstandarddurations.append(int(goldstandardduration)) + rankingpieces.append(w.rankingpiece) df = pd.DataFrame({ - 'name':names, + 'ID': ids, 'date':startdatetimes, + 'name':names, + 'link':workout_links, 'timezone':timezones, 'type':types, + 'boat type':boattypes, 'distance (m)':distances, 'duration ':durations, + 'ranking piece':rankingpieces, 'weight category':weightcategories, 'adaptive classification':adaptivetypes, 'weight (kg)':weightvalues, - 'notes':notes, 'Stroke Data TCX':tcx_links, 'Stroke Data CSV':csv_links, 'TRIMP Training Load':trimps, 'TSS Training Load':rscores, + 'hrTSS Training Load':hrtss, + 'GS':goldstandards, + 'GS_secs':goldstandarddurations, + 'notes':notes, }) return df @@ -1026,28 +1054,58 @@ from rowers.datautils import p0 from rowers.utils import calculate_age from scipy import optimize -def fitscore(rower,workout): +def get_workoutsummaries(userid,startdate): + u = User.objects.get(id=userid) + r = u.rower + df = workout_summary_to_df(r,startdate=startdate) + df.drop(['Stroke Data TCX','Stroke Data CSV'],axis=1,inplace=True) + df = df.sort_values('date',ascending=False) + + return df + +def workout_goldmedalstandard(workout): + if workout.goldmedalstandard > 0: + return workout.goldmedalstandard,workout.goldmedalseconds + if workout.workouttype in rowtypes: + goldmedalstandard,goldmedalseconds = calculate_goldmedalstandard(workout.user,workout) + workout.goldmedalstandard = goldmedalstandard + workout.goldmedalseconds = goldmedalseconds + workout.save() + return goldmedalstandard, goldmedalseconds + else: + return 0,0 + +def calculate_goldmedalstandard(rower,workout): cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=workout.id) try: df = pd.read_parquet(cpfile) except: df, delta, cpvalues = setcp(workout) + if df.empty: + df, delta, cpvalues = setcp(workout) + age = calculate_age(rower.birthdate,today=workout.date) + agerecords = CalcAgePerformance.objects.filter( age=age, sex=rower.sex, weightcategory = rower.weightcategory ) + wcdurations = [] wcpower = [] + getrecords = len(agerecords) == 0 for record in agerecords: - wcdurations.append(record.duration) - wcpower.append(record.power) + if record.power > 0: + wcdurations.append(record.duration) + wcpower.append(record.power) + else: + getrecords = True - if len(agerecords)==0: - durations = [1,4,10,20,30,60] - distances = [] + if getrecords: + durations = [1,4,30,60] + distances = [100,500,1000,2000,5000,6000,10000,21097,42195] df2 = pd.DataFrame( list( C2WorldClassAgePerformance.objects.filter( @@ -1066,12 +1124,13 @@ def fitscore(rower,workout): fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/pars[3])) errfunc = lambda pars,x,y: fitfunc(pars,x)-y - if len(wcdurations)>4: + if len(wcdurations)>=4: p1wc, success = optimize.leastsq(errfunc, p0[:],args=(wcdurations,wcpower)) else: factor = fitfunc(p0,wcdurations.mean()/wcpower.mean()) p1wc = [p0[0]/factor,p0[1]/factor,p0[2],p0[3]] success = 0 + return 0,0 times = df['delta'] @@ -1079,11 +1138,12 @@ def fitscore(rower,workout): wcpowers = fitfunc(p1wc,times) scores = 100.*powers/wcpowers + try: indexmax = scores.idxmax() - delta = df.loc[indexmax,'delta'] + delta = int(df.loc[indexmax,'delta']) maxvalue = scores.max() - except ValueError: + except (ValueError,TypeError): indexmax = 0 delta = 0 maxvalue = 0 @@ -1127,7 +1187,6 @@ def setcp(workout,background=False): return job.id - if not strokesdf.empty: totaltime = strokesdf['time'].max() try: @@ -1150,6 +1209,10 @@ def setcp(workout,background=False): 'id':workout.id, }) df.to_parquet(filename,engine='fastparquet',compression='GZIP') + goldmedalstandard, goldmedalduration = calculate_goldmedalstandard(workout.user,workout) + workout.goldmedalstandard = goldmedalstandard + workout.goldmedalduration = goldmedalduration + workout.save() return df,delta,cpvalues return pd.DataFrame({'delta':[],'cp':[]}),pd.Series(),pd.Series() @@ -2580,7 +2643,10 @@ def read_df_sql(id): rowdata,row = getrowdata(id=id) if rowdata and len(rowdata.df): data = dataprep(rowdata.df,id=id,bands=True,otwpower=True,barchart=True) - df = pd.read_parquet(f) + try: + df = pd.read_parquet(f) + except OSError: + df = data else: df = pd.DataFrame() diff --git a/rowers/forms.py b/rowers/forms.py index 6d074114..5266bdc4 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -768,7 +768,7 @@ class FitnessFitForm(forms.Form): fitnesstest = forms.IntegerField(required=True,initial=20, label='Test Duration (minutes)') - usefitscore = forms.BooleanField(required=False,initial=False, + usegoldmedalstandard = forms.BooleanField(required=False,initial=False, label='Use best performance against world class') kfitness = forms.IntegerField(initial=42,required=True, diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 14711cc9..5012ba5b 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -25,7 +25,7 @@ import itertools from bokeh.plotting import figure, ColumnDataSource, Figure,curdoc from bokeh.models import CustomJS,Slider, TextInput,BoxAnnotation, Band -from rowers.utils import myqueue +from rowers.utils import myqueue, totaltime_sec_to_string import django_rq queue = django_rq.get_queue('default') queuelow = django_rq.get_queue('low') @@ -102,39 +102,52 @@ import rowers.datautils as datautils from pandas.core.groupby.groupby import DataError -def get_fitscore(workouts,kfitness): +def build_goldmedalstandards(workouts,kfitness): dates = [] testpower = [] + testduration = [] fatigues = [] fitnesses = [] + data = [] - fitscores = [] + goldmedalstandards = [] + goldmedaldurations = [] ids = [] for w in workouts: - fitscore,fitnesstestsecs = dataprep.fitscore(w.user,w) + goldmedalstandard,goldmedalseconds = dataprep.workout_goldmedalstandard(w) ids.append(w.id) - fitscores.append(fitscore) + goldmedalstandards.append(goldmedalstandard) + goldmedaldurations.append(goldmedalseconds) - df = pd.DataFrame({'workout':ids,'fitscore':fitscores}) + df = pd.DataFrame({ + 'workout':ids, + 'goldmedalstandard':goldmedalstandards, + 'goldmedalduration':goldmedaldurations, + }) for w in workouts: ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=kfitness), date__lte=w.date)] powerdf = df[df['workout'].isin(ids)] - powertest = powerdf['fitscore'].max() + indexmax = powerdf['goldmedalstandard'].idxmax() + powertest = powerdf['goldmedalstandard'].max() + durationtest = powerdf.loc[indexmax,'goldmedalduration'] dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) testpower.append(powertest) + testduration.append(durationtest) + fatigues.append(np.nan) fitnesses.append(np.nan) - return dates, testpower, fatigues, fitnesses + return dates, testpower, testduration, fatigues, fitnesses def get_testpower(workouts,fitnesstestsecs,kfitness): dates = [] testpower = [] + testduration = [] fatigues = [] fitnesses = [] data = [] @@ -192,10 +205,11 @@ def get_testpower(workouts,fitnesstestsecs,kfitness): dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) testpower.append(powertest) + testduration.append(fitnesstestsecs) fatigues.append(np.nan) fitnesses.append(np.nan) - return dates,testpower,fatigues,fitnesses + return dates,testpower, testduration,fatigues,fitnesses @@ -1632,7 +1646,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): return [script,div,js_resources,css_resources] def getfatigues( - fatigues,fitnesses,dates,testpower, + fatigues,fitnesses,dates,testpower,testduration, startdate,enddate,user,metricchoice,kfatigue,kfitness): fatigue = 0 @@ -1685,8 +1699,9 @@ def getfatigues( fitnesses.append(fitness) dates.append(datetime.datetime.combine(date,datetime.datetime.min.time())) testpower.append(np.nan) + testduration.append(np.nan) - return fatigues,fitnesses,dates,testpower,impulses + return fatigues,fitnesses,dates,testpower,testduration,impulses def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, metricchoice='trimp',doform=False,dofatigue=False): @@ -1699,6 +1714,7 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, fitnesses = [] dates = [] testpower = [] + testduration = [] modelchoice = 'coggan' p0 = 0 @@ -1707,11 +1723,10 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, - - fatigues,fitnesses,dates,testpower,impulses = getfatigues(fatigues, + fatigues,fitnesses,dates,testpower,testduration,impulses = getfatigues(fatigues, fitnesses, dates, - testpower, + testpower,testduration, startdate,enddate, user,metricchoice, kfatigue,kfitness) @@ -1918,7 +1933,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, metricchoice='rscore', k1=1,k2=1,p0=100, modelchoice='tsb', - usefitscore=False): + usegoldmedalstandard=False): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' @@ -1929,12 +1944,12 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, fitnesstestsecs = fitnesstest*60 df = pd.DataFrame() - if not usefitscore: - dates,testpower,fatigues,fitnesses = get_testpower( + if not usegoldmedalstandard: + dates,testpower,testduration, fatigues,fitnesses = get_testpower( workouts,fitnesstestsecs,kfitness ) else: - dates,testpower,fatigues,fitnesses = get_fitscore( + dates,testpower, testduration,fatigues,fitnesses = build_goldmedalstandards( workouts,kfitness ) # create CP data @@ -1942,6 +1957,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, df = pd.DataFrame({ 'date':dates, 'testpower':testpower, + 'testduration':testduration, 'fatigue':fatigues, 'fitness':fitnesses, }) @@ -1962,9 +1978,10 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, testpower = df['testpower'].values.tolist() fatigues = df['fatigue'].values.tolist() fitnesses = df['fitness'].values.tolist() + testduration = df['testduration'].values.tolist() - fatigues,fitnesses,dates,testpower,impulses = getfatigues( - fatigues,fitnesses,dates,testpower, + fatigues,fitnesses,dates,testpower,testduration,impulses = getfatigues( + fatigues,fitnesses,dates,testpower,testduration, startdate,enddate,user,metricchoice,kfatigue,kfitness ) @@ -1972,6 +1989,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, df = pd.DataFrame({ 'date':dates, 'testpower':testpower, + 'testduration':testduration, 'fatigue':fatigues, 'fitness':fitnesses, }) @@ -1997,6 +2015,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, source = ColumnDataSource( data = dict( testpower = df['testpower'], + testduration = df['testduration'].apply(lambda x:totaltime_sec_to_string(x,shorten=True)), date = df['date'], fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')), fitness = df['fitness'], @@ -2051,7 +2070,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, formlabel = 'TSB' rightaxlabel = 'Coggan CTL/ATL/TSB' - if usefitscore: + if usegoldmedalstandard: legend_label = 'Test Score' yaxlabel = 'Test Score' else: @@ -2106,7 +2125,8 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, hover = plot.select(dict(type=HoverTool)) hover.tooltips = OrderedDict([ - (legend_label,'@testpower'), + (legend_label,'@testpower{int}'), + ('Test', '@testduration'), ('Date','@fdate'), (fitlabel,'@fitness'), (fatiguelabel,'@fatigue'), diff --git a/rowers/models.py b/rowers/models.py index 8a6663a3..aa31934e 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -2953,6 +2953,7 @@ class Workout(models.Model): normv = models.FloatField(default=-1,blank=True) normw = models.FloatField(default=-1,blank=True) goldmedalstandard = models.FloatField(default=-1,blank=True,verbose_name='Gold Medal Standard') + goldmedalseconds = models.IntegerField(default=0,blank=True,verbose_name='Gold Medal Seconds') rpe = models.IntegerField(default=0,blank=True,choices=rpechoices, verbose_name='Rate of Perceived Exertion') diff --git a/rowers/tasks.py b/rowers/tasks.py index 103f09fe..57cbdbae 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -16,6 +16,7 @@ import json from scipy import optimize from scipy.signal import savgol_filter +from scipy.interpolate import griddata import rowingdata from rowingdata import make_cumvalues @@ -333,7 +334,11 @@ def getagegrouprecord(age,sex='male',weightcategory='hwt', power = 0.5*(np.abs(power)+power) else: - power = 0 + new_age = np.range([age]) + ww = griddata(ages.values, + powers.values, + new_age,method='linear',rescale=True) + power = 0.5*(np.abs(power)+power) else: power = 0 diff --git a/rowers/templates/workoutstats.html b/rowers/templates/workoutstats.html index a7eaf737..088b6af3 100644 --- a/rowers/templates/workoutstats.html +++ b/rowers/templates/workoutstats.html @@ -49,6 +49,13 @@
Gold Medal Standard: For rowing workouts, the best performance, relative to world class rowers + of your age, gender and weight category, + found in this workout. This metric uses your power data over time and + compares them with the power that the best rowers of your age, gender and weight category + can hold over time.
+Gold Medal Standard Duration: The time interval over which your best + performance in this workout was achieved.
rPower: Equivalent steady state power for the duration of the workout.
Heart Rate Drift: Comparing heart rate normalized for average power for the first and second half of the workout
TRIMP: TRaining IMPact. A way to combine duration and heart rate into a single number.
@@ -69,7 +76,7 @@