diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 7bb507f7..2c5b46fa 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -114,7 +114,7 @@ def timedeltaconv(x): return dt # Create new workout from file and store it in the database -# This routine should be used everywhere in views.py and mailprocessing.pu +# This routine should be used everywhere in views.py and mailprocessing.py # Currently there is code duplication def new_workout_from_file(r,f2, workouttype='rower', diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 4935c8a1..c0deb3cd 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -38,35 +38,39 @@
-

Pro

-
-

- {% if user.rower.rowerplan == 'pro' %} - Power Histogram - {% else %} - Power Histogram - {% endif %} -

-

- Plot a power histogram of all your strokes over a date range. -

-
-
-

- Pro Feature 2 -

-

- Reserved for future functionality. -

-
-
-

- Pro Feature 3 -

-

- Reserved for future functionality. -

-
+

Pro

+
+

+ {% if user.rower.rowerplan == 'pro' %} + Power Histogram + {% else %} + Power Histogram + {% endif %} +

+

+ Plot a power histogram of all your strokes over a date range. +

+
+
+

+ {% if user.rower.rowerplan == 'pro' %} + Statistics + {% else %} + Statistics + {% endif %} +

+

+ BETA: Statistics of stroke metrics over a date range +

+
+
+

+ Pro Feature 3 +

+

+ Reserved for future functionality. +

+
diff --git a/rowers/templates/cumstats.html b/rowers/templates/cumstats.html new file mode 100644 index 00000000..59b32911 --- /dev/null +++ b/rowers/templates/cumstats.html @@ -0,0 +1,139 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workout Statistics{% endblock %} + +{% block content %} +
+

Workout Statistics for

+

+ This is an experimental page which just lists a bunch of statistics for + your workouts. This page is under rapid development. +

+
+
+ + + +

Summary for {{ theuser.first_name }} {{ theuser.last_name }} + between {{ startdate|date }} and {{ enddate|date }}

+ +

Direct link for other Pro users: + https://rowsandall.com/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }} +

+
+
+ {% csrf_token %} + {% if workstrokesonly == True %} + + + {% else %} + + + {% endif %} +
+ If your data source allows, this will show or hide strokes taken during rest intervals. +
+
+
+

Use this form to select a different date range:

+

+ Select start and end date for a date range: +

