Podsumowanie wyników dla lat 2015--2017
z16 <- read.csv("wyniki_zulawy_2016_D.csv", sep = ';', header=T, na.string="NA", dec=","); aggregate (z16$time, list(Numer = z16$dist), summary) z16$year <- 2016; z15 <- read.csv("wyniki_zulawy_2015_D.csv", sep = ';', header=T, na.string="NA", dec=","); aggregate (z15$time, list(Numer = z15$dist), summary) z15$year <- 2015; z17 <- read.csv("wyniki_zulawy_2017_D.csv", sep = ';', header=T, na.string="NA", dec="."); aggregate (z17$time, list(Numer = z17$dist), summary) z17$year <- 2017; zz15 <- z15[, c("dist", "kmH", "time", "year")]; zz16 <- z16[, c("dist", "kmH", "time", "year")]; zz17 <- z17[, c("dist", "kmH", "time", "year")]; zz <- rbind (zz15, zz16, zz17); ## tylko dystans 140 zz140 <- subset (zz, ( dist == 140 )); sum140 <- aggregate (zz140$kmH, list(Numer = zz140$year), summary) boxplot (kmH ~ year, zz140, ylab = "Śr.prędkość [kmh]", col = "yellow", main="140km" ) ## tylko dystans 55 zz75 <- subset (zz, ( dist > 60 & dist < 90 )); sum75 <- aggregate (zz75$kmH, list(Numer = zz75$year), summary) sum75 boxplot (kmH ~ year, zz75, ylab = "Śr.prędkość [kmh]", col = "yellow", main="80/75km" ) ## tylko dystans 55 zz55 <- subset (zz, ( dist < 60 )); sum55 <- aggregate (zz55$kmH, list(Numer = zz55$year), summary) sum55 xl <- paste ("średnie 2015=", sum55$x[1,4], "kmh 2016=", sum55$x[2,4], "kmh 2017=", sum55$x[3,4], " kmh") boxplot (kmH ~ year, zz55, xlab = xl, ylab = "Śr.prędkość [kmh]", col = "yellow", main="55km" )
A ja (numer 418) byłem 70 w kategorii 140 km, z czasem 5:42:03 co dało 24,56 kmh przeciętną. Do pierwszego bufetu się spinałem, potem już nie...
Jutro planuję przejechać 140km biorąc udział w imprezie pn. Żuławy wKoło 2017. Niby Żuławy a profil trasy sugeruje jakieś istotne wzniesienie w okolicach 25--40km:
Uważnie przyjrzenie się liczbom (zwłaszcza na osi OY) pozwala stwierdzić, że jest to złudzenie, wynikające z różnicy w jednostkach miary obu osi (kilometry vs metry). W rzeczywistości góra tam jest symboliczna o czym można się przekonać robiąc wykres nachyleń. Żeby pozbyć się przypadkowych błędów związanych z niedokładnością pomiaru oryginalne 673 punktowe dane zostały zmienione na 111 punktowe (uśrednienie minimum 1 km) lub 62 punktowe (uśrednienie minimum 2 km).
Przy czym uśrednienie minimum $x$ oznacza obliczenie nachylenia dla najkrótszego odcinka kolejnych $n$ punktów z oryginalnego śladu GPX, który będzie dłuższy niż $x$.
Skrypty R/dane są tutaj. Oryginale ślady GPX/TCX skopiowane ze strony ŻwK są tutaj.
Żuławy w koło to maraton rowerowy (czyli przejazd rowerem na dłuższym dystansie -- nie mylić z wyścigiem) organizowany od paru lat na Żuławach jak nazwa wskazuje. Sprawdziłem jak ta impreza wyglądała pod kątem prędkości w roku 2016. W tym celu ze strony Wyniki żUŁAWY wKOŁO 2016 ściągnąłem stosowny plik PDF z danymi, który następnie skonwertowałem do pliku w formacie XLS (Excel) wykorzystując konwerter on-line tajemniczej firmy convertio.pl. Tajemniczej w tym sensie, że nie znalazłem informacji kto i po co tą usługę świadczy.
Konwersja (do formatu CSV) -- jak to zwykle konwersja -- nie poszła na 100% poprawnie i wymagała jeszcze circa 30 minutowej ręcznej obróbki. Być może zresztą są lepsze konwertery, ale problem był z gatunku banalnych i wolałem stracić 30 minut na poprawianiu wyników konwersji niż 2 godziny na ustalaniu, który z konwerterów on-line konwertuje ten konkretny plik PDF (w miarę) bezbłędnie.
Po konwersji wypadało by sprawdzić (chociaż zgrubnie) czy wszystko jest OK.
## Czy każdy wiersz zawieraja 9 pól (powinien) $ awk -F ';' 'NF != 9 {print NR, NF}' wyniki_zulawy_2016S.csv ## Ilu było uczestników na dystansie 140km? $ awk -F ';' '$7 ==140 {print $0}' wyniki_zulawy_2016S.csv | wc -l 133 ## Ilu było wszystkich (winno być 567 + 1 nagłówek) $ cat wyniki_zulawy_2016S.csv | wc -l 568 # ok!
Do analizy statystycznej wykorzystano wykres pudełkowy (porównanie wyników na różnych dystansach) oraz histogram (rozkład średnich prędkości na dystansie 140km). BTW gdyby ktoś nie wiedział co to jest wykres pudełkowy to wyjaśnienie jest na rysunku obok. Objaśnienie: Me, $Q_1$, $Q_3$ to odpowiednio mediana i kwartyle. Dolna/górna krawędź prostokąta wyznacza zatem rozstęp kwartylny (IQR). Wąsy ($W_L$/$W_U$) są wyznaczane jako 150% wartości rozstępu kwartylnego. Wartości leżące poza ,,wąsami'' (nietypowe) są oznaczane kółkami.
Ww. wykresy wygenerowano następującym skryptem:
# co <- "Żuławy wKoło 2016" # z <- read.csv("wyniki_zulawy_2016_C.csv", sep = ';', header=T, na.string="NA", dec=","); aggregate (z$meanv, list(Numer = z$dist), fivenum) boxplot (meanv ~ dist, z, xlab = "Dystans [km]", ylab = "Śr.prędkość [kmh]", col = "yellow", main=co ) ## tylko dystans 140 z140 <- subset (z, ( dist == 140 )); ## statystyki zbiorcze s140 <- summary(z140$meanv) names(s140) summary_label <- paste (sep='', "Średnia = ", s140[["Mean"]], "\nMediana = ", s140[["Median"]], "\nQ1 = ", s140[["1st Qu."]], "\nQ3 = ", s140[["3rd Qu."]], "\n\nMax = ", s140[["Max."]] ) # drukuje wartości kolumny meanv # z140$meanv # drukuje wartości statystyk zbiorczych s140 # wykres słupkowy h <- hist(z140$meanv, breaks=c(14,18,22,26,30,34,38), freq=TRUE, col="orange", main=paste (co, "[140km]"), # tytuł xlab="Prędkość [kmh]",ylab="L.kolarzy", labels=T, xaxt='n' ) # xaxt usuwa domyślną oś # axis definiuje lepiej oś OX axis(side=1, at=c(14,18,22,26,30,34,38)) text(38, 37, summary_label, cex = .8, adj=c(1,1) )
Dane i wyniki są tutaj
Informacja o kadencji jest w pliku .tcx rejestrowana (przez GarminaEdge 500) w następujący sposób:
<Activities> <Activity Sport="Biking"> <Lap StartTime="2017-08-31T14:51:16Z"> <Cadence>77</Cadence> ... <Track> <Trackpoint> <Time>2017-08-31T14:52:02Z</Time> <Cadence>0</Cadence> </Trackpoint> <Trackpoint> <Time>2017-08-31T14:52:06Z</Time> <Cadence>46</Cadence> </Trackpoint> <Trackpoint> <Time>2017-08-31T14:52:07Z</Time> <Cadence>53</Cadence> </Trackpoint>
Lap-ów może być wiele. Każdy zawiera element Track
,
w którym zapisana jest informacja o fragmencie trasy. Pomiędzy
Lap
a Track
znajduje się nagłówek zawierający różne
obliczone/zbiorcze informacje, m.in. średnią kadencję. Co by oznaczało, że
w powyższym przykładzie średnia kadencja wynosiła
77 obrotów/min. Na odcinku od 14:52:02Z do 14:52:06Z (4 sekundy) średnia kadencja wyniosła 46 o/min,
zaś na odcinku 14:52:06Z--07Z (1s) 53 o/min.
Można też policzyć kadencję samodzielnie, co pozwoli na uzyskanie dodatkowej informacji. Liczenie kadencji jest proste. Mnożąc średnią kadencję odcinka razy czas w sekundach otrzymamy liczbę obrotów korby na tym odcinku (kadencję należy podzielić przez 60 bo jest podana w minutach). Odcinki o zerowej kadencji pomija się (Garmin też tak oblicza BTW). Dzieląc łączną liczbę obrotów przez czas otrzymamy średnią kadencję. Ja dodałem opcję pomijania odcinków o pewnej minimalnej prędkości (np. 8 kmh) -- takie odcinki to zwykle jakieś nietypowe fragmenty, zaniżające tylko średnią.
#!/usr/bin/perl # Cadence calculator for GarminEdge tcx files (or converted fits) # tprzechlewski@gmail.com use Getopt::Long; my $prev_Time = -1; my $parsingTrack ='N'; my $regCadence = 'N'; my $min_speed = -1; # minimum speed my $trackNo=1; GetOptions( "s=f" => \$min_speed, ); $min_speedMS = ($min_speed *1000)/3600.0; if ($min_speed > 0) { print "*** Segments with speed below $min_speed kmh skipped ***\n"; } while (<>) { chomp(); # Order is importany (first check for <Track>: if ( /<Track>/ ) { if ( $regCadence eq 'N' ) { ## Check if Cadence is registered print STDERR "*** No cadence registered! ***\n"; exit 1; } else { $parsingTrack = 'Y' ; print STDERR "### Parsing track #$trackNo...\n"; $trackNo++; } } # Parsing header: if ($parsingTrack eq 'N' ) { if ( /<TotalTimeSeconds>/) { $edge_LapTime = xmlEleValue('TotalTimeSeconds', $_) ; } elsif (/<Cadence>/) { $edge_LapCadence = xmlEleValue('Cadence', $_) ; $regCadence = 'Y' } elsif (/<DistanceMeters>/) { $edge_LapDist = xmlEleValue('DistanceMeters', $_) ; } ##print STDERR "## Parsing track info: $_\n"; next; } ## start parsing Track now: if (/Speed>/) { $speed = xmlEleValue ('Speed', $_); } elsif ($_ =~ /<Time>(.*)T(.*)Z<\/Time>/ ) { $time = $2; ($h, $m, $s ) = split /:/, $time; $current_time = $h * 60 * 60 + $m * 60 + $s ; ##print STDERR "$current_time\n"; } elsif (/<Cadence>/ ) { $cadence = xmlEleValue ('Cadence', $_); } elsif (/<DistanceMeters>/ ) { $distance = xmlEleValue ('DistanceMeters', $_); } elsif ($_ =~ /<\/Trackpoint>/ ) { if ( $prevTime > 0 ) {## pomija pierwsze $lastTime = $current_time ; if ($cadence > 0 && $speed > $min_speedMS ) { $timeDiff = $current_time - $prevTime; $total_time_cycled += $timeDiff ; $total_cycles += $cadence * $timeDiff / 60; $prevTime = $current_time ; } else { $timeDiff = $current_time - $prevTime; $total_time_idle += $timeDiff ; $prevTime = $current_time ; } } else { $prevTime = $current_time ; $firstTime = $current_time ; } } if ( /<\/Track>/ ) { $total_Time = $total_time_idle + $total_time_cycled; printf "Time = Idle: %d s Spinning: %d s Total: %d s\n", $total_time_idle, $total_time_cycled, $total_Time; printf "Time = Idle: %.2f%% Spinning: %.2f%% Total: %.2f%%\n", $total_time_idle/$total_Time *100, $total_time_cycled/$total_Time * 100, $total_Time/$total_Time *100; print "Rotations (total) = $total_cycles\n"; print "Mean cadence (computed) = " . $total_cycles/$total_time_cycled * 60 . "\n"; $totalTimeTime = $lastTime - $firstTime; print "Total time (last - first) = $totalTimeTime s\n"; print "Registered (header) values: lap time: $edge_LapTime " . "cadence: $edge_LapCadence lap distance (m): $edge_LapDist\n"; ## reset values ## ## ## $grand_total_time_idle += $total_time_idle; $grand_total_time_cycled += $total_time_cycled ; $grand_total_cycles += $total_cycles ; $grand_total_Dist += $edge_LapDist; $total_time_idle = $total_time_cycled = $total_cycles = 0 ; $prev_Time = -1; $parsingTrack ='N'; $regCadence = 'N'; } } ##/while ## Grand Totals: $trackNo--; $grand_total_Time = $grand_total_time_idle + $grand_total_time_cycled; print "====== Totals/means for $trackNo tracks =====\n"; printf "Time = Idle: %d s Spinning: %d s Total: %d s\n", $grand_total_time_idle, $grand_total_time_cycled, $grand_total_Time; printf "Time = Idle: %.2f%% Spinning: %.2f%% Total: %.2f%%\n", $grand_total_time_idle/$grand_total_Time *100, $grand_total_time_cycled/$grand_total_Time * 100, $grand_total_Time/$grand_total_Time *100; print "Rotations (total) = $grand_total_cycles\n"; print "Mean cadence (computed) = " . $grand_total_cycles/$grand_total_time_cycled * 60 . "\n"; print "Total distance (registered): $grand_total_Dist (m)\n"; ## ### ### ### ### ### sub xmlEleValue { my $en = shift; # element name my $el = shift; # line $el =~ /<$en>(.*)<\/$en>/; return "$1"; }
Przykładowy wydruk dla pliku fit/tcx zawierającego trzy segmenty:
$ cadencecalc.pl 2017-08-31.tcx ### Parsing track #1... Time = Idle: 552 s Spinning: 2082 s Total: 2634 s Time = Idle: 20.96% Spinning: 79.04% Total: 100.00% Rotations (total) = 2684.95 Mean cadence (computed) = 77.3760806916427 Total time (last - first) = 2634 s Registered (header) values: lap time: 2438.06 cadence: 77 lap distance (m): 16339.2 ### Parsing track #2... Time = Idle: 80 s Spinning: 652 s Total: 732 s Time = Idle: 10.93% Spinning: 89.07% Total: 100.00% Rotations (total) = 860.716666666666 Mean cadence (computed) = 79.2070552147239 Total time (last - first) = 3366 s Registered (header) values: lap time: 731.05 cadence: 79 lap distance (m): 4994.82 ### Parsing track #3... Time = Idle: 29 s Spinning: 105 s Total: 134 s Time = Idle: 21.64% Spinning: 78.36% Total: 100.00% Rotations (total) = 120.716666666667 Mean cadence (computed) = 68.9809523809524 Total time (last - first) = 3500 s Registered (header) values: lap time: 134.378 cadence: 69 lap distance (m): 761.78 ====== Totals/means for 3 tracks ===== Time = Idle: 661 s Spinning: 2839 s Total: 3500 s Time = Idle: 18.89% Spinning: 81.11% Total: 100.00% Rotations (total) = 3666.38333333333 Mean cadence (computed) = 77.4860866502289 Total distance (registered): 22095.8 (m)
Podając jako minimalną prędkość 9 km/h otrzymamy:
$ cadencecalc.pl -s 9 2017-08-31.tcx *** Segments with speed below 9 kmh skipped *** ### Parsing track #1... Time = Idle: 654 s Spinning: 1980 s Total: 2634 s Time = Idle: 24.83% Spinning: 75.17% Total: 100.00% Rotations (total) = 2582.11666666667 Mean cadence (computed) = 78.2459595959596 Total time (last - first) = 2634 s Registered (header) values: lap time: 2438.06 cadence: 77 lap distance (m): 16339.2 ### Parsing track #2... Time = Idle: 80 s Spinning: 652 s Total: 732 s Time = Idle: 10.93% Spinning: 89.07% Total: 100.00% Rotations (total) = 860.716666666666 Mean cadence (computed) = 79.2070552147239 Total time (last - first) = 3366 s Registered (header) values: lap time: 731.05 cadence: 79 lap distance (m): 4994.82 ### Parsing track #3... Time = Idle: 29 s Spinning: 105 s Total: 134 s Time = Idle: 21.64% Spinning: 78.36% Total: 100.00% Rotations (total) = 120.716666666667 Mean cadence (computed) = 68.9809523809524 Total time (last - first) = 3500 s Registered (header) values: lap time: 134.378 cadence: 69 lap distance (m): 761.78 ====== Totals/means for 3 tracks ===== Time = Idle: 763 s Spinning: 2737 s Total: 3500 s Time = Idle: 21.80% Spinning: 78.20% Total: 100.00% Rotations (total) = 3563.55 Mean cadence (computed) = 78.1194738765071 Total distance (registered): 22095.8 (m)
Idle oznacza czas w którym nie kręcimy i/lub jedziemy poniżej prędkości minimalnej (podawany jest czas w sekundach i udział w całości). Rotations to liczba obrotów. Obliczone wartości wyglądają na prawidłowe na co wskazywałoby, że są bliskie wartościom liczonym przez Garmina.