From af6226799625442ec3062ace168d0df3481db032 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 Apr 2024 13:35:02 +0200 Subject: [PATCH] some testing, some pandas to polars in tasks.py --- rowers/dataprep.py | 3 +- rowers/dataroutines.py | 7 +- rowers/integrations/c2.py | 2 +- rowers/interactiveplots.py | 351 +------------------------- rowers/tasks.py | 131 ++-------- rowers/tests/test_async_tasks.py | 44 ---- rowers/tests/test_unit_tests.py | 2 +- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes rowers/views/statements.py | 2 +- 9 files changed, 35 insertions(+), 507 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index c69aeffa..c5195951 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -89,10 +89,9 @@ from rowers.dataroutines import * from rowers.tasks import ( handle_sendemail_newftp, - handle_sendemail_unrecognized, handle_setcp, + handle_sendemail_unrecognized, handle_getagegrouprecords, handle_update_wps, handle_request_post, handle_calctrimp, - handle_updatecp, handle_updateergcp, handle_sendemail_breakthrough, handle_sendemail_hard, ) diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index 09e70f02..dad0dfc6 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -2705,7 +2705,7 @@ def create_c2_stroke_data_db( else: power = 0 - df = pd.DataFrame({ + df = pl.DataFrame({ 'TimeStamp (sec)': unixtime, ' Horizontal (meters)': d, ' Cadence (stokes/min)': spm, @@ -2726,9 +2726,10 @@ def create_c2_stroke_data_db( 'cum_dist': d }) - df[' ElapsedTime (sec)'] = df['TimeStamp (sec)'] + df = df.with_columns((pl.col("TimeStamp (sec)")).alias(" ElapsedTime (sec)")) - _ = df.to_csv(csvfilename, index_label='index', compression='gzip') + row = rrdata_pl(df=df) + row.writecsv(csvfilename, compression=True) data = dataplep(df, id=workoutid, bands=False, debug=debug) diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 1a7268ea..2a4a57ab 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -2,7 +2,7 @@ from .integrations import SyncIntegration, NoTokenError, create_or_update_syncre from rowers.models import User, Rower, Workout, TombStone from django.db.utils import IntegrityError -from rowingdata import rowingdata +from rowingdata import rowingdata, rowingdata_pl import numpy as np import datetime import json diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 50b3f2bf..072fe65c 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -61,7 +61,7 @@ from rowers.models import ( Workout, User, Rower, WorkoutForm, RowerForm, GraphImage, GeoPolygon, GeoCourse, GeoPoint, ) -from rowers.tasks import handle_setcp + from rowingdata import rower as rrower from rowingdata import main as rmain from rowingdata import cumcpdata, histodata @@ -1380,7 +1380,7 @@ def interactive_chart(id=0, promember=0, intervaldata={}): columns = ['time', 'pace', 'hr', 'fpace', 'ftime', 'spm'] datadf = dataprep.getsmallrowdata_pl(columns, ids=[id]) - if datadf.is_empty(): + if datadf.is_eompty(): return "", "No Valid Data Available" datadf = datadf.fill_nan(None).drop_nulls() @@ -2372,223 +2372,6 @@ def get_zones_report_pl(rower, startdate, enddate, trainingzones='hr', date_agg= return chart_data - - -def get_zones_report(rower, startdate, enddate, trainingzones='hr', date_agg='week', - yaxis='time'): - - dates = [] - dates_sorting = [] - minutes = [] - hours = [] - zones = [] - - enddate = enddate + datetime.timedelta(days=1) - - workouts = Workout.objects.filter( - user=rower, - startdatetime__gte=startdate, - startdatetime__lte=enddate, - duplicate=False, - ).order_by("-startdatetime") - - ids = [w.id for w in workouts] - - columns = ['workoutid', 'hr', 'power', 'time'] - - df = dataprep.getsmallrowdata_db(columns, ids=ids) - try: - df['deltat'] = df['time'].diff().clip(lower=0).clip(upper=20*1e3) - except KeyError: # pragma: no cover - pass - - df = dataprep.clean_df_stats(df, workstrokesonly=False, - ignoreadvanced=True, ignorehr=False) - - hrzones = rower.hrzones - powerzones = rower.powerzones - - for w in workouts: - dd3 = w.date.strftime('%Y/%m') - dd4 = '{year}/{week:02d}'.format( - week=arrow.get(w.date).isocalendar()[1], - year=w.date.strftime('%y') - ) - dd4 = (w.date - datetime.timedelta(days=w.date.weekday()) - ).strftime('%y/%m/%d') - - # print(w.date,arrow.get(w.date),arrow.get(w.date).isocalendar()) - iswater = w.workouttype in mytypes.otwtypes - qryw = 'workoutid == {workoutid}'.format(workoutid=w.id) - - qry = 'hr < {ut2}'.format(ut2=rower.ut2) - if trainingzones == 'power': - qry = 'power < {ut2}'.format(ut2=rower.pw_ut2) - timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3) - if date_agg == 'week': - dates.append(dd4) - dates_sorting.append(dd4) - else: # pragma: no cover - dates.append(dd3) - dates_sorting.append(dd3) - minutes.append(timeinzone) - hours.append(timeinzone/60.) - if trainingzones == 'hr': - zones.append('<{ut2}'.format(ut2=hrzones[1])) - else: - zones.append('<{ut2}'.format(ut2=powerzones[1])) - # print(w,dd,timeinzone,'= enddate: # pragma: no cover - st = startdate - startdate = enddate - enddate = st - - hrzones = rower.hrzones - powerzones = rower.powerzones - - color_map = { - '<{ut2}'.format(ut2=hrzones[1]): 'green', - hrzones[1]: 'lime', - hrzones[2]: 'yellow', - hrzones[3]: 'blue', - hrzones[4]: 'purple', - hrzones[5]: 'red', - } - if trainingzones == 'power': - color_map = { - '<{ut2}'.format(ut2=powerzones[1]): 'green', - powerzones[1]: 'lime', - powerzones[2]: 'yellow', - powerzones[3]: 'blue', - powerzones[4]: 'purple', - powerzones[5]: 'red', - } - - zones_order = [ - '<{ut2}'.format(ut2=hrzones[1]), - hrzones[1], - hrzones[2], - hrzones[3], - hrzones[4], - hrzones[5] - ] - - if trainingzones == 'power': - zones_order = [ - '<{ut2}'.format(ut2=powerzones[1]), - powerzones[1], - powerzones[2], - powerzones[3], - powerzones[4], - powerzones[5] - ] - - df = pd.DataFrame(data) - df2 = pd.DataFrame(data) - - df.drop('minutes', inplace=True, axis='columns') - - df.sort_values('date_sorting', inplace=True) - df.drop('date_sorting', inplace=True, axis='columns') - df['totaltime'] = 0 - if df.empty: # pragma: no cover - return '', 'No Data Found' - - if yaxis == 'percentage': - dates = list(set(df['date'].values)) - for date in dates: - qry = 'date == "{d}"'.format(d=date) - - totaltime = df.query(qry)['hours'].sum() - - mask = df['date'] == date - df.loc[mask, 'totaltime'] = totaltime - - df['percentage'] = 100.*df['hours']/df['totaltime'] - df.drop('hours', inplace=True, axis='columns') - df.drop('totaltime', inplace=True, axis='columns') - - hv.extension('bokeh') - - xrotation = 0 - nrdates = len(list(set(df['date'].values))) - if nrdates > 10: - xrotation = 45 - - bars = hv.Bars(df, kdims=['date', 'zones']).aggregate( - function=np.sum).redim.values(zones=zones_order) - - bars.opts( - opts.Bars(cmap=color_map, show_legend=True, stacked=True, - tools=['tap', 'hover'], width=550, padding=(0, (0, .1)), - legend_position='bottom', - xrotation=xrotation, - show_frame=False) - ) - - p = hv.render(bars) - - p.title.text = 'Activity {d1} to {d2} for {r}'.format( - d1=startdate.strftime("%Y-%m-%d"), - d2=enddate.strftime("%Y-%m-%d"), - r=str(rower), - ) - - if date_agg == 'week': - p.xaxis.axis_label = 'Week' - else: # pragma: no cover - p.xaxis.axis_label = 'Month' - - if yaxis == 'percentage': - p.yaxis.axis_label = 'Percentage' - - p.width = 550 - p.height = 350 - p.toolbar_location = 'right' - p.y_range.start = 0 - #p.sizing_mode = 'stretch_both' - - if yaxis == 'percentage': - tidy_df = df2.groupby(['date']).sum() - - source2 = ColumnDataSource(tidy_df) - y2rangemax = tidy_df.loc[:, 'hours'].max()*1.1 - p.extra_y_ranges["yax2"] = Range1d(start=0, end=y2rangemax) - p.line('date', 'hours', source=source2, - y_range_name="yax2", color="black", width=5) - p.circle('date', 'hours', source=source2, y_range_name="yax2", color="black", size=10) - -# p.circle('date', 'hours', source=source2, y_range_name="yax2", color="black", size=10, -# legend_label='Hours') - p.add_layout(LinearAxis(y_range_name="yax2", - axis_label='Hours'), 'right') - - script, div = components(p) - - return script, div diff --git a/rowers/tasks.py b/rowers/tasks.py index f42891c9..1a54e8c3 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -56,7 +56,7 @@ from scipy.signal import savgol_filter from scipy.interpolate import griddata import rowingdata -from rowingdata import make_cumvalues +from rowingdata import make_cumvalues, make_cumvalues_array from uuid import uuid4 from rowingdata import rowingdata as rdata @@ -115,6 +115,9 @@ tpapilocation = TP_API_LOCATION from requests_oauthlib import OAuth1, OAuth1Session import pandas as pd +import polars as pl +from polars.exceptions import ColumnNotFoundError + from django_rq import job from django.utils import timezone @@ -2583,53 +2586,6 @@ def handle_otwsetpower(self, f1, boattype, boatclass, coastalbrand, weightvalue, return 1 -@app.task -def handle_updateergcp(rower_id, workoutfilenames, debug=False, **kwargs): - therows = [] - for f1 in workoutfilenames: - try: - rowdata = rdata(csvfile=f1) - except IOError: # pragma: no cover - try: - rowdata = rdata(csvfile=f1 + '.csv') - except IOError: - try: - rowdata = rdata(csvfile=f1 + '.gz') - except IOError: - rowdata = 0 - if rowdata != 0: - therows.append(rowdata) - - cpdata = rowingdata.cumcpdata(therows) - cpdata.columns = cpdata.columns.str.lower() - - updatecpdata_sql(rower_id, cpdata['delta'], cpdata['cp'], - table='ergcpdata', distance=cpdata['distance'], - debug=debug) - - return 1 - - -@app.task -def handle_updatecp(rower_id, workoutids, debug=False, table='cpdata', **kwargs): - columns = ['power', 'workoutid', 'time'] - df = getsmallrowdata_db(columns, ids=workoutids, debug=debug) - - if df.empty: # pragma: no cover - return 0 - - maxt = 1.05*df['time'].max()/1000. - - logarr = datautils.getlogarr(maxt) - - dfgrouped = df.groupby(['workoutid']) - - delta, cpvalue, avgpower = datautils.getcp(dfgrouped, logarr) - - updatecpdata_sql(rower_id, delta, cpvalue, debug=debug, table=table) - - return 1 - @app.task def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename, @@ -3179,43 +3135,6 @@ def handle_sendemail_invite_reject(email, name, teamname, managername, return 1 -@app.task -def handle_setcp(strokesdf, filename, workoutid, debug=False, **kwargs): - try: - os.remove(filename) - except FileNotFoundError: - pass - if not strokesdf.empty: - - try: - totaltime = strokesdf['time'].max() - except KeyError: # pragma: no cover - return 0 - try: - powermean = strokesdf['power'].mean() - except KeyError: # pragma: no cover - powermean = 0 - - if powermean != 0: - thesecs = totaltime - maxt = 1.05 * thesecs - - if maxt > 0: - logarr = datautils.getlogarr(maxt) - dfgrouped = strokesdf.groupby(['workoutid']) - delta, cpvalues, avgpower = datautils.getcp(dfgrouped, logarr) - - df = pd.DataFrame({ - 'delta': delta, - 'cp': cpvalues, - 'id': workoutid, - }) - df.to_parquet(filename, engine='fastparquet', - compression='GZIP') - return 1 - - return 1 # pragma: no cover - @app.task def handle_sendemail_invite_accept(email, name, teamname, managername, @@ -3647,7 +3566,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec, loncoord = np.zeros(nr_rows) try: - strokelength = strokedata.loc[:, 'strokelength'] + strokelength = strokedata.loc[:,'strokelength'] except: # pragma: no cover strokelength = np.zeros(nr_rows) @@ -3901,7 +3820,7 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, pace[np.isinf(pace)] = 0.0 try: - strokedata = pd.DataFrame({'t': 10*t, + strokedata = pl.DataFrame({'t': 10*t, 'd': 10*d, 'p': 10*pace, 'spm': spm, @@ -3947,18 +3866,18 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, starttimeunix = arrow.get(rowdatetime).timestamp() - res = make_cumvalues(0.1*strokedata['t']) - cum_time = res[0] - lapidx = res[1] + res = make_cumvalues_array(0.1*strokedata['t'].to_numpy()) + cum_time = pl.Series(res[0]) + lapidx = pl.Series(res[1]) unixtime = cum_time+starttimeunix - seconds = 0.1*strokedata.loc[:, 't'] + seconds = 0.1*strokedata['t'] nr_rows = len(unixtime) try: - latcoord = strokedata.loc[:, 'lat'] - loncoord = strokedata.loc[:, 'lon'] + latcoord = strokedata['lat'] + loncoord = strokedata['lon'] if latcoord.std() == 0 and loncoord.std() == 0 and workouttype == 'water': # pragma: no cover workouttype = 'rower' except: # pragma: no cover @@ -3968,29 +3887,29 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, workouttype = 'rower' try: - strokelength = strokedata.loc[:, 'strokelength'] + strokelength = strokedata['strokelength'] except: # pragma: no cover strokelength = np.zeros(nr_rows) - dist2 = 0.1*strokedata.loc[:, 'd'] + dist2 = 0.1*strokedata['d'] try: - spm = strokedata.loc[:, 'spm'] - except KeyError: # pragma: no cover + spm = strokedata['spm'] + except (KeyError, ColumnNotFoundError): # pragma: no cover spm = 0*dist2 try: - hr = strokedata.loc[:, 'hr'] - except KeyError: # pragma: no cover + hr = strokedata['hr'] + except (KeyError, ColumnNotFoundError): # pragma: no cover hr = 0*spm - pace = strokedata.loc[:, 'p']/10. + pace = strokedata['p']/10. pace = np.clip(pace, 0, 1e4) - pace = pace.replace(0, 300) + pace = pl.Series(pace).replace(0, 300) velo = 500./pace try: - power = strokedata.loc[:, 'power'] + power = strokedata['power'] except KeyError: # pragma: no cover power = 2.8*velo**3 @@ -3999,7 +3918,7 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, # save csv # Create data frame with all necessary data to write to csv - df = pd.DataFrame({'TimeStamp (sec)': unixtime, + df = pl.DataFrame({'TimeStamp (sec)': unixtime, ' Horizontal (meters)': dist2, ' Cadence (stokes/min)': spm, ' HRCur (bpm)': hr, @@ -4019,10 +3938,10 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, 'cum_dist': dist2, }) - df.sort_values(by='TimeStamp (sec)', ascending=True) + df.sort('TimeStamp (sec)') - row = rowingdata.rowingdata(df=df) - row.write_csv(csvfilename, gzip=False) + row = rowingdata.rowingdata_pl(df=df) + row.write_csv(csvfilename, compressed=False) # summary = row.allstats() # maxdist = df['cum_dist'].max() diff --git a/rowers/tests/test_async_tasks.py b/rowers/tests/test_async_tasks.py index f644e53d..5aab5a37 100644 --- a/rowers/tests/test_async_tasks.py +++ b/rowers/tests/test_async_tasks.py @@ -484,51 +484,7 @@ class AsyncTaskTests(TestCase): res = tasks.handle_c2_import_stroke_data(c2token,c2id,workoutid,starttimeunix,csvfilename) self.assertEqual(res,1) - @patch('rowers.tasks.grpc',side_effect=mocked_grpc) - @patch('rowers.tasks.send_template_email',side_effect=mocked_send_template_email) - def test_handle_otwsetpower(self,mocked_send_template_email,mocked_grpc): - f1 = get_random_file(filename='rowers/tests/testdata/sprintervals.csv')['filename'] - boattype = '1x' - boatclass = 'water' - coastalbrand = 'other' - weightvalue = 80. - first_name = self.u.first_name - last_name = self.u.last_name - email = self.u.email - workoutid = self.wwater.id - job = fakerequest() - res = tasks.handle_otwsetpower(f1,boattype,boatclass,coastalbrand, - weightvalue,first_name,last_name,email,workoutid, - jobkey='23') - - self.assertEqual(res,1) - - @patch('rowers.dataprep.create_engine') - def test_handle_updateergcp(self,mocked_sqlalchemy): - f1 = get_random_file()['filename'] - res = tasks.handle_updateergcp(1,[f1]) - self.assertEqual(res,1) - - - @patch('rowers.dataprep.getsmallrowdata_db') - def test_handle_updatecp(self,mocked_getsmallrowdata_db_updatecp): - rower_id = 1 - workoutids = [1] - res = tasks.handle_updatecp(rower_id,workoutids) - self.assertEqual(res,1) - - @patch('rowers.dataprep.getsmallrowdata_db') - def test_handle_setcp(self,mocked_getsmallrowdata_db_setcp): - strokesdf = pd.read_csv('rowers/tests/testdata/uhfull.csv') - filename = 'rowers/tests/testdata/temp/pq.gz' - workoutids = 1 - res = tasks.handle_setcp(strokesdf,filename,1) - self.assertEqual(res,1) - try: - os.remove(filename) - except FileNotFoundError: - pass @patch('rowers.dataprep.getsmallrowdata_db') def test_handle_update_wps(self,mocked_getsmallrowdata_db_wps): diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index 92903b5e..5e57721c 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -703,7 +703,7 @@ class InteractivePlotTests(TestCase): self.assertFalse(len(div)==0) @patch('rowers.dataprep.create_engine') - @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) + @patch('rowers.dataprep.getsmallrowdata_pl', side_effect=mocked_getsmallrowdata_pl) def test_interactive_chart(self, mocked_sqlalchemy, mocked_getsmallrowdata_db): workout = Workout.objects.filter(user=self.r,workouttype__in=mytypes.rowtypes)[0] diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 3826d3a2acbe5db0977e25fc8549d45c7c3e72fc..d4c7d683121479b2057f01d3d1792b90e5e3bdf1 100644 GIT binary patch literal 4000 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 GfB^s+065D4 literal 3999 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 F0RWDHHmd*t diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 4eb83f0a..991b440f 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -252,7 +252,7 @@ from rowers.tasks import ( handle_sendemailfile, handle_sendemailkml, handle_sendemailnewresponse, handle_updatedps, - handle_updatecp, long_test_task, long_test_task2, + long_test_task, long_test_task2, handle_zip_file, handle_getagegrouprecords, handle_update_empower, handle_sendemailics,