+ +
+ + + {{ form.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+ Or use the last {{ deltaform }} days. +
+
+ {% csrf_token %} + + +
+
+ +
+{% if stats %} +{% for key, value in stats.items %} +

{{ value.verbosename }}

+ + + + + + + + + + + + + + + + + + + + + + + + +
MetricValue
Mean{{ value.mean|floatformat:-2 }}
Minimum{{ value.min|floatformat:-2 }}
25%{{ value.firstq|floatformat:-2 }}
Median{{ value.median|floatformat:-2 }}
75%{{ value.thirdq|floatformat:-2 }}
Maximum{{ value.max|floatformat:-2 }}
Standard Deviation{{ value.std|floatformat:-2 }}
+{% endfor %} +{% endif %} +
+
+ {% if cordict %} +

Correlation table

+

This table indicates a positive (+) or negative (-) correlation between two parameters. The strong correlations are indicated with ++ and --. +

+ + + + + {% for key,value in cordict.items %} + + {% endfor %} + + + + {% for key, thedict in cordict.items %} + + + {% for key2,value in thedict.items %} + + {% endfor %} + + {% endfor %} + +
 
{{ key }}
{{ key }} + {% if value > 0.5 %} +
++
+ {% elif value > 0.1 %} +
+
+ {% elif value < -0.5 %} +
--
+ {% elif value < -0.1 %} +
-
+ {% else %} +   + {% endif %} +
+ + {% endif %} +
+ +{% endblock %} diff --git a/rowers/templates/document_form.html b/rowers/templates/document_form.html index de53ef7d..37ca763a 100644 --- a/rowers/templates/document_form.html +++ b/rowers/templates/document_form.html @@ -39,6 +39,7 @@
  • SpeedCoach GPS and SpeedCoach GPS 2 CSV export
  • ErgData CSV export
  • ErgStick CSV export
  • +
  • BoatCoach CSV export
  • A FIT file with location data (experimental)
  • diff --git a/rowers/urls.py b/rowers/urls.py index c493656a..9d82dfc1 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -128,6 +128,11 @@ urlpatterns = [ url(r'^histo/(?P\w+.*)/(?P\w+.*)$',views.histo), url(r'^histo/(?P\d+)$',views.histo), url(r'^histo/$',views.histo), + url(r'^(?P\d+)/cumstats/(?P\w+.*)/(?P\w+.*)$',views.cumstats), + url(r'^(?P\d+)/cumstats/(?P\d+)$',views.cumstats), + url(r'^cumstats/(?P\w+.*)/(?P\w+.*)$',views.cumstats), + url(r'^cumstats/(?P\d+)$',views.cumstats), + url(r'^cumstats/$',views.cumstats), url(r'^histo-all/$',views.histo_all), url(r'^(?P\d+)/histo-all/$',views.histo_all), url(r'^graph/(\d+)/$',views.graph_show_view), diff --git a/rowers/views.py b/rowers/views.py index 317a467f..9bc7c79f 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1526,6 +1526,8 @@ def workout_histo_view(request,id=0): 'mayedit':mayedit, }) + + # Histogram for a date/time range @login_required() def histo(request,theuser=0, @@ -2604,6 +2606,193 @@ def workout_geeky_view(request,id=0,message="",successmessage=""): 'interactiveplot':script, 'the_div':div}) +# Cumulative stats page +@login_required() +def cumstats(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring="", + workstrokesonly=True): + + workstrokesonly = True + if request.method == 'POST' and 'workstrokesonly' in request.POST: + workstrokesonly = request.POST['workstrokesonly'] + + 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 + + promember=0 + if theuser == 0: + theuser = request.user.id + + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and r.rowerplan=='pro' + if result: + promember=1 + + if not promember: + return HttpResponseRedirect("/rowers/promembership/") + + # get all indoor rows of in date range + # process form + if request.method == 'POST' and "daterange" in request.POST: + form = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.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 != 0: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + + else: + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + try: + r2 = Rower.objects.get(user=theuser) + allergworkouts = Workout.objects.filter(user=r2, + workouttype__in=['rower','dynamic','slides'], + startdatetime__gte=startdate, + startdatetime__lte=enddate) + + except Rower.DoesNotExist: + allergworkouts = [] + r2=0 + + try: + u = User.objects.get(id=theuser) + except User.DoesNotExist: + u = '' + + ids = [workout.id for workout in allergworkouts] + + # Get field names and remove those that are not useful in stats + fields = StrokeData._meta.get_fields() + + fielddict = {field.name:field.verbose_name for field in fields} + + + fielddict.pop('workoutid') + fielddict.pop('ergpace') + fielddict.pop('hr_an') + fielddict.pop('hr_tr') + fielddict.pop('hr_at') + fielddict.pop('hr_ut2') + fielddict.pop('hr_ut1') + fielddict.pop('time') + fielddict.pop('distance') + fielddict.pop('nowindpace') + fielddict.pop('fnowindpace') + fielddict.pop('fergpace') + fielddict.pop('equivergpower') + fielddict.pop('workoutstate') + fielddict.pop('fpace') + fielddict.pop('pace') + fielddict.pop('id') + fielddict.pop('ftime') + fielddict.pop('x_right') + fielddict.pop('hr_max') + fielddict.pop('hr_bottom') + fielddict.pop('cumdist') + + fieldlist = [field for field,value in fielddict.iteritems()] + + # prepare data frame + datadf = dataprep.read_cols_df_sql(ids,fieldlist) + + if datadf.empty: + return HttpResponse("No data found") + + workoutstateswork = [1,4,5,8,9,6,7] + workoutstatesrest = [3] + workoutstatetransition = [0,2,10,11,12,13] + + if workstrokesonly=='True' or workstrokesonly==True: + try: + datadf = datadf[~datadf['workoutstate'].isin(workoutstatesrest)] + except: + pass + workstrokesonly = True + + # Create stats + stats = {} + + + for field,verbosename in fielddict.iteritems(): + thedict = { + 'mean':datadf[field].mean(), + 'min': datadf[field].min(), + 'std': datadf[field].std(), + 'max': datadf[field].max(), + 'median': datadf[field].median(), + 'firstq':datadf[field].quantile(q=0.25), + 'thirdq':datadf[field].quantile(q=0.75), + 'verbosename':verbosename, + } + stats[field] = thedict + + # Create a dict with correlation values + cor = datadf.corr() + cor.fillna(value=0,inplace=True) + cordict = {} + for field1,verbosename in fielddict.iteritems(): + thedict = {} + for field2,verbosename in fielddict.iteritems(): + try: + thedict[field2] = cor.ix[field1,field2] + except KeyError: + thedict[field2] = 0 + + cordict[field1] = thedict + + return render(request, + 'cumstats.html', + { + 'stats':stats, + 'workstrokesonly':workstrokesonly, + 'id':theuser, + 'theuser':u, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'deltaform':deltaform, + 'cordict':cordict, + }) + # Stats page @login_required() def workout_stats_view(request,id=0,message="",successmessage=""):