diff --git a/rowers/dataprep.py b/rowers/dataprep.py index c1cf63b0..d3e8c4ac 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -6,7 +6,9 @@ from __future__ import unicode_literals # All the data preparation, data cleaning and data mangling should # be defined here from __future__ import unicode_literals, absolute_import -from rowers.models import Workout, Team +from rowers.models import ( + Workout, Team, CalcAgePerformance,C2WorldClassAgePerformance, + ) import pytz import collections @@ -23,7 +25,10 @@ from rowingdata import ( get_file_type, get_empower_rigging,get_empower_firmware ) -from rowers.tasks import handle_sendemail_unrecognized,handle_setcp +from rowers.tasks import ( + handle_sendemail_unrecognized,handle_setcp, + handle_getagegrouprecords + ) from rowers.tasks import handle_zip_file from pandas import DataFrame, Series @@ -643,7 +648,7 @@ def clean_df_stats(datadf, workstrokesonly=True, ignorehr=True, pass try: - mask = datadf['spm'] > 60 + mask = datadf['spm'] > 120 datadf.mask(mask,inplace=True) except (KeyError,TypeError): pass @@ -1016,6 +1021,74 @@ def fetchcperg(rower,theworkouts): return cpdf +from rowers.datautils import p0 +from rowers.utils import calculate_age +from scipy import optimize + +def fitscore(rower,workout): + cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=workout.id) + try: + df = pd.read_parquet(cpfile) + except: + 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 = [] + for record in agerecords: + wcdurations.append(record.duration) + wcpower.append(record.power) + + if len(agerecords)==0: + durations = [1,4,10,20,30,60] + distances = [] + df2 = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + sex=rower.sex, + weightcategory=rower.weightcategory + ).values() + ) + ) + jsondf = df2.to_json() + job = myqueue(queue,handle_getagegrouprecords, + jsondf,distances,durations,age,rower.sex,rower.weightcategory) + + wcpower = pd.Series(wcpower) + wcdurations = pd.Series(wcdurations) + + 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: + 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 + + + times = df['delta'] + powers = df['cp'] + wcpowers = fitfunc(p1wc,times) + scores = 100.*powers/wcpowers + + try: + indexmax = scores.idxmax() + delta = df.loc[indexmax,'delta'] + maxvalue = scores.max() + except ValueError: + indexmax = 0 + delta = 0 + maxvalue = 0 + + return maxvalue,delta + def fetchcp_new(rower,workouts): data = [] @@ -3009,6 +3082,7 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, return data + def workout_trimp(w): r = w.user diff --git a/rowers/forms.py b/rowers/forms.py index cd2615ab..bc05c8e7 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -732,6 +732,9 @@ class FitnessFitForm(forms.Form): fitnesstest = forms.IntegerField(required=True,initial=20, label='Test Duration (minutes)') + usefitscore = forms.BooleanField(required=False,initial=False, + label='Use best performance against world class') + kfitness = forms.IntegerField(initial=42,required=True, label='Fitness Time Constant (days)') diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index c95c88c6..3709ddd7 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import colorsys from rowers.models import ( Workout, User, Rower, WorkoutForm,RowerForm, - GraphImage,GeoPolygon,GeoCourse,GeoPoint + GraphImage,GeoPolygon,GeoCourse,GeoPoint, ) from rowers.tasks import handle_setcp from rowingdata import rower as rrower @@ -102,6 +102,103 @@ import rowers.datautils as datautils from pandas.core.groupby.groupby import DataError +def get_fitscore(workouts,kfitness): + dates = [] + testpower = [] + fatigues = [] + fitnesses = [] + data = [] + fitscores = [] + ids = [] + for w in workouts: + fitscore,fitnesstestsecs = dataprep.fitscore(w.user,w) + ids.append(w.id) + fitscores.append(fitscore) + + df = pd.DataFrame({'workout':ids,'fitscore':fitscores}) + + 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() + + dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) + testpower.append(powertest) + fatigues.append(np.nan) + fitnesses.append(np.nan) + + return dates, testpower, fatigues, fitnesses + + +def get_testpower(workouts,fitnesstestsecs,kfitness): + dates = [] + testpower = [] + fatigues = [] + fitnesses = [] + data = [] + for w in workouts: + cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=w.id) + try: + df = pd.read_parquet(cpfile) + df['workout'] = w.id + df['workoutdate'] = w.date.strftime('%d-%m-%Y') + data.append(df) + except: + strokesdf = dataprep.getsmallrowdata_db(['power','workoutid','time'],ids=[w.id]) + res = myqueue(queuelow, + handle_setcp, + strokesdf, + cpfile,w.id) + + if len(data)>1: + df = pd.concat(data,axis=0) + + fitfunc = lambda pars,x: abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) + errfunc = lambda pars,x,y: fitfunc(pars,x)-y + + for w in workouts: + # Create CP data point for date range + ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=kfitness), + date__lte=w.date)] + + try: + powerdf = df[df['workout'].isin(ids)] + + powerdf = powerdf[powerdf['cp'] == powerdf.groupby(['delta'])['cp'].transform('max')] + powerdf = powerdf.sort_values(['delta']).reset_index() + + + powerdf = powerdf[powerdf['cp']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['delta','cp'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='delta',keep='first',inplace=True) + except KeyError: + powerdf = pd.DataFrame() + + # p1,fitt,fitpower,ratio = datautils.cpfit(powerdf) + if len(powerdf['delta'])>= 4: + thesecs = powerdf['delta'].values + theavpower = powerdf['cp'].values + + if thesecs.min() < fitnesstestsecs and thesecs.max() > fitnesstestsecs: + ww = griddata(thesecs,theavpower,np.array([fitnesstestsecs]),method='linear',rescale=True) + powertest = ww[0] + else: + powertest = np.nan + + + + dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) + testpower.append(powertest) + fatigues.append(np.nan) + fitnesses.append(np.nan) + + return dates,testpower,fatigues,fitnesses + + + def errorbar(fig, x, y, source=ColumnDataSource(), xerr=False, yerr=False, color='black', point_kwargs={}, error_kwargs={}): @@ -1538,81 +1635,27 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, enddate=None,kfitness=42,kfatigue=7,fitnesstest=20, metricchoice='rscore', k1=1,k2=1,p0=100, - modelchoice='tsb'): + modelchoice='tsb', + usefitscore=False): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' - dates = [] - testpower = [] - fatigues = [] - fitnesses = [] + workouts = workouts.order_by('date') - data = [] fitnesstestsecs = fitnesstest*60 df = pd.DataFrame() + if not usefitscore: + dates,testpower,fatigues,fitnesses = get_testpower( + workouts,fitnesstestsecs,kfitness + ) + else: + dates,testpower,fatigues,fitnesses = get_fitscore( + workouts,kfitness + ) # create CP data - for w in workouts: - cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=w.id) - try: - df = pd.read_parquet(cpfile) - df['workout'] = w.id - df['workoutdate'] = w.date.strftime('%d-%m-%Y') - data.append(df) - except: - strokesdf = dataprep.getsmallrowdata_db(['power','workoutid','time'],ids=[w.id]) - res = myqueue(queuelow, - handle_setcp, - strokesdf, - cpfile,w.id) - - if len(data)>1: - df = pd.concat(data,axis=0) - - fitfunc = lambda pars,x: abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) - errfunc = lambda pars,x,y: fitfunc(pars,x)-y - - for w in workouts: - # Create CP data point for date range - ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=kfitness), - date__lte=w.date)] - - try: - powerdf = df[df['workout'].isin(ids)] - - powerdf = powerdf[powerdf['cp'] == powerdf.groupby(['delta'])['cp'].transform('max')] - powerdf = powerdf.sort_values(['delta']).reset_index() - - - powerdf = powerdf[powerdf['cp']>0] - powerdf.dropna(axis=0,inplace=True) - powerdf.sort_values(['delta','cp'],ascending=[1,0],inplace=True) - powerdf.drop_duplicates(subset='delta',keep='first',inplace=True) - except KeyError: - powerdf = pd.DataFrame() - - # p1,fitt,fitpower,ratio = datautils.cpfit(powerdf) - if len(powerdf['delta'])>= 4: - thesecs = powerdf['delta'].values - theavpower = powerdf['cp'].values - - if thesecs.min() < fitnesstestsecs and thesecs.max() > fitnesstestsecs: - ww = griddata(thesecs,theavpower,np.array([fitnesstestsecs]),method='linear',rescale=True) - powertest = ww[0] - else: - powertest = np.nan - - - - dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) - testpower.append(powertest) - fatigues.append(np.nan) - fitnesses.append(np.nan) - - - df = pd.DataFrame({ 'date':dates, @@ -1651,12 +1694,9 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, weight = 0 for w in ws: weight += getattr(w,metricchoice) - if modelchoice == 'tsb': - fatigue = (1-lambda_a)*fatigue+weight*lambda_a - fitness = (1-lambda_c)*fitness+weight*lambda_c - else: - fatigue = fatigue*math.exp(-1./kfatigue) + weight - fitness = fitness*math.exp(-1./kfitness) + weight + + fatigue = (1-lambda_a)*fatigue+weight*lambda_a + fitness = (1-lambda_c)*fitness+weight*lambda_c fatigues.append(fatigue) fitnesses.append(fitness) @@ -1739,22 +1779,31 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, fitlabel = 'PTE (fitness)' fatiguelabel = 'NTE (fatigue)' formlabel = 'Performance' + rightaxlabel = 'Banister PTE/NTE/Performance' else: fitlabel = 'CTL' fatiguelabel = 'ATL' formlabel = 'TSB' + rightaxlabel = 'Coggan CTL/ATL/TSB' + + if usefitscore: + legend_label = 'Test Score' + yaxlabel = 'Test Score' + else: + legend_label = '{fitnesstest} min power' + yaxlabel = 'Test Power (Watt)' plot.circle('date','testpower',source=source,fill_color='green',size=10, - legend_label='{fitnesstest} min power'.format(fitnesstest=fitnesstest)) + legend_label=legend_label.format(fitnesstest=fitnesstest)) plot.xaxis.axis_label = 'Date' - plot.yaxis.axis_label = 'Power (W)' + plot.yaxis.axis_label = yaxlabel y2rangemin = df.loc[:,['fitness','fatigue','form']].min().min() y2rangemax = df.loc[:,['fitness','fatigue','form']].max().max() plot.extra_y_ranges["yax2"] = Range1d(start=y2rangemin,end=y2rangemax) - plot.add_layout(LinearAxis(y_range_name="yax2",axis_label="Score"),"right") + plot.add_layout(LinearAxis(y_range_name="yax2",axis_label=rightaxlabel),"right") plot.line('date','fitness',source=source,color='blue', legend_label=fitlabel,y_range_name="yax2") @@ -4757,7 +4806,6 @@ def interactive_flexchart_stacked(id,r,xparam='time', workstrokesonly=False) - if r.usersmooth > 1: for column in columns: try: diff --git a/rowers/models.py b/rowers/models.py index ed96081a..f38ccc04 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -265,15 +265,15 @@ def update_records(url=c2url,verbose=True): for nr,row in df.iterrows(): if 'm' in row['Record']: - df.ix[nr,'Distance'] = row['Record'][:-1] - df.ix[nr,'Duration'] = 60*row['Event'] + df.loc[nr,'Distance'] = row['Record'][:-1] + df.loc[nr,'Duration'] = 60*row['Event'] else: - df.ix[nr,'Distance'] = row['Event'] + df.loc[nr,'Distance'] = row['Event'] try: tobj = datetime.datetime.strptime(row['Record'],'%M:%S.%f') except ValueError: tobj = datetime.datetime.strptime(row['Record'],'%H:%M:%S.%f') - df.ix[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6 + df.loc[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6 for nr,row in df.iterrows(): try: @@ -334,6 +334,15 @@ class CalcAgePerformance(models.Model): class Meta: db_table = 'calcagegrouprecords' + def __str_(self): + stri = 'Calculated World Class Performance for {s}, {a}, {d} secs, {p} Watts'.format( + s = self.sex, + a = self.age, + d = self.duration, + p = self.power + ) + return stri + class PowerTimeFitnessMetric(models.Model): modechoices = ( ('rower','Rower'), diff --git a/rowers/utils.py b/rowers/utils.py index ce2a017f..cf76d3ca 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -323,8 +323,9 @@ def myqueue(queue,function,*args,**kwargs): from datetime import date -def calculate_age(born): - today = date.today() +def calculate_age(born,today=None): + if not today: + today = date.today() if born: return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) else: diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index efba7b2a..e3402241 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -1557,6 +1557,7 @@ def fitness_from_cp_view(request,userid=0,mode='rower', fitnesstest = 20 metricchoice = 'rscore' modelchoice = 'tsb' + usefitscore = False # temp fit parameters k1 = 1 @@ -1578,6 +1579,7 @@ def fitness_from_cp_view(request,userid=0,mode='rower', k2 = form.cleaned_data['k2'] p0 = form.cleaned_data['p0'] modelchoice = form.cleaned_data['modelchoice'] + usefitscore = form.cleaned_data['usefitscore'] else: form = FitnessFitForm() @@ -1602,6 +1604,7 @@ def fitness_from_cp_view(request,userid=0,mode='rower', metricchoice=metricchoice, k1=k1,k2=k2,p0=p0, modelchoice=modelchoice, + usefitscore=usefitscore, ) breadcrumbs = [