Mistrzostwa świata w Tri na dystansie Ironman za nami. Współzawodnictwo na Hawajach to marzenie każdego ambitnego triathlonisty. Z tej okazji wziąłem na warsztat wyniki zawodów triathlonowych na dystansie Ironman w latach 2005-2016 (436131 rekordów) znalezione na http://academictorrents.com. Do analizy wykorzystałem pythona, a wszczególności numpy, pandas oraz matplotlib.
Repo z plikiem jupyter i wykresami
dane
CSV-ki wyglądają mniej więcej tak:

Importy
import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib import datetime import os
Załadowanie danych
Zacznijmy od załadowania danych i dodania ich do pandas DataFrame. Zanim zacząłem mielić wszystkie pliki, zabrałem się za pojedynczą csv-ke.
files = [] for file in os.listdir("."): if file.endswith(".csv"): files.append(file)
Wersja z wszystkimi csv-kami w folderze.
files = [] for file in os.listdir("."): if file.endswith(".csv"): files.append(file) file = pd.read_csv(files[0]) for path in files[1:]: file = file.append(pd.read_csv(path))
Natomiast chcąc przetworzyć tylko dane z mistrzostw świata:
files = [] for file in os.listdir("."): if file.endswith(".csv") and "championships" in file: files.append(file) file = pd.read_csv(files[0]) for path in files[1:]: file = file.append(pd.read_csv(path))
Nie wszystkie kolumny są nam potrzebne
im = file[["division","age","country","swim","bike","run","overall"]]
Zmiana typów/Wybór kolumny
Tutaj zaczynają się schodki. Jak widać poniżej wszystkie kolumny są typu object. W tym przypadku interesuje nas string dla division, int dla age i czas dla swim, bike, run i overall

Próby zmiany typu kolumny age nie udadzą się bez poprzedniego oczyszczenia pól z wartości „—„

Początkowo chciałem zrobić wykres czasów w zależności od wieku. Jak widać poniżej, w kolumnie age jest dużo śmieci. Dlatego lepszym wyborem będzie division czyli grupa wiekowa.


Porządki
Zacznijmy od usunięcia wyników osób które zawodów nie ukończyły, zostały zdyskwalifikowane itp.
im = im[im.overall != "DNF"] im = im[im.overall != "DNS"] im = im[im.overall != "---"] im = im[im.overall != "DSQ"] im = im[im.overall != "DQ"] im_swim = im[im.swim != "---"][["division","swim"]] im_bike = im[im.bike != "---"][["division","bike"]] im_run = im[im.run != "---"][["division","run"]]
Celowo utworzyłem osobne DataFame dla pływania, roweru i biegu. Nie zawsze czas jednej z dyscyplin zostanie zarejestrowany (np. zepsuty/zgubiony chip).
Czasy konwertuje na timedelta, a następnie na int64.
im.overall = pd.to_timedelta(im.overall) im_swim.swim = pd.to_timedelta(im_swim.swim) im_bike.bike = pd.to_timedelta(im_bike.bike) im_run.run = pd.to_timedelta(im_run.run) im["overall_as_int"] = im.overall.astype(np.int64) im_swim["swim_as_int"] = im_swim.swim.astype(np.int64) im_bike["bike_as_int"] = im_bike.bike.astype(np.int64) im_run["run_as_int"] = im_run.run.astype(np.int64)
Analiza
Dane oczyszczone i skonwertowane, można przeprowadzić analizę. W tym przypadku nie będzie to jakiś rocket science. Interesuje nas średnia, mediana, odchylenie standardowe oraz min i max.
overall_stats = im[["division","overall_as_int"]].groupby("division").agg(['mean','median', 'std', 'min', 'max', ]) swim_stats = im_swim[["division","swim_as_int"]].groupby("division").agg(['mean','median', 'std', 'min', 'max', ]) bike_stats = im_bike[["division","bike_as_int"]].groupby("division").agg(['mean','median', 'std', 'min', 'max', ]) run_stats = im_run[["division","run_as_int"]].groupby("division").agg(['mean','median', 'std', 'min', 'max', ])
Otrzymaliśmy taką oto tabelkę. Pierwsza sprawa to format czasów które teraz dużo nam nie mówią. Druga to kategorie PRO, XC, PC i nknown których nie potrzebujemy. W szczególności PRO. I tak wiemy że daleko nam do nich :-).

overall_stats = overall_stats.apply(pd.to_timedelta) swim_stats = swim_stats.apply(pd.to_timedelta) bike_stats = bike_stats.apply(pd.to_timedelta) run_stats = run_stats.apply(pd.to_timedelta) overall_stats = overall_stats.drop(["nknown","XC","PRO","PC"]) swim_stats = swim_stats.drop(["nknown","XC","PRO","PC"]) bike_stats = bike_stats.drop(["nknown","XC","PRO","PC"]) run_stats = run_stats.drop(["nknown","XC","PRO","PC"])

Wizualizacja
Najlepsze pismo to pismo pierwotne, tj. obrazkowe. Użyłem do tego matplotlib. Problem z timedelta jest taki, że biblioteka nie za bardzo wie jak je wyświetlić w sposób sensowny dla człowieka. Stąd poniższa funkcja:
def timeTicks(x, pos): d = datetime.timedelta(seconds=x) return str(d) formatter = matplotlib.ticker.FuncFormatter(timeTicks)
Funkcja użyta jest jako formatter dla ax.yaxis. Dla średniej + odchylenia używam errorbar-a, natomiastmediana to przerywany plot.
fig = plt.figure(figsize=(16,8)) ax = fig.add_subplot(111) x = overall_stats.index.values y = overall_stats.overall_as_int["mean"] / np.timedelta64(1, 's') yerr = overall_stats.overall_as_int["std"] / np.timedelta64(1, 's') ax.errorbar(x, y, yerr, uplims=True, lolims=True, elinewidth =0.5, label="mean") ax.plot(overall_stats.overall_as_int["median"] / np.timedelta64(1, 's'),"--", label="median") ax.yaxis.set_major_formatter(formatter) handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels) plt.xlabel("Division") plt.ylabel("Time") plt.title("Ironman overall time") plt.yticks(np.arange(10, 18, step=0.5)*60*60) plt.grid(axis="y") plt.show()




Wnioski
I oto udało się! Jakie wnioski?
1. Człowiek im starszy tym wolniejszy (kto by pomyślał…)
2. Młodzi dobrze pływają
3. Ale wolniej kręcą i biegają
4. Najlepsze czasy mają 30-34
Czyli największe prawdopodobieństwo wycieczki na Hawaje to… trenować i dumnie się starzeć lub wysłać potomstwo na treningi do Triathlon Serwis 😎
jak na jednorazowy kod to nie ma sie do czego przyczepić (przykłady z artykułu) chociaż zmiene d, im, plt sa dalekie od conajmniej użytecznych kiedy wracasz do zagadnienia po całkiem nie dużym czasie.
Mając na uwadze zakres dat jaki podlegał analizie, ~10 lat, w mojej ocenie całkiem fajnym ćwiczeniem byłoby pokuszenie się o porównanie wyników za 2/3 lata celem oceny postawionych tez, trendów.
Głupio by był pisać „analizator” od nowa, przeciez stare csv’ki się juz nie zmienią.
Propozycja z mojej strony, pisząc kod uzywaj:
– metody narzędziowych, których nazwa mówi o tym co robi – ale logicznie a nie technicznie, to drugie to kod w jej ciele
– nazywaj zmienne tak, żebyś za 6 miesięcy wiedział co przechowuje tylko patrząc na jej nazwę zamiast wracać to analizy tego co wylicza jej wartość. Przypadkowy czytelnik artykułu tez będział łatwiej zrozumieć twój przekaz
– OOP (skoro juz piszesz o sobie .net developer ;))
pozdro!
Dzięki za feedback 🙂 Pisząc kod np. w vs code na bank podszedłbym do tematu w sposób OOP. W tym przypadku użyłem Juptera. Wrzucenie całej klasy z metodami do jednego paragrafu wydaje mi się nienaturalne przy prototypowaniu/zabawie w stylu adhoc. Temat dopiero zgłębiam więc może się mylę. Co do nazw zmiennych: fakt. Jak każdy wie… There are only two hard things in Computer Science: cache invalidation and naming things. 😉