From 32e7696e9296dd76de91bbeaac13da6bf4c8b3ba Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 12 Dec 2017 16:44:33 +0100 Subject: [PATCH 01/18] add C2 World Class Performance model --- rowers/admin.py | 7 ++++- rowers/models.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/rowers/admin.py b/rowers/admin.py index 1cbc67be..b658fcd0 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from .models import ( Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement, Team,TeamInvite,TeamRequest, - WorkoutComment, + WorkoutComment,C2WorldClassAgePerformance, ) # Register your models here so you can use them in the Admin module @@ -26,6 +26,9 @@ class WorkoutAdmin(admin.ModelAdmin): class FavoriteChartAdmin(admin.ModelAdmin): list_display = ('user','xparam','yparam1','yparam2','plottype','workouttype','reststrokes') +class C2WorldClassAgePerformanceAdmin(admin.ModelAdmin): + list_display = ('sex','weightcategory','agemin','agemax','distance','power','name','season') + class SiteAnnouncementAdmin(admin.ModelAdmin): list_display = ('announcement','created','modified','expires','dotweet') @@ -51,3 +54,5 @@ admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin) admin.site.register(TeamInvite,TeamInviteAdmin) admin.site.register(TeamRequest,TeamRequestAdmin) admin.site.register(WorkoutComment,WorkoutCommentAdmin) +admin.site.register(C2WorldClassAgePerformance, + C2WorldClassAgePerformanceAdmin) diff --git a/rowers/models.py b/rowers/models.py index 2fa222f5..2fa57a5c 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -21,6 +21,8 @@ from sqlalchemy import create_engine import sqlalchemy as sa from sqlite3 import OperationalError from django.utils import timezone +import pandas as pd +from dateutil import parser import datetime from django.core.exceptions import ValidationError from rowers.rows import validate_file_extension @@ -149,6 +151,75 @@ class PowerZonesField(models.TextField): value = self._get_val_from_obj(obj) return self.get_deb_prep_value(value) +# Age records +def save_agegroup(df,weightcategory,sex): + for id,row in df.iterrows(): + agemin = int(row['Age2']) + agemax = int(row['Age3']) + duration = row['Time'] + power = int(row['Power']) + season = int(row['Season']) + name = row['Name'] + record = C2WorldClassAgePerformance( + agemin = agemin, + agemax = agemax, + weightcategory = weightcategory, + sex=sex, + distance = 2000, + duration = duration, + power = power, + season = season, + name = name, + ) + record.save() + print record + +def make_records(readfile): + xls = pd.ExcelFile(readfile) + female_df = xls.parse('Female') + female_lw_df = xls.parse('Female LW') + male_df = xls.parse('Male') + male_lw_df = xls.parse('Male LW') + save_agegroup(female_df,'hwt','female') + save_agegroup(male_df,'hwt','male') + save_agegroup(female_lw_df,'lwt','female') + save_agegroup(female_lw_df,'lwt','male') + + +class C2WorldClassAgePerformance(models.Model): + weightcategories = ( + ('hwt','heavy-weight'), + ('lwt','light-weight'), + ) + + sexcategories = ( + ('male','male'), + ('female','female'), + ) + + weightcategory = models.CharField(default="hwt", + max_length=30, + choices=weightcategories) + + sex = models.CharField(default="female", + max_length=30, + choices=sexcategories) + + agemin = models.IntegerField(default=19,verbose_name="Minimum Age") + agemax = models.IntegerField(default=29,verbose_name="Maximum Age") + + distance = models.IntegerField(default=2000) + name = models.CharField(max_length=200,blank=True) + duration = models.TimeField(default=1,blank=True) + season = models.IntegerField(default=2013) + power = models.IntegerField(default=200) + + class Meta: + unique_together = ('agemin','agemax','sex','weightcategory','distance') + + def __unicode__(self): + return self.name+':'+str(self.agemin)+'-'+str(self.agemax)+' ('+str(self.season)+')' + # For future Team functionality class Team(models.Model): choices = ( From 73c03fa54e0b3f5660e0257024574123fa43267e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 12 Dec 2017 17:22:40 +0100 Subject: [PATCH 02/18] view agegrouprecordview Doesn't do much yet. It gets the sex/weight group records as a pandas dataframe --- rowers/urls.py | 2 ++ rowers/views.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rowers/urls.py b/rowers/urls.py index 244a6b0d..51371b36 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -121,6 +121,8 @@ urlpatterns = [ url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), + url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)$', + views.agegrouprecordview), url(r'^list-workouts/ranking$',views.workouts_view,{'rankingonly':True}), url(r'^list-workouts/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/team/(?P\d+)/$',views.workouts_view), diff --git a/rowers/views.py b/rowers/views.py index 1359fd5e..120d108b 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -10917,3 +10917,19 @@ def team_members_stats_view(request,id): }) return response + +from rowers.models import C2WorldClassAgePerformance + +def agegrouprecordview(request,sex='male',weightcategory='hwt'): + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + + + + return HttpResponse(1) From 228ea8f980068a547dce268b1a0ed49dada6e5e4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 12 Dec 2017 18:01:06 +0100 Subject: [PATCH 03/18] charts age decline of 2k power per weight/sex group --- rowers/dataprep.py | 4 +++ rowers/interactiveplots.py | 31 +++++++++++++++++++ rowers/templates/agegroupchart.html | 47 +++++++++++++++++++++++++++++ rowers/views.py | 13 ++++++-- 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 rowers/templates/agegroupchart.html diff --git a/rowers/dataprep.py b/rowers/dataprep.py index c9c76554..94c6f1f5 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -473,6 +473,10 @@ def strfdelta(tdelta): return res +def timedelta_to_seconds(tdelta): + return 60.*tdelta.minute+tdelta.second + + # A nice printable format for pace values diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 25a181c2..3b187788 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1069,6 +1069,37 @@ def interactive_otwcpchart(powerdf,promember=0): return [script,div,p1,ratio,message] +def interactive_agegroup_plot(df): + + age = df['agemin'] + power = df['power'] + + poly_coefficients = np.polyfit(age,power,6) + + age2 = np.linspace(11,95) + poly_vals = np.polyval(poly_coefficients,age2) + + source = ColumnDataSource( + data = dict( + age = age, + power = power, + age2 = age2, + poly_vals = poly_vals + ) + ) + + plot = Figure(plot_width=900) + plot.circle('age','power',source=source,fill_color='red',size=15, + legend='2k Power') + plot.line(age2,poly_vals) + plot.xaxis.axis_label = "Age" + plot.yaxis.axis_label = "Concept2 2k power" + + + script,div = components(plot) + + return script,div + def interactive_cpchart(rower,thedistances,thesecs,theavpower, theworkouts,promember=0): diff --git a/rowers/templates/agegroupchart.html b/rowers/templates/agegroupchart.html new file mode 100644 index 00000000..2bebbe61 --- /dev/null +++ b/rowers/templates/agegroupchart.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall {% endblock %} + +{% block content %} + + + + + {{ interactiveplot |safe }} + + + + + +
+ + +

