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
+
+{% 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: