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.
+
+
+
+
+
+
+{% if stats %}
+{% for key, value in stats.items %}
+
{{ value.verbosename }}
+
+
+
+ | Metric |
+ Value |
+
+
+
+
+ | 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 %}
+ {{ key }} |
+ {% endfor %}
+
+
+
+ {% for key, thedict in cordict.items %}
+
+ | {{ key }} |
+ {% for key2,value in thedict.items %}
+
+ {% if value > 0.5 %}
+ ++
+ {% elif value > 0.1 %}
+ +
+ {% elif value < -0.5 %}
+ --
+ {% elif value < -0.1 %}
+ -
+ {% else %}
+
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+ {% 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=""):