Interactive Plot

+ + + {{ the_div|safe }} + +
+ +{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index 120d108b..4464e9d1 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -10930,6 +10930,15 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt'): ) ) - + df['seconds'] = df['duration'].apply( + lambda x:dataprep.timedelta_to_seconds(x) + ) + + script,div = interactive_agegroup_plot(df) + + return render(request, 'agegroupchart.html', + { + 'interactiveplot':script, + 'the_div':div, + }) - return HttpResponse(1) From d21f808bd3854732972fbf8244bc57da94d81ab9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 12 Dec 2017 18:09:14 +0100 Subject: [PATCH 04/18] bugfix --- rowers/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rowers/views.py b/rowers/views.py index 1359fd5e..aa825a34 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -7556,7 +7556,10 @@ def workout_flexchart3_view(request,*args,**kwargs): rowdata = rdata(row.csvfilename) - additionalmetrics = rowdata.get_additional_metrics() + try: + additionalmetrics = rowdata.get_additional_metrics() + except AttributeError: + additionalmetrics = [] extrametrics = {m.replace('/','_slsh_'):m for m in additionalmetrics} From 86a72545e0736fc85e9d36ec64f0f3729078766c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 12 Dec 2017 21:59:28 +0100 Subject: [PATCH 05/18] calculates age group 2k --- rowers/metrics.py | 21 +++++++++++++++++++++ rowers/models.py | 4 ++-- rowers/templates/rankings.html | 3 +++ rowers/utils.py | 5 +++++ rowers/views.py | 14 +++++++++++++- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/rowers/metrics.py b/rowers/metrics.py index bf1e790b..ee4233bd 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -1,5 +1,7 @@ from utils import lbstoN import numpy as np +from models import C2WorldClassAgePerformance +import pandas as pd rowingmetrics = ( ('time',{ @@ -316,3 +318,22 @@ def calc_trimp(df,sex,hrmax,hrmin): trimp = trimpdata.sum() return trimp + +def getagegroup2k(age,sex='male',weightcategory='hwt'): + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + + ages = df['agemin'] + powers = df['power'] + + poly_coefficients = np.polyfit(ages,powers,6) + + power = np.polyval(poly_coefficients,age) + + return power diff --git a/rowers/models.py b/rowers/models.py index 2fa57a5c..987f1aeb 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -183,7 +183,7 @@ def make_records(readfile): save_agegroup(female_df,'hwt','female') save_agegroup(male_df,'hwt','male') save_agegroup(female_lw_df,'lwt','female') - save_agegroup(female_lw_df,'lwt','male') + save_agegroup(male_lw_df,'lwt','male') class C2WorldClassAgePerformance(models.Model): @@ -218,7 +218,7 @@ class C2WorldClassAgePerformance(models.Model): unique_together = ('agemin','agemax','sex','weightcategory','distance') def __unicode__(self): - return self.name+':'+str(self.agemin)+'-'+str(self.agemax)+' ('+str(self.season)+')' + return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.agemin)+'-'+str(self.agemax)+' ('+str(self.season)+')' # For future Team functionality class Team(models.Model): diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index 741f5a41..ecaa84e2 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -259,6 +259,9 @@ {% endif %} +{% if worldclasspower %} +World class 2k for your age, weight is {{ worldclasspower }} Watt +{% endif %} {% endblock %} diff --git a/rowers/utils.py b/rowers/utils.py index 0b3f2d93..5b87053a 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -263,3 +263,8 @@ def myqueue(queue,function,*args,**kwargs): return job +from datetime import date + +def calculate_age(born): + today = date.today() + return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) diff --git a/rowers/views.py b/rowers/views.py index 4464e9d1..68bf1644 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -710,7 +710,8 @@ def splitstdata(lijst): from utils import ( geo_distance,serialize_list,deserialize_list,uniqify, - str2bool,range_to_color_hex,absolute,myqueue,get_call + str2bool,range_to_color_hex,absolute,myqueue,get_call, + calculate_age ) import datautils @@ -3220,6 +3221,16 @@ def rankings_view(request,theuser=0, promember=0 if not request.user.is_anonymous(): r = getrower(request.user) + if r.birthdate: + age = calculate_age(r.birthdate) + worldclasspower = int(metrics.getagegroup2k( + age, + sex=r.sex, + weightcategory=r.weightcategory + )) + else: + worldclasspower = None + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -3488,6 +3499,7 @@ def rankings_view(request,theuser=0, 'form':form, 'dateform':dateform, 'deltaform':deltaform, + 'worldclasspower':worldclasspower, 'id': theuser, 'theuser':uu, 'startdate':startdate, From c43d832151fa39ea6c45ef065b860f13c2912bcf Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 08:05:04 +0100 Subject: [PATCH 06/18] changed age record model --- rowers/.#metrics.py | 1 + rowers/admin.py | 2 +- rowers/models.py | 21 +++++++++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 rowers/.#metrics.py diff --git a/rowers/.#metrics.py b/rowers/.#metrics.py new file mode 100644 index 00000000..79063b70 --- /dev/null +++ b/rowers/.#metrics.py @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.126064:1512983261 \ No newline at end of file diff --git a/rowers/admin.py b/rowers/admin.py index b658fcd0..1004a06b 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -27,7 +27,7 @@ class FavoriteChartAdmin(admin.ModelAdmin): list_display = ('user','xparam','yparam1','yparam2','plottype','workouttype','reststrokes') class C2WorldClassAgePerformanceAdmin(admin.ModelAdmin): - list_display = ('sex','weightcategory','agemin','agemax','distance','power','name','season') + list_display = ('sex','weightcategory','age','distance','power','name','season') class SiteAnnouncementAdmin(admin.ModelAdmin): list_display = ('announcement','created','modified','expires','dotweet') diff --git a/rowers/models.py b/rowers/models.py index 987f1aeb..5227e829 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -160,12 +160,12 @@ def save_agegroup(df,weightcategory,sex): power = int(row['Power']) season = int(row['Season']) name = row['Name'] + distance = int(row['Distance']) record = C2WorldClassAgePerformance( - agemin = agemin, - agemax = agemax, + age = age, weightcategory = weightcategory, sex=sex, - distance = 2000, + distance = distance, duration = duration, power = power, season = season, @@ -184,8 +184,14 @@ def make_records(readfile): save_agegroup(male_df,'hwt','male') save_agegroup(female_lw_df,'lwt','female') save_agegroup(male_lw_df,'lwt','male') - +c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All' + +def update_records(url=c2url): + dfs = pd.read_html(url,attrs={'class':'views-table'}) + df = dfs[0] + df.columns = df.columns.str.strip() + class C2WorldClassAgePerformance(models.Model): weightcategories = ( ('hwt','heavy-weight'), @@ -205,8 +211,7 @@ class C2WorldClassAgePerformance(models.Model): max_length=30, choices=sexcategories) - agemin = models.IntegerField(default=19,verbose_name="Minimum Age") - agemax = models.IntegerField(default=29,verbose_name="Maximum Age") + age = models.IntegerField(default=19,verbose_name="Age") distance = models.IntegerField(default=2000) name = models.CharField(max_length=200,blank=True) @@ -215,10 +220,10 @@ class C2WorldClassAgePerformance(models.Model): power = models.IntegerField(default=200) class Meta: - unique_together = ('agemin','agemax','sex','weightcategory','distance') + unique_together = ('age','sex','weightcategory','distance') def __unicode__(self): - return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.agemin)+'-'+str(self.agemax)+' ('+str(self.season)+')' + return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.age)+' ('+str(self.season)+')' # For future Team functionality class Team(models.Model): From 4b50d6694e7bb38e8c73b718dfd6f86786549b52 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 08:05:32 +0100 Subject: [PATCH 07/18] removed temp file --- rowers/.#metrics.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rowers/.#metrics.py diff --git a/rowers/.#metrics.py b/rowers/.#metrics.py deleted file mode 100644 index 79063b70..00000000 --- a/rowers/.#metrics.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.126064:1512983261 \ No newline at end of file From 098dbb9c4a0c36873dbb14770bc4185533046c0b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 13:40:07 +0100 Subject: [PATCH 08/18] age group records from C2 database web scraping --- rowers/interactiveplots.py | 2 +- rowers/metrics.py | 2 +- rowers/models.py | 85 ++++++++++++++++++++++++-------------- rowers/urls.py | 4 ++ rowers/views.py | 29 +++++++++---- 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 3b187788..3fad9778 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1071,7 +1071,7 @@ def interactive_otwcpchart(powerdf,promember=0): def interactive_agegroup_plot(df): - age = df['agemin'] + age = df['age'] power = df['power'] poly_coefficients = np.polyfit(age,power,6) diff --git a/rowers/metrics.py b/rowers/metrics.py index ee4233bd..e4efe26f 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -329,7 +329,7 @@ def getagegroup2k(age,sex='male',weightcategory='hwt'): ) ) - ages = df['agemin'] + ages = df['age'] powers = df['power'] poly_coefficients = np.polyfit(ages,powers,6) diff --git a/rowers/models.py b/rowers/models.py index 5227e829..8e024c1d 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -151,16 +151,54 @@ class PowerZonesField(models.TextField): value = self._get_val_from_obj(obj) return self.get_deb_prep_value(value) -# Age records -def save_agegroup(df,weightcategory,sex): - for id,row in df.iterrows(): - agemin = int(row['Age2']) - agemax = int(row['Age3']) - duration = row['Time'] - power = int(row['Power']) - season = int(row['Season']) - name = row['Name'] - distance = int(row['Distance']) + +c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All' + +def update_records(url=c2url): + try: + dfs = pd.read_html(url,attrs={'class':'views-table'}) + df = dfs[0] + df.columns = df.columns.str.strip() + success = 1 + except: + df = pd.DataFrame() + + if not df.empty: + C2WorldClassAgePerformance.objects.all().delete() + + df.Gender = df.Gender.apply(lambda x: 'male' if x=='M' else 'female') + df['Distance'] = df['Event'] + df['Duration'] = 0 + + 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'] + else: + df.ix[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 + + print row.Duration + for nr,row in df.iterrows(): + try: + weightcategory = row.Weight.lower() + except AttributeError: + weightcategory = 'hwt' + + sex = row.Gender + name = row.Name + age = int(row.Age) + distance = int(row.Distance) + duration = float(row.Duration) + season = int(row.Season) + + velo = distance/duration + power = int(2.8*velo**3) + record = C2WorldClassAgePerformance( age = age, weightcategory = weightcategory, @@ -171,26 +209,11 @@ def save_agegroup(df,weightcategory,sex): season = season, name = name, ) - record.save() - print record - -def make_records(readfile): - xls = pd.ExcelFile(readfile) - female_df = xls.parse('Female') - female_lw_df = xls.parse('Female LW') - male_df = xls.parse('Male') - male_lw_df = xls.parse('Male LW') - save_agegroup(female_df,'hwt','female') - save_agegroup(male_df,'hwt','male') - save_agegroup(female_lw_df,'lwt','female') - save_agegroup(male_lw_df,'lwt','male') - -c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All' - -def update_records(url=c2url): - dfs = pd.read_html(url,attrs={'class':'views-table'}) - df = dfs[0] - df.columns = df.columns.str.strip() + try: + record.save() + except: + print record + class C2WorldClassAgePerformance(models.Model): weightcategories = ( @@ -215,7 +238,7 @@ class C2WorldClassAgePerformance(models.Model): distance = models.IntegerField(default=2000) name = models.CharField(max_length=200,blank=True) - duration = models.TimeField(default=1,blank=True) + duration = models.FloatField(default=1,blank=True) season = models.IntegerField(default=2013) power = models.IntegerField(default=200) diff --git a/rowers/urls.py b/rowers/urls.py index 51371b36..ac1c26b7 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -121,6 +121,10 @@ urlpatterns = [ url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), + url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)m$', + views.agegrouprecordview), + url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)min$', + views.agegrouprecordview), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)$', views.agegrouprecordview), url(r'^list-workouts/ranking$',views.workouts_view,{'rankingonly':True}), diff --git a/rowers/views.py b/rowers/views.py index 68bf1644..1d78b6b4 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -10932,19 +10932,30 @@ def team_members_stats_view(request,id): from rowers.models import C2WorldClassAgePerformance -def agegrouprecordview(request,sex='male',weightcategory='hwt'): - df = pd.DataFrame( - list( - C2WorldClassAgePerformance.objects.filter( - sex=sex, - weightcategory=weightcategory +def agegrouprecordview(request,sex='male',weightcategory='hwt', + distance=2000,duration=None): + if not duration: + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + distance=distance, + sex=sex, + weightcategory=weightcategory ).values() ) ) - - df['seconds'] = df['duration'].apply( - lambda x:dataprep.timedelta_to_seconds(x) + else: + duration = int(duration)*60 + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + duration=duration, + sex=sex, + weightcategory=weightcategory + ).values() + ) ) + script,div = interactive_agegroup_plot(df) From 451f15398398eba7910887a37140df6a3de4284f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 15:37:52 +0100 Subject: [PATCH 09/18] added age records to ote-bests --- rowers/interactiveplots.py | 58 +++++- rowers/metrics.py | 26 ++- rowers/templates/.#rankings.html | 1 + rowers/templates/rankings.html | 13 +- rowers/urls.py | 6 + rowers/views.py | 340 ++++++++++++++++++++++++++++++- 6 files changed, 431 insertions(+), 13 deletions(-) create mode 100644 rowers/templates/.#rankings.html diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 3fad9778..816eceee 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1101,7 +1101,8 @@ def interactive_agegroup_plot(df): return script,div def interactive_cpchart(rower,thedistances,thesecs,theavpower, - theworkouts,promember=0): + theworkouts,promember=0, + wcpower=[],wcdurations=[]): message = 0 # plot tools @@ -1167,11 +1168,23 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, ) - # fitting the data to three parameter CP model 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 - + p0 = [500,350,10,8000] + wcpower = pd.Series(wcpower) + wcdurations = pd.Series(wcdurations) + + # fitting WC data to three parameter CP model + if len(wcdurations)>=4: + p1wc, success = optimize.leastsq(errfunc, p0[:], + args = (wcdurations,wcpower)) + else: + p1wc = None + + # fitting the data to three parameter CP model + + p1 = p0 if len(thesecs)>=4: @@ -1184,6 +1197,21 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, fitt = pd.Series(10**(4*np.arange(100)/100.)) fitpower = fitfunc(p1,fitt) + if p1wc is not None: + fitpowerwc = 0.95*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 + + message = "" if len(fitpower[fitpower<0]) > 0: @@ -1203,6 +1231,11 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, ), spm = 0*fitpower, power = fitpower, + fitpowerwc = fitpowerwc, + fitpowerexcellent = fitpowerexcellent, + fitpowergood = fitpowergood, + fitpowerfair = fitpowerfair, + fitpoweraverage = fitpoweraverage, fpace = nicepaceformat(fitp2), ) ) @@ -1281,6 +1314,25 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, plot.line('duration','power',source=sourcepaul,legend="Paul's Law") plot.line('duration','power',source=sourcecomplex,legend="CP Model", color='green') + plot.line('duration','fitpowerwc',source=sourcecomplex, + legend="World Class", + color='Maroon',line_dash='dotted') + + plot.line('duration','fitpowerexcellent',source=sourcecomplex, + legend="Excellent", + color='Purple',line_dash='dotted') + + plot.line('duration','fitpowergood',source=sourcecomplex, + legend="Good", + color='Olive',line_dash='dotted') + + plot.line('duration','fitpowerfair',source=sourcecomplex, + legend="Fair", + color='Gray',line_dash='dotted') + + plot.line('duration','fitpoweraverage',source=sourcecomplex, + legend="Average", + color='SkyBlue',line_dash='dotted') script, div = components(plot) diff --git a/rowers/metrics.py b/rowers/metrics.py index e4efe26f..fbd6041e 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -319,12 +319,26 @@ def calc_trimp(df,sex,hrmax,hrmin): return trimp -def getagegroup2k(age,sex='male',weightcategory='hwt'): - df = pd.DataFrame( - list( - C2WorldClassAgePerformance.objects.filter( - sex=sex, - weightcategory=weightcategory +def getagegrouprecord(age,sex='male',weightcategory='hwt', + distance=2000,duration=None): + if not duration: + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + distance=distance, + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + else: + duration=60*int(duration) + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + duration=duration, + sex=sex, + weightcategory=weightcategory ).values() ) ) diff --git a/rowers/templates/.#rankings.html b/rowers/templates/.#rankings.html new file mode 100644 index 00000000..8f484e75 --- /dev/null +++ b/rowers/templates/.#rankings.html @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.12348:1512983261 \ No newline at end of file diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index ecaa84e2..8e82db94 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -160,6 +160,15 @@ {{ the_div|safe }} +

