From 0cda0255ceb42776a4abcc4d66c515b63c816797 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 19 Jan 2021 07:31:44 +0100 Subject: [PATCH] adding cpoverlay (world class) --- rowers/forms.py | 4 + rowers/interactiveplots.py | 58 ++++++++- rowers/templates/user_analysis_select.html | 130 +++++++++++++++++++++ rowers/views/analysisviews.py | 62 ++++++++-- 4 files changed, 245 insertions(+), 9 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 7429ca3c..f22cda74 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1218,6 +1218,10 @@ class AnalysisChoiceForm(forms.Form): cpfit = forms.ChoiceField(choices=cpfitchoices, label = 'Model Fit',initial='data',required=False) + cpoverlay = forms.BooleanField(initial=False, + label='Overlay World Record Performance', + required=False) + piece = forms.IntegerField(initial=4,label='Ranking Piece (minutes)', required=False) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 8e5287cf..5004b7ad 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -3304,7 +3304,8 @@ def interactive_agegroupcpchart(age,normalized=False): def interactive_otwcpchart(powerdf,promember=0,rowername="",r=None,cpfit='data', - title='',type='water'): + title='',type='water', + wcpower=[],wcdurations=[],cpoverlay=False): powerdf = powerdf[~(powerdf == 0).any(axis=1)] # plot tools @@ -3356,6 +3357,34 @@ def interactive_otwcpchart(powerdf,promember=0,rowername="",r=None,cpfit='data', workouts = powerdf['workout'] urls = powerdf['url'] + # add world class + wcpower = pd.Series(wcpower) + wcdurations = pd.Series(wcdurations) + + + # fitting WC data to three parameter CP model + if len(wcdurations)>=4: + 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 + p1wc, success = optimize.leastsq(errfunc, p0[:], + args = (wcdurations,wcpower)) + else: + p1wc = None + + if p1wc is not None and cpoverlay: + fitpowerwc = fitfunc(p1wc,fitt) + fitpowerexcellent = 0.7*fitfunc(p1wc,fitt) + fitpowergood = 0.6*fitfunc(p1wc,fitt) + fitpowerfair = 0.5*fitfunc(p1wc,fitt) + fitpoweraverage = 0.4*fitfunc(p1wc,fitt) + + else: + fitpowerwc = 0*fitpower + fitpowerexcellent = 0*fitpower + fitpowergood = 0*fitpower + fitpowerfair = 0*fitpower + fitpoweraverage = 0*fitpower + sourcecomplex = ColumnDataSource( data = dict( @@ -3364,6 +3393,11 @@ def interactive_otwcpchart(powerdf,promember=0,rowername="",r=None,cpfit='data', duration = fitt/60., ftime = ftime, workout = workouts, + fitpowerwc = fitpowerwc, + fitpowerexcellent = fitpowerexcellent, + fitpowergood = fitpowergood, + fitpowerfair = fitpowerfair, + fitpoweraverage = fitpoweraverage, url = urls, ) ) @@ -3424,6 +3458,7 @@ def interactive_otwcpchart(powerdf,promember=0,rowername="",r=None,cpfit='data', ('Power (W)','@CP{int}'), ('Power (W) upper','@CPmax{int}'), ('Workout','@workout'), + ('World Class','@fitpowerwc{int}') ]) hover.mode = 'mouse' @@ -3437,6 +3472,27 @@ def interactive_otwcpchart(powerdf,promember=0,rowername="",r=None,cpfit='data', plot.line('duration','CPmax',source=sourcecomplex,legend_label="CP Model", color='red') + if p1wc is not None: + plot.line('duration','fitpowerwc',source=sourcecomplex, + legend_label="World Class", + color='darkgoldenrod',line_dash='dotted') + + plot.line('duration','fitpowerexcellent',source=sourcecomplex, + legend_label="90% percentile", + color='goldenrod',line_dash='dotted') + + plot.line('duration','fitpowergood',source=sourcecomplex, + legend_label="75% percentile", + color='sandybrown',line_dash='dotted') + + plot.line('duration','fitpowerfair',source=sourcecomplex, + legend_label="50% percentile", + color='rosybrown',line_dash='dotted') + + plot.line('duration','fitpoweraverage',source=sourcecomplex, + legend_label="25% percentile", + color='tan',line_dash='dotted') + script, div = components(plot) return [script,div,p1,ratio,message] diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html index 5d05f318..f098f78d 100644 --- a/rowers/templates/user_analysis_select.html +++ b/rowers/templates/user_analysis_select.html @@ -91,6 +91,7 @@ var reststrokes = $("#id_includereststrokes").parent().parent(); var piece = $("#id_piece").parent().parent(); var cpfit = $("#id_cpfit").parent().parent(); + var cpoverlay = $("#id_cpoverlay").parent().parent(); // Hide the fields. @@ -113,6 +114,7 @@ spmmin.hide(); spmmax.hide(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); if (functionfield.val() == 'boxplot') { @@ -153,6 +155,7 @@ if (functionfield.val() == 'cp') { cpfit.show(); piece.show(); + cpoverlay.show(); } @@ -182,6 +185,7 @@ plottype.hide(); reststrokes.show(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); } else if (Value=='histo') { @@ -202,6 +206,7 @@ plottype.hide(); reststrokes.show(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); } @@ -223,6 +228,7 @@ plottype.hide(); reststrokes.show(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); } @@ -244,6 +250,7 @@ errorbars.hide(); reststrokes.show(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); } else if (Value=='stats') { @@ -260,6 +267,7 @@ plottype.hide(); reststrokes.show(); cpfit.hide(); + cpoverlay.hide(); piece.hide(); } else if (Value=='compare') { @@ -280,6 +288,7 @@ errorbars.hide(); piece.hide(); cpfit.hide(); + cpoverlay.hide(); reststrokes.show(); @@ -302,6 +311,7 @@ plottype.hide(); reststrokes.hide(); cpfit.show(); + cpoverlay.hide(); piece.show(); } }); @@ -372,6 +382,126 @@ + {% if worldclass %} + {% if age and sex != 'not specified' %} +
  • +

    World Records

    +

    The dashed lines are based on the + Concept2 + rankings for your age ({{ age }}), gender ({{ sex }}) + and weight category ({{ weightcategory }}). World class means within 5% of + + World Record in terms + of power. + The percentile lines are estimates of where the percentiles + of the Concept2 rankings historically are for those of exactly + your age, gender and weight class. +

    +

    + For rowing on the water, the results are corrected for the expected + difference in power between the Concept2 indoor rower and power + values on the water. +

    +
  • +
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + 100m + +
    + 500m + +
    + 1000m + +
    + 2000m + +
    + 5000m + +
    + 6000m + +
    + 10000m + +
    + Half Marathon + +
    + Full Marathon + +
    + 1 minute + +
    + 4 minutes + +
    + 30 minutes + +
    + 1 hour + +
    +
  • + {% endif %} + {% endif %} diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 76c01275..483ea122 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -39,6 +39,7 @@ def analysis_new(request,userid=0,function='boxplot',teamid=0,id=''): r = getrequestrower(request, userid=userid) user = r.user userid = user.id + worldclass = False firstworkout = None if id: @@ -71,6 +72,10 @@ def analysis_new(request,userid=0,function='boxplot',teamid=0,id=''): modalities = [m[0] for m in mytypes.workouttypes_ordered.items()] modality = 'all' + try: + worldclass = options['cpoverlay'] + except KeyError: + worldclass = False try: @@ -132,6 +137,12 @@ def analysis_new(request,userid=0,function='boxplot',teamid=0,id=''): options['modalities'] = modalities options['waterboattype'] = waterboattype + try: + worldclass = options['cpoverlay'] + except KeyError: + worldclass = False + options['cpoverlay'] = worldclass + chartform = AnalysisChoiceForm(request.POST) if chartform.is_valid(): @@ -240,6 +251,10 @@ def analysis_new(request,userid=0,function='boxplot',teamid=0,id=''): }) + if r.birthdate: + age = calculate_age(r.birthdate) + else: + age = 0 startdatestring = startdate.strftime('%Y-%m-%d') enddatestring = enddate.strftime('%Y-%m-%d') @@ -272,6 +287,10 @@ def analysis_new(request,userid=0,function='boxplot',teamid=0,id=''): 'chartform':chartform, 'searchform':searchform, 'optionsform':optionsform, + 'worldclass':worldclass, + 'age':age, + 'sex':r.sex, + 'weightcategory':r.weightcategory, 'teams':get_my_teams(request.user), }) @@ -519,6 +538,7 @@ def histodata(workouts, options): def cpdata(workouts, options): userid = options['userid'] cpfit = options['cpfit'] + cpoverlay = options['cpoverlay'] u = User.objects.get(id=userid) r = u.rower @@ -546,6 +566,9 @@ def cpdata(workouts, options): rowername = r.user.first_name+" "+r.user.last_name + wcdurations = [] + wcpower = [] + if len(powerdf) !=0 : datefirst = pd.Series(w.date for w in workouts).min() datelast = pd.Series(w.date for w in workouts).max() @@ -561,19 +584,42 @@ def cpdata(workouts, options): # for Mike wtype = 'erg' + if cpoverlay: + if r.birthdate: + age = calculate_age(r.birthdate) + else: + worldclasspower = None + age = 0 + + agerecords = CalcAgePerformance.objects.filter( + age = age, + sex = r.sex, + weightcategory = r.weightcategory + ) + + if len(agerecords) == 0: + wcpower = [] + wcdurations = [] + else: + wcdurations = [] + wcpower = [] + for record in agerecords: + recordpower = record.power + if wtype == 'water': + recordpower = record.power*(100.-r.otwslack)/100. + + wcdurations.append(record.duration) + wcpower.append(recordpower) + res = interactive_otwcpchart(powerdf,promember=True,rowername=rowername,r=r, - cpfit=cpfit,title=title,type=wtype) + cpfit=cpfit,title=title,type=wtype, + cpoverlay=cpoverlay, + wcdurations=wcdurations,wcpower=wcpower) script = res[0] div = res[1] p1 = res[2] ratio = res[3] - #r.p0 = p1[0] - #r.p1 = p1[1] - #r.p2 = p1[2] - #r.p3 = p1[3] - #r.cpratio = ratio - #r.save() paulslope = 1 paulintercept = 1 message = res[4] @@ -2194,7 +2240,7 @@ def rankings_view2(request,userid=0, if len(agerecords) == 0: recalc = True wcpower = [] - wcduration = [] + wcdurations = [] else: wcdurations = [] wcpower = []