Jak zostać Ironmanem? Analiza CSV-ek w pandas

running

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 „—„

ups

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.

pandas
wiek -1?
pandas

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 :-).

pandas
cięzko coś tu odczytać
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"])
pandas
o wiele lepiej

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()
matplotlib

Wnioski

I oto udało się! Jakie wnioski?
1. Człowiek im starszy tym wolniejszy (kto by pomyślał…)
2. Ale do 50 lat poziom jest wyrównany (psst, nie jest za poźno, aby zacząć trenować)
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 ?

2 myśli w temacie “Jak zostać Ironmanem? Analiza CSV-ek w pandas”

  1. 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!

    1. 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. 😉

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *