diff --git a/rowers/admin.py b/rowers/admin.py index 1cbc67be..1004a06b 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','age','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/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..816eceee 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1069,8 +1069,40 @@ def interactive_otwcpchart(powerdf,promember=0): return [script,div,p1,ratio,message] +def interactive_agegroup_plot(df): + + age = df['age'] + 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): + theworkouts,promember=0, + wcpower=[],wcdurations=[]): message = 0 # plot tools @@ -1136,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: @@ -1153,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: @@ -1172,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), ) ) @@ -1250,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 bf1e790b..fbd6041e 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,36 @@ def calc_trimp(df,sex,hrmax,hrmin): trimp = trimpdata.sum() return trimp + +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() + ) + ) + + ages = df['age'] + 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 2fa222f5..8e024c1d 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,103 @@ class PowerZonesField(models.TextField): value = self._get_val_from_obj(obj) return self.get_deb_prep_value(value) + +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, + sex=sex, + distance = distance, + duration = duration, + power = power, + season = season, + name = name, + ) + try: + record.save() + except: + print record + + +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) + + age = models.IntegerField(default=19,verbose_name="Age") + + distance = models.IntegerField(default=2000) + name = models.CharField(max_length=200,blank=True) + duration = models.FloatField(default=1,blank=True) + season = models.IntegerField(default=2013) + power = models.IntegerField(default=200) + + class Meta: + unique_together = ('age','sex','weightcategory','distance') + + def __unicode__(self): + return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.age)+' ('+str(self.season)+')' + # For future Team functionality class Team(models.Model): choices = ( 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 }} + + + + + +
-
Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces.
+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.
+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 @@ -10920,3 +11270,39 @@ def team_members_stats_view(request,id): }) return response + +from rowers.models import C2WorldClassAgePerformance + +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() + ) + ) + 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) + + return render(request, 'agegroupchart.html', + { + 'interactiveplot':script, + 'the_div':div, + }) +