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. 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 *