The dashed lines are based on the Concept2 rankings for your age, gender + and weight category. World class means within 5% of World Record in terms + of power. Excellent, Good, and Fair indicate the power levels of the top + 10%, 25% and 50% of the Concept2 rankings. Average is taken + as being in the top 75%, given that the Concept2 rankings probably + represent the more competitive sub-group of all people who erg. + Please note that this is a prediction for people of exactly your age, +and your actual place in the Concept2 Ranking may be different.

+
@@ -259,9 +268,7 @@ {% endif %}
-{% if worldclasspower %} -World class 2k for your age, weight is {{ worldclasspower }} Watt -{% endif %} + {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index ac1c26b7..d0b07cf3 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -165,6 +165,12 @@ urlpatterns = [ url(r'^ote-bests/(?P\d+)$',views.rankings_view), url(r'^ote-bests/$',views.rankings_view), url(r'^(?P\d+)/ote-bests/$',views.rankings_view), + url(r'^(?P\d+)/ote-bests2/(?P\w+.*)/(?P\w+.*)$',views.rankings_view), + url(r'^(?P\d+)/ote-bests2/(?P\d+)$',views.rankings_view2), + url(r'^ote-bests2/(?P\w+.*)/(?P\w+.*)$',views.rankings_view2), + url(r'^ote-bests2/(?P\d+)$',views.rankings_view2), + url(r'^ote-bests2/$',views.rankings_view2), + url(r'^(?P\d+)/ote-bests2/$',views.rankings_view2), url(r'^(?P\d+)/otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), url(r'^(?P\d+)/otw-bests/(?P\d+)$',views.otwrankings_view), url(r'^otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), diff --git a/rowers/views.py b/rowers/views.py index 1d78b6b4..5f8b5d4c 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3223,7 +3223,7 @@ def rankings_view(request,theuser=0, r = getrower(request.user) if r.birthdate: age = calculate_age(r.birthdate) - worldclasspower = int(metrics.getagegroup2k( + worldclasspower = int(metrics.getagegrouprecord( age, sex=r.sex, weightcategory=r.weightcategory @@ -3507,6 +3507,344 @@ def rankings_view(request,theuser=0, 'teams':get_my_teams(request.user), }) +# Show ranking distances including predicted paces +@login_required() +def rankings_view2(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring=""): + + if deltadays>0: + startdate = enddate-datetime.timedelta(days=int(deltadays)) + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + theuser = request.user.id + + promember=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + wcdurations = [] + wcpower = [] + + if r.birthdate: + age = calculate_age(r.birthdate) + durations = [1,4,30,60] + distances = [100,500,1000,2000,5000,6000,10000,21097,42195] + print r.weightcategory,r.sex,age,'aap' + for distance in distances: + worldclasspower = metrics.getagegrouprecord( + age, + sex=r.sex, + distance=distance, + weightcategory=r.weightcategory + ) + velo = (worldclasspower/2.8)**(1./3.) + duration = distance/velo + wcdurations.append(duration) + wcpower.append(worldclasspower) + for duration in durations: + worldclasspower = metrics.getagegrouprecord( + age, + sex=r.sex, + duration=duration, + weightcategory=r.weightcategory + ) + wcdurations.append(60.*duration) + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + wcpower.append(worldclasspower) + else: + worldclasspower = None + + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all indoor rows in date range + + # process form + if request.method == 'POST' and "daterange" in request.POST: + dateform = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + elif request.method == 'POST' and "datedelta" in request.POST: + deltaform = DeltaDaysForm(request.POST) + if deltaform.is_valid(): + deltadays = deltaform.cleaned_data['deltadays'] + if deltadays: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + dateform = DateRangeForm() + deltaform = DeltaDaysForm() + + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + # get all 2k (if any) - this rower, in date range + try: + r = getrower(theuser) + except Rower.DoesNotExist: + allergworkouts = [] + r=0 + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000] + rankingdurations = [] + rankingdurations.append(datetime.time(minute=1)) + rankingdurations.append(datetime.time(minute=4)) + rankingdurations.append(datetime.time(minute=30)) + rankingdurations.append(datetime.time(hour=1,minute=15)) + rankingdurations.append(datetime.time(hour=1)) + + thedistances = [] + theworkouts = [] + thesecs = [] + + + + rankingdistances.sort() + rankingdurations.sort() + + for rankingdistance in rankingdistances: + + workouts = Workout.objects.filter(user=r,distance=rankingdistance, + workouttype__in=['rower','dynamic','slides'], + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by('duration') + if workouts: + thedistances.append(rankingdistance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-6*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + for rankingduration in rankingdurations: + + workouts = Workout.objects.filter(user=r,duration=rankingduration, + workouttype='rower', + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by('-distance') + if workouts: + thedistances.append(workouts[0].distance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-5*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + thedistances = np.array(thedistances) + thesecs = np.array(thesecs) + + thevelos = thedistances/thesecs + theavpower = 2.8*(thevelos**3) + + + # create interactive plot + if len(thedistances) !=0 : + res = interactive_cpchart( + r,thedistances,thesecs,theavpower, + theworkouts,promember=promember, + wcdurations=wcdurations,wcpower=wcpower + ) + script = res[0] + div = res[1] + paulslope = res[2] + paulintercept = res[3] + p1 = res[4] + message = res[5] + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + if request.method == 'POST' and "piece" in request.POST: + form = PredictedPieceForm(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + pieceunit = form.cleaned_data['pieceunit'] + if pieceunit == 'd': + rankingdistances.append(value) + else: + rankingdurations.append(datetime.time(minute=int(value),hour=int(hourvalue))) + else: + form = PredictedPieceForm() + + rankingdistances.sort() + rankingdurations.sort() + + + predictions = [] + cpredictions = [] + + + for rankingdistance in rankingdistances: + # Paul's model + p = paulslope*np.log10(rankingdistance)+paulintercept + velo = 500./p + t = rankingdistance/velo + pwr = 2.8*(velo**3) + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model - + pwr2 = p1[0]/(1+t/p1[2]) + pwr2 += p1[1]/(1+t/p1[3]) + + if pwr2 <= 0: + pwr2 = 50. + + velo2 = (pwr2/2.8)**(1./3.) + + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = rankingdistance/velo2 + + pwr3 = p1[0]/(1+t2/p1[2]) + pwr3 += p1[1]/(1+t2/p1[3]) + + if pwr3 <= 0: + pwr3 = 50. + + velo3 = (pwr3/2.8)**(1./3.) + if np.isnan(velo3) or velo3 <= 0: + velo3 = 1.0 + + t3 = rankingdistance/velo3 + p3 = 500./velo3 + + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t3), + 'pace':timedeltaconv(p3), + 'power':int(pwr3)} + cpredictions.append(a) + + + + + for rankingduration in rankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + # Paul's model + ratio = paulintercept/paulslope + + u = ((2**(2+ratio))*(5.**(3+ratio))*t*np.log(10))/paulslope + + d = 500*t*np.log(10.) + d = d/(paulslope*lambertw(u)) + d = d.real + + velo = d/t + p = 500./velo + pwr = 2.8*(velo**3) + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + if pwr <= 0: + pwr = 50. + + velo = (pwr/2.8)**(1./3.) + + if np.isnan(velo) or velo <=0: + velo = 1.0 + + d = t*velo + p = 500./velo + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + cpredictions.append(a) + + + messages.error(request,message) + return render(request, 'rankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'predictions':predictions, + 'cpredictions':cpredictions, + 'nrdata':len(thedistances), + 'form':form, + 'dateform':dateform, + 'deltaform':deltaform, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + @user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_update_cp_view(request,id=0): try: From 10bb693aac29d117a607d6ce0ac8c9c76c74f4e7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 15:42:16 +0100 Subject: [PATCH 10/18] added link to ranking pieces 2.0 on Analysis page --- rowers/templates/analysis.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 4c8d41da..9d3ec5aa 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -79,8 +79,13 @@
-

 

-
+
+

+ + Ranking Pieces 2.0

+

Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces.

+
+
From 9aa501093c0c560832d1c6c4e5962593248fad5e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 15:43:38 +0100 Subject: [PATCH 11/18] removed unnecessary temp file --- rowers/templates/.#rankings.html | 1 - rowers/templates/rankings.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 rowers/templates/.#rankings.html diff --git a/rowers/templates/.#rankings.html b/rowers/templates/.#rankings.html deleted file mode 100644 index 8f484e75..00000000 --- a/rowers/templates/.#rankings.html +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.12348:1512983261 \ No newline at end of file diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index 8e82db94..299dcd97 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -167,7 +167,7 @@ as being in the top 75%, given that the Concept2 rankings probably represent the more competitive sub-group of all people who erg. Please note that this is a prediction for people of exactly your age, -and your actual place in the Concept2 Ranking may be different.

+ and your actual place in the Concept2 Ranking may be different.

From 8590dc7a8870c2b0e81063676618fd652f7c1b30 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 20:53:02 +0100 Subject: [PATCH 12/18] some improvements --- rowers/interactiveplots.py | 5 ++--- rowers/metrics.py | 11 +++++++---- rowers/templates/rankings.html | 14 ++++++++++---- rowers/views.py | 29 +++++++++++++++++++++-------- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 816eceee..10fefd9f 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1185,7 +1185,6 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, # fitting the data to three parameter CP model - p1 = p0 if len(thesecs)>=4: p1, success = optimize.leastsq(errfunc, p0[:], args = (thesecs,theavpower)) @@ -1271,7 +1270,7 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, if cpdata.empty: message = 'Calculations are running in the background. Please refresh this page to see updated results' - return ['','',paulslope,paulintercept,p1,message] + return ['','',paulslope,paulintercept,p1,message,p1wc] velo = cpdata['distance']/cpdata['delta'] @@ -1338,7 +1337,7 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, script, div = components(plot) - return [script,div,paulslope,paulintercept,p1,message] + return [script,div,paulslope,paulintercept,p1,message,p1wc] def interactive_windchart(id=0,promember=0): # check if valid ID exists (workout exists) diff --git a/rowers/metrics.py b/rowers/metrics.py index fbd6041e..6496b8ab 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -343,11 +343,14 @@ def getagegrouprecord(age,sex='male',weightcategory='hwt', ) ) - ages = df['age'] - powers = df['power'] + if not df.empty: + ages = df['age'] + powers = df['power'] - poly_coefficients = np.polyfit(ages,powers,6) + poly_coefficients = np.polyfit(ages,powers,6) - power = np.polyval(poly_coefficients,age) + power = np.polyval(poly_coefficients,age) + else: + power = 0 return power diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index 299dcd97..8bbcdf26 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -160,15 +160,21 @@ {{ the_div|safe }} -

The dashed lines are based on the Concept2 rankings for your age, gender - and weight category. World class means within 5% of World Record in terms +{% if age %} +

The dashed lines are based on the + Concept2 + rankings for your age, gender + and weight category. World class means within 5% of + + World Record in terms of power. Excellent, Good, and Fair indicate the power levels of the top 10%, 25% and 50% of the Concept2 rankings. Average is taken - as being in the top 75%, given that the Concept2 rankings probably + as being in the top 75%, which is quite good, + given that the Concept2 rankings probably represent the more competitive sub-group of all people who erg. Please note that this is a prediction for people of exactly your age, and your actual place in the Concept2 Ranking may be different.

- +{% endif %}
diff --git a/rowers/views.py b/rowers/views.py index cecd8ea6..e32cc706 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3543,7 +3543,6 @@ def rankings_view2(request,theuser=0, age = calculate_age(r.birthdate) durations = [1,4,30,60] distances = [100,500,1000,2000,5000,6000,10000,21097,42195] - print r.weightcategory,r.sex,age,'aap' for distance in distances: worldclasspower = metrics.getagegrouprecord( age, @@ -3552,9 +3551,12 @@ def rankings_view2(request,theuser=0, weightcategory=r.weightcategory ) velo = (worldclasspower/2.8)**(1./3.) - duration = distance/velo - wcdurations.append(duration) - wcpower.append(worldclasspower) + try: + duration = distance/velo + wcdurations.append(duration) + wcpower.append(worldclasspower) + except ZeroDivisionError: + pass for duration in durations: worldclasspower = metrics.getagegrouprecord( age, @@ -3562,12 +3564,16 @@ def rankings_view2(request,theuser=0, duration=duration, weightcategory=r.weightcategory ) - wcdurations.append(60.*duration) - velo = (worldclasspower/2.8)**(1./3.) - distance = int(60*duration*velo) - wcpower.append(worldclasspower) + try: + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + wcdurations.append(60.*duration) + wcpower.append(worldclasspower) + except ValueError: + pass else: worldclasspower = None + age = 0 result = request.user.is_authenticated() and ispromember(request.user) if result: @@ -3702,6 +3708,10 @@ def rankings_view2(request,theuser=0, paulintercept = res[3] p1 = res[4] message = res[5] + try: + testcalc = float(pd.Series(res[6])) + except TypeError: + age = 0 else: script = '' div = '

No ranking pieces found.

' @@ -3840,6 +3850,9 @@ def rankings_view2(request,theuser=0, 'deltaform':deltaform, 'id': theuser, 'theuser':uu, + 'age':age, + 'sex':r.sex, + 'weightcategory':r.weightcategory, 'startdate':startdate, 'enddate':enddate, 'teams':get_my_teams(request.user), From 035a4bf87fd5c05752500820d06205694c098877 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 21:32:23 +0100 Subject: [PATCH 13/18] added table with World Records to ranking 2.0 --- rowers/templates/rankings.html | 100 +++++++++++++++++++++++++++++++++ rowers/views.py | 3 +- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index 8bbcdf26..04cd245c 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -275,6 +275,106 @@ {% endif %}
+
+ {% if age %} +

World Records

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 100m + +
+ 500m + +
+ 1000m + +
+ 2000m + +
+ 5000m + +
+ 6000m + +
+ 10000m + +
+ Half Marathon + +
+ Full Marathon + +
+ 1 minute + +
+ 4 minutes + +
+ 30 minutes + +
+ 1 hour + +
+{% endif %} +
{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index e32cc706..5f46f332 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3709,9 +3709,10 @@ def rankings_view2(request,theuser=0, p1 = res[4] message = res[5] try: - testcalc = float(pd.Series(res[6])) + testcalc = pd.Series(res[6])*3 except TypeError: age = 0 + else: script = '' div = '

No ranking pieces found.

' From 84448c6a541473bbd888eb65b8a6e465ff76aaae Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 21:39:22 +0100 Subject: [PATCH 14/18] added callouts to age group record graph --- rowers/interactiveplots.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 10fefd9f..611b1926 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1073,6 +1073,8 @@ def interactive_agegroup_plot(df): age = df['age'] power = df['power'] + name = df['name'] + season = df['season'] poly_coefficients = np.polyfit(age,power,6) @@ -1084,17 +1086,29 @@ def interactive_agegroup_plot(df): age = age, power = power, age2 = age2, - poly_vals = poly_vals + poly_vals = poly_vals, + season = season, + name=name, ) ) + + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' - plot = Figure(plot_width=900) + plot = Figure(tools=TOOLS,plot_width=900) plot.circle('age','power',source=source,fill_color='red',size=15, legend='2k Power') plot.line(age2,poly_vals) plot.xaxis.axis_label = "Age" plot.yaxis.axis_label = "Concept2 2k power" + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Name ','@name'), + ('Season ','@season'), + ]) + + hover.mode = 'mouse' script,div = components(plot) From 50c578b8af9640c0808b0e09f52412b80ae8a97c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 13 Dec 2017 21:40:37 +0100 Subject: [PATCH 15/18] improved legend age group chart --- rowers/interactiveplots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 611b1926..e0ab8416 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1096,7 +1096,7 @@ def interactive_agegroup_plot(df): plot = Figure(tools=TOOLS,plot_width=900) plot.circle('age','power',source=source,fill_color='red',size=15, - legend='2k Power') + legend='World Record') plot.line(age2,poly_vals) plot.xaxis.axis_label = "Age" plot.yaxis.axis_label = "Concept2 2k power" From 6b7207321caff13506689f326efa307145e90d7c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Dec 2017 11:57:40 +0100 Subject: [PATCH 16/18] changed naming of rower ranking to percentile --- rowers/interactiveplots.py | 8 ++++---- rowers/templates/rankings.html | 12 +++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index e0ab8416..27f8f908 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1332,19 +1332,19 @@ def interactive_cpchart(rower,thedistances,thesecs,theavpower, color='Maroon',line_dash='dotted') plot.line('duration','fitpowerexcellent',source=sourcecomplex, - legend="Excellent", + legend="90% percentile", color='Purple',line_dash='dotted') plot.line('duration','fitpowergood',source=sourcecomplex, - legend="Good", + legend="75% percentile", color='Olive',line_dash='dotted') plot.line('duration','fitpowerfair',source=sourcecomplex, - legend="Fair", + legend="50% percentile", color='Gray',line_dash='dotted') plot.line('duration','fitpoweraverage',source=sourcecomplex, - legend="Average", + legend="25% percentile", color='SkyBlue',line_dash='dotted') diff --git a/rowers/templates/rankings.html b/rowers/templates/rankings.html index 04cd245c..ee6fc209 100644 --- a/rowers/templates/rankings.html +++ b/rowers/templates/rankings.html @@ -167,13 +167,11 @@ and weight category. World class means within 5% of World Record in terms - of power. Excellent, Good, and Fair indicate the power levels of the top - 10%, 25% and 50% of the Concept2 rankings. Average is taken - as being in the top 75%, which is quite good, - given that the Concept2 rankings probably - represent the more competitive sub-group of all people who erg. - Please note that this is a prediction for people of exactly your age, - and your actual place in the Concept2 Ranking may be different.

+ 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. +

{% endif %} From 03eec117a6d10345d802efad8fa241282f9ca485 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Dec 2017 15:41:01 +0100 Subject: [PATCH 17/18] changed to exponential function --- rowers/interactiveplots.py | 28 ++++++++++++++++++++++------ rowers/metrics.py | 16 ++++++++++++++-- rowers/views.py | 4 +++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 27f8f908..9b4ccc07 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -53,6 +53,7 @@ import stravastuff from rowers.dataprep import rdata import rowers.dataprep as dataprep +import rowers.metrics as metrics from rowers.metrics import axes,axlabels,yaxminima,yaxmaxima @@ -1069,24 +1070,38 @@ def interactive_otwcpchart(powerdf,promember=0): return [script,div,p1,ratio,message] -def interactive_agegroup_plot(df): +def interactive_agegroup_plot(df,distance=2000,duration=None, + sex='male',weightcategory='hwt'): age = df['age'] power = df['power'] name = df['name'] season = df['season'] - poly_coefficients = np.polyfit(age,power,6) +# poly_coefficients = np.polyfit(age,power,6) age2 = np.linspace(11,95) - poly_vals = np.polyval(poly_coefficients,age2) +# poly_vals = np.polyval(poly_coefficients,age2) +# poly_vals = 0.5*(np.abs(poly_vals)+poly_vals) + + fitfunc = lambda pars, x: np.abs(pars[0])*(1-x/max(120,pars[1]))-np.abs(pars[2])*np.exp(-x/np.abs(pars[3]))+np.abs(pars[4])*(np.sin(np.pi*x/max(50,pars[5]))) + errfunc = lambda pars, x,y: fitfunc(pars,x)-y + + p0 = [700,120,700,10,100,100] + + p1, success = optimize.leastsq(errfunc,p0[:], + args = (age,power)) + + expo_vals = fitfunc(p1, age2) + expo_vals = 0.5*(np.abs(expo_vals)+expo_vals) + source = ColumnDataSource( data = dict( age = age, power = power, age2 = age2, - poly_vals = poly_vals, + expo_vals = expo_vals, season = season, name=name, ) @@ -1097,9 +1112,10 @@ def interactive_agegroup_plot(df): plot = Figure(tools=TOOLS,plot_width=900) plot.circle('age','power',source=source,fill_color='red',size=15, legend='World Record') - plot.line(age2,poly_vals) + + plot.line(age2,expo_vals) plot.xaxis.axis_label = "Age" - plot.yaxis.axis_label = "Concept2 2k power" + plot.yaxis.axis_label = "Concept2 power" hover = plot.select(dict(type=HoverTool)) diff --git a/rowers/metrics.py b/rowers/metrics.py index 6496b8ab..72d2d1bb 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -2,6 +2,7 @@ from utils import lbstoN import numpy as np from models import C2WorldClassAgePerformance import pandas as pd +from scipy import optimize rowingmetrics = ( ('time',{ @@ -347,9 +348,20 @@ def getagegrouprecord(age,sex='male',weightcategory='hwt', ages = df['age'] powers = df['power'] - poly_coefficients = np.polyfit(ages,powers,6) + #poly_coefficients = np.polyfit(ages,powers,6) + fitfunc = lambda pars, x: np.abs(pars[0])*(1-x/max(120,pars[1]))-np.abs(pars[2])*np.exp(-x/np.abs(pars[3]))+np.abs(pars[4])*(np.sin(np.pi*x/max(50,pars[5]))) + errfunc = lambda pars, x,y: fitfunc(pars,x)-y - power = np.polyval(poly_coefficients,age) + p0 = [700,120,700,10,100,100] + + p1, success = optimize.leastsq(errfunc,p0[:], + args = (ages,powers)) + + power = fitfunc(p1, float(age)) + + #power = np.polyval(poly_coefficients,age) + + power = 0.5*(np.abs(power)+power) else: power = 0 diff --git a/rowers/views.py b/rowers/views.py index 5f46f332..19db2372 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -11312,7 +11312,9 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt', ) - script,div = interactive_agegroup_plot(df) + script,div = interactive_agegroup_plot(df,sex=sex,distance=distance, + duration=duration, + weightcategory=weightcategory) return render(request, 'agegroupchart.html', { From 7616220da10bd23d3b4a5013fc15a4cba0ae7776 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Dec 2017 16:35:26 +0100 Subject: [PATCH 18/18] added agegroupcp --- rowers/interactiveplots.py | 249 +++++++++++++++++++++++++++++++ rowers/templates/agegroupcp.html | 47 ++++++ rowers/urls.py | 2 + rowers/views.py | 12 ++ 4 files changed, 310 insertions(+) create mode 100644 rowers/templates/agegroupcp.html diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 9b4ccc07..9a5aa37f 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -977,6 +977,255 @@ def googlemap_chart(lat,lon,name=""): return [script,div] +def interactive_agegroupcpchart(age,normalized=False): + durations = [1,4,30,60] + distances = [100,500,1000,2000,5000,6000,10000,21097,42195] + + fhduration = [] + fhpower = [] + + for distance in distances: + worldclasspower = metrics.getagegrouprecord( + age, + sex='female', + distance=distance, + weightcategory='hwt' + ) + velo = (worldclasspower/2.8)**(1./3.) + try: + duration = distance/velo + fhduration.append(duration) + fhpower.append(worldclasspower) + except ZeroDivisionError: + pass + for duration in durations: + worldclasspower = metrics.getagegrouprecord( + age, + sex='female', + duration=duration, + weightcategory='hwt' + ) + try: + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + fhduration.append(60.*duration) + fhpower.append(worldclasspower) + except ValueError: + pass + + flduration = [] + flpower = [] + + for distance in distances: + worldclasspower = metrics.getagegrouprecord( + age, + sex='female', + distance=distance, + weightcategory='lwt' + ) + velo = (worldclasspower/2.8)**(1./3.) + try: + duration = distance/velo + flduration.append(duration) + flpower.append(worldclasspower) + except ZeroDivisionError: + pass + for duration in durations: + worldclasspower = metrics.getagegrouprecord( + age, + sex='female', + duration=duration, + weightcategory='lwt' + ) + try: + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + flduration.append(60.*duration) + flpower.append(worldclasspower) + except ValueError: + pass + + mlduration = [] + mlpower = [] + + for distance in distances: + worldclasspower = metrics.getagegrouprecord( + age, + sex='male', + distance=distance, + weightcategory='lwt' + ) + velo = (worldclasspower/2.8)**(1./3.) + try: + duration = distance/velo + mlduration.append(duration) + mlpower.append(worldclasspower) + except ZeroDivisionError: + pass + for duration in durations: + worldclasspower = metrics.getagegrouprecord( + age, + sex='male', + duration=duration, + weightcategory='lwt' + ) + try: + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + mlduration.append(60.*duration) + mlpower.append(worldclasspower) + except ValueError: + pass + + + mhduration = [] + mhpower = [] + + for distance in distances: + worldclasspower = metrics.getagegrouprecord( + age, + sex='male', + distance=distance, + weightcategory='hwt' + ) + velo = (worldclasspower/2.8)**(1./3.) + try: + duration = distance/velo + mhduration.append(duration) + mhpower.append(worldclasspower) + except ZeroDivisionError: + pass + for duration in durations: + worldclasspower = metrics.getagegrouprecord( + age, + sex='male', + duration=duration, + weightcategory='hwt' + ) + try: + velo = (worldclasspower/2.8)**(1./3.) + distance = int(60*duration*velo) + mhduration.append(60.*duration) + mhpower.append(worldclasspower) + except ValueError: + pass + + + + 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 + + p0 = [500,350,10,8000] + + # fitting WC data to three parameter CP model + if len(fhduration)>=4: + p1fh, success = optimize.leastsq(errfunc, p0[:], + args = (fhduration,fhpower)) + else: + p1fh = None + + # fitting WC data to three parameter CP model + if len(flduration)>=4: + p1fl, success = optimize.leastsq(errfunc, p0[:], + args = (flduration,flpower)) + else: + p1fl = None + + # fitting WC data to three parameter CP model + if len(mlduration)>=4: + p1ml, success = optimize.leastsq(errfunc, p0[:], + args = (mlduration,mlpower)) + else: + p1ml = None + + if len(mhduration)>=4: + p1mh, success = optimize.leastsq(errfunc, p0[:], + args = (mhduration,mhpower)) + else: + p1mh = None + + fitt = pd.Series(10**(4*np.arange(100)/100.)) + + fitpowerfh = fitfunc(p1fh,fitt) + fitpowerfl = fitfunc(p1fl,fitt) + fitpowerml = fitfunc(p1ml,fitt) + fitpowermh = fitfunc(p1mh,fitt) + + if normalized: + facfh = fitfunc(p1fh,60) + facfl = fitfunc(p1fl,60) + facml = fitfunc(p1ml,60) + facmh = fitfunc(p1mh,60) + fitpowerfh /= facfh + fitpowerfl /= facfl + fitpowermh /= facmh + fitpowerml /= facml + fhpower /= facfh + flpower /= facfl + mlpower /= facml + mhpower /= facmh + + + + source = ColumnDataSource( + data = dict( + duration = fitt, + fitpowerfh = fitpowerfh, + fitpowerfl = fitpowerfl, + fitpowerml = fitpowerml, + fitpowermh = fitpowermh, + flduration = flduration, + flpower = flpower, + fhduration = fhduration, + fhpower = fhpower, + mlduration = mlduration, + mlpower = mlpower, + mhduration = mhduration, + mhpower = mhpower, + ) + ) + + x_axis_type = 'log' + y_axis_type = 'linear' + + plot = Figure(plot_width=900,x_axis_type=x_axis_type) + + plot.line('duration','fitpowerfh',source=source, + legend='Female HW',color='blue') + plot.line('duration','fitpowerfl',source=source, + legend='Female LW',color='red') + + plot.line('duration','fitpowerml',source=source, + legend='Male LW',color='green') + + plot.line('duration','fitpowermh',source=source, + legend='Male HW',color='orange') + + + plot.circle('flduration','flpower',source=source, + fill_color='red',size=15) + + plot.circle('fhduration','fhpower',source=source, + fill_color='blue',size=15) + + plot.circle('mlduration','mlpower',source=source, + fill_color='green',size=15) + + plot.circle('mhduration','mhpower',source=source, + fill_color='orange',size=15) + + plot.title.text = 'age '+str(age) + + plot.xaxis.axis_label = "Duration (seconds)" + if normalized: + plot.yaxis.axis_label = "Power (normalized)" + else: + plot.yaxis.axis_label = "Power (W)" + + script,div = components(plot) + + return script,div + def interactive_otwcpchart(powerdf,promember=0): powerdf = powerdf[~(powerdf == 0).any(axis=1)] diff --git a/rowers/templates/agegroupcp.html b/rowers/templates/agegroupcp.html new file mode 100644 index 00000000..2bebbe61 --- /dev/null +++ b/rowers/templates/agegroupcp.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall {% endblock %} + +{% block content %} + + + + + {{ interactiveplot |safe }} + + + + + +
+ + +

Interactive Plot

+ + + {{ the_div|safe }} + +
+ +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index d0b07cf3..0a80c22a 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -121,6 +121,8 @@ urlpatterns = [ url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), + url(r'^agegroupcp/(?P\d+)$',views.agegroupcpview), + url(r'^agegroupcp/(?P\d+)/(?P\d+)$',views.agegroupcpview), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)m$', views.agegrouprecordview), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)min$', diff --git a/rowers/views.py b/rowers/views.py index 19db2372..07e0d6c5 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -11287,6 +11287,18 @@ def team_members_stats_view(request,id): from rowers.models import C2WorldClassAgePerformance +def agegroupcpview(request,age,normalize=0): + script,div = interactive_agegroupcpchart(age,normalized=normalize) + + response = render(request,'agegroupcp.html', + { + 'interactiveplot':script, + 'the_div':div, + } + ) + + return response + def agegrouprecordview(request,sex='male',weightcategory='hwt', distance=2000,duration=None): if not duration: