Jakub’s hardloop data

Author

Jakub Sobotka

Code
!pip install garminconnect pandas
from garminconnect import Garmin
import getpass

email = input("Garmin email: ")
# getpass verbergt je wachtwoord terwijl je typt
password = getpass.getpass("Garmin wachtwoord: ") 

try:
    # Initialiseer de Garmin client
    client = Garmin(email, password)
    client.login()
    print("Succesvol ingelogd!")
    
    # Test of we data kunnen ophalen
    print(f"Welkom, {client.get_full_name()}")
    
except Exception as e:
    print(f"Oeps, inloggen mislukt: {e}")
Requirement already satisfied: garminconnect in /opt/anaconda3/lib/python3.13/site-packages (0.3.5)
Requirement already satisfied: pandas in /opt/anaconda3/lib/python3.13/site-packages (2.3.3)
Requirement already satisfied: curl_cffi>=0.6 in /opt/anaconda3/lib/python3.13/site-packages (from garminconnect) (0.15.0)
Requirement already satisfied: requests>=2.28 in /opt/anaconda3/lib/python3.13/site-packages (from garminconnect) (2.32.5)
Requirement already satisfied: ua-generator>=1.0 in /opt/anaconda3/lib/python3.13/site-packages (from garminconnect) (2.1.1)
Requirement already satisfied: numpy>=1.26.0 in /opt/anaconda3/lib/python3.13/site-packages (from pandas) (2.3.5)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/anaconda3/lib/python3.13/site-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/anaconda3/lib/python3.13/site-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /opt/anaconda3/lib/python3.13/site-packages (from pandas) (2025.2)
Requirement already satisfied: cffi>=2.0.0 in /opt/anaconda3/lib/python3.13/site-packages (from curl_cffi>=0.6->garminconnect) (2.0.0)
Requirement already satisfied: certifi>=2024.2.2 in /opt/anaconda3/lib/python3.13/site-packages (from curl_cffi>=0.6->garminconnect) (2026.5.20)
Requirement already satisfied: rich in /opt/anaconda3/lib/python3.13/site-packages (from curl_cffi>=0.6->garminconnect) (14.2.0)
Requirement already satisfied: pycparser in /opt/anaconda3/lib/python3.13/site-packages (from cffi>=2.0.0->curl_cffi>=0.6->garminconnect) (2.23)
Requirement already satisfied: six>=1.5 in /opt/anaconda3/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/anaconda3/lib/python3.13/site-packages (from requests>=2.28->garminconnect) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /opt/anaconda3/lib/python3.13/site-packages (from requests>=2.28->garminconnect) (3.11)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/anaconda3/lib/python3.13/site-packages (from requests>=2.28->garminconnect) (2.5.0)
Requirement already satisfied: markdown-it-py>=2.2.0 in /opt/anaconda3/lib/python3.13/site-packages (from rich->curl_cffi>=0.6->garminconnect) (2.2.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /opt/anaconda3/lib/python3.13/site-packages (from rich->curl_cffi>=0.6->garminconnect) (2.19.2)
Requirement already satisfied: mdurl~=0.1 in /opt/anaconda3/lib/python3.13/site-packages (from markdown-it-py>=2.2.0->rich->curl_cffi>=0.6->garminconnect) (0.1.2)
Garmin email:  jakub_8@live.nl
Garmin wachtwoord:  ········
mobile+cffi returned 429: Mobile login returned 429 — IP rate limited by Garmin
mobile+requests returned 429: Mobile login returned 429 — IP rate limited by Garmin
Succesvol ingelogd!
Welkom, Jakub
Code
import pandas as pd

activities = client.get_activities(0, 582)

df_raw = pd.DataFrame(activities)

relevante_kolommen = [
    'startTimeLocal', 
    'distance', 
    'duration', 
    'averageHR', 
    'maxHR' 
]

bestaande_kolommen = [col for col in relevante_kolommen if col in df_raw.columns]

df_raw[bestaande_kolommen].head()
startTimeLocal distance duration averageHR maxHR
0 2026-06-15 07:20:01 15125.469727 5042.044922 145.0 162.0
1 2026-06-14 10:30:11 39612.378906 5151.511230 165.0 210.0
2 2026-06-11 08:55:35 57560.710938 7411.664062 150.0 198.0
3 2026-06-10 06:52:57 11079.509766 3574.379883 144.0 163.0
4 2026-06-09 11:13:18 0.000000 3610.988037 137.0 182.0
Code
df_raw['afstand_km'] = df_raw['distance'] / 1000
df_raw['duur_min'] = df_raw['duration'] / 60
Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")

df_recent = df_raw[df_raw['datum'] >= start_van_het_jaar].copy()

df_week = df_recent.groupby(pd.Grouper(key='datum', freq='W-MON'))['afstand_km'].sum().reset_index()

# 4. De grafiek tekenen
plt.figure(figsize=(12, 6))
plt.bar(df_week['datum'], df_week['afstand_km'], width=5, color='#2c3e50', label='Afgelegde kilometers')

plt.title(f'Wekelijkse Kilometers ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Totale Afstand (km)', fontsize=12)
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.5)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")

toegestane_sporten = ['running', 'trail_running']

df_recent = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten))
].copy()

df_week = df_recent.groupby(pd.Grouper(key='datum', freq='W-MON'))['afstand_km'].sum().reset_index()

plt.figure(figsize=(12, 6))
plt.bar(df_week['datum'], df_week['afstand_km'], width=5, color='#2c3e50', label='Hardloop & Trail km')

plt.title(f'Wekelijkse Hardloop Kilometers ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Totale Afstand (km)', fontsize=12)
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.5)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000
if 'duur_min' not in df_raw.columns:
    df_raw['duur_min'] = df_raw['duration'] / 60

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_pace = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['afstand_km'] > 0)
].copy()


df_pace['tempo_decimaal'] = df_pace['duur_min'] / df_pace['afstand_km']

df_pace = df_pace.sort_values('datum')

df_pace['trendlijn'] = df_pace['tempo_decimaal'].rolling(window=5, min_periods=1).mean()

plt.figure(figsize=(12, 6))

plt.scatter(df_pace['datum'], df_pace['tempo_decimaal'], color='#3498db', alpha=0.6, label='Individuele Run')

plt.plot(df_pace['datum'], df_pace['trendlijn'], color='#e74c3c', linewidth=2, label='Trend (Rollend gem. 5 runs)')

plt.title(f'Tempo per Run ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Tempo (min/km, decimaal)', fontsize=12)

plt.gca().invert_yaxis()

plt.legend()
plt.grid(axis='both', linestyle='--', alpha=0.4)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime


if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))


huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_hartslag = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['averageHR'] > 0) 
].copy()


df_hartslag = df_hartslag.sort_values('datum')



df_hartslag['trendlijn'] = df_hartslag['averageHR'].rolling(window=5, min_periods=1).mean()


plt.figure(figsize=(12, 6))

plt.scatter(df_hartslag['datum'], df_hartslag['averageHR'], color='#9b59b6', alpha=0.5, label='Gem. Hartslag per Run')

plt.plot(df_hartslag['datum'], df_hartslag['trendlijn'], color='#8e44ad', linewidth=2.5, label='Trend (5 runs)')


plt.title(f'Gemiddelde Hartslag per Run ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Hartslag (bpm)', fontsize=12)
plt.legend()

plt.grid(axis='y', linestyle='--', alpha=0.6)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime


if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000
if 'duur_min' not in df_raw.columns:
    df_raw['duur_min'] = df_raw['duration'] / 60

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_combo = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['averageHR'] > 0) &
    (df_raw['afstand_km'] > 0)
].copy()

df_combo['tempo_decimaal'] = df_combo['duur_min'] / df_combo['afstand_km']
df_combo = df_combo.sort_values('datum')

df_combo['trend_hr'] = df_combo['averageHR'].rolling(window=5, min_periods=1).mean()
df_combo['trend_tempo'] = df_combo['tempo_decimaal'].rolling(window=5, min_periods=1).mean()

fig, ax1 = plt.subplots(figsize=(12, 6))
#laag 1
kleur_hr = '#e74c3c'  
ax1.scatter(df_combo['datum'], df_combo['averageHR'], color=kleur_hr, alpha=0.2)
ax1.plot(df_combo['datum'], df_combo['trend_hr'], color=kleur_hr, linewidth=3, label='Trend Hartslag (bpm)')
ax1.set_xlabel('Datum', fontsize=12)
ax1.set_ylabel('Hartslag (bpm)', color=kleur_hr, fontsize=12, fontweight='bold')
ax1.tick_params(axis='y', labelcolor=kleur_hr)

#laag 2
ax2 = ax1.twinx()  
kleur_tempo = '#3498db'  
ax2.scatter(df_combo['datum'], df_combo['tempo_decimaal'], color=kleur_tempo, alpha=0.2)
ax2.plot(df_combo['datum'], df_combo['trend_tempo'], color=kleur_tempo, linewidth=3, label='Trend Tempo (min/km)')
ax2.set_ylabel('Tempo (min/km)', color=kleur_tempo, fontsize=12, fontweight='bold')
ax2.tick_params(axis='y', labelcolor=kleur_tempo)

ax2.invert_yaxis()

plt.title(f'Hartslag vs Tempo ', fontsize=16, fontweight='bold')
ax1.grid(axis='x', linestyle='--', alpha=0.4)

fig.legend(loc='upper right', bbox_to_anchor=(0.9, 0.88))

plt.show()

Code
!pip install folium
Requirement already satisfied: folium in /opt/anaconda3/lib/python3.13/site-packages (0.20.0)
Requirement already satisfied: branca>=0.6.0 in /opt/anaconda3/lib/python3.13/site-packages (from folium) (0.8.2)
Requirement already satisfied: jinja2>=2.9 in /opt/anaconda3/lib/python3.13/site-packages (from folium) (3.1.6)
Requirement already satisfied: numpy in /opt/anaconda3/lib/python3.13/site-packages (from folium) (2.3.5)
Requirement already satisfied: requests in /opt/anaconda3/lib/python3.13/site-packages (from folium) (2.32.5)
Requirement already satisfied: xyzservices in /opt/anaconda3/lib/python3.13/site-packages (from folium) (2025.4.0)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/anaconda3/lib/python3.13/site-packages (from jinja2>=2.9->folium) (3.0.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/anaconda3/lib/python3.13/site-packages (from requests->folium) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /opt/anaconda3/lib/python3.13/site-packages (from requests->folium) (3.11)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/anaconda3/lib/python3.13/site-packages (from requests->folium) (2.5.0)
Requirement already satisfied: certifi>=2017.4.17 in /opt/anaconda3/lib/python3.13/site-packages (from requests->folium) (2026.5.20)
Code
import folium
import pandas as pd

run_data = df_recent.copy()
totaal_runs = len(run_data)


#Canvas opzetten
heatmap = folium.Map(location=[52.46, 4.62], zoom_start=11, tiles='CartoDB dark_matter')

#Loop door alle runs
succesvol = 0
overgeslagen = 0

for index, run in run_data.iterrows():
    activity_id = run['activityId']
    
    try:
        details = client.get_activity_details(activity_id)
        
        if details and details.get('geoPolylineDTO') and details['geoPolylineDTO'].get('polyline'):
            gps_punten = details['geoPolylineDTO']['polyline']
            
            route = [
                [punt['lat'], punt['lon']] 
                for punt in gps_punten 
                if punt.get('lat') is not None and punt.get('lon') is not None
            ]
            
            if route:
                folium.PolyLine(
                    locations=route,
                    color='#fc4c02',
                    weight=2,        
                    opacity=0.3      
                ).add_to(heatmap)
                succesvol += 1
        else:
            overgeslagen += 1
                
    except Exception as e:
        overgeslagen += 1
        
    if (succesvol + overgeslagen) % 15 == 0:
        print(f"Voortgang: {succesvol + overgeslagen} van de {totaal_runs} runs verwerkt...")

print(f"\nKlaar! {succesvol} routes getekend. {overgeslagen} runs genegeerd (geen GPS data of loopband).")

heatmap
Voortgang: 15 van de 93 runs verwerkt...
Voortgang: 30 van de 93 runs verwerkt...
Voortgang: 45 van de 93 runs verwerkt...
Voortgang: 60 van de 93 runs verwerkt...
Voortgang: 75 van de 93 runs verwerkt...
Voortgang: 90 van de 93 runs verwerkt...

Klaar! 93 routes getekend. 0 runs genegeerd (geen GPS data of loopband).
Make this Notebook Trusted to load map: File -> Trust Notebook
Code
 heatmap.save('2026 runs.html')
Code
import folium
import pandas as pd
from IPython.display import display

run_data = df_recent.copy()
totaal_runs = len(run_data)

print(f"🚀 Start proces... Er staan {totaal_runs} runs in de wachtrij.")

if totaal_runs == 0:
    print("⚠️ Let op: df_recent is leeg. Ga even terug naar de cel waar je df_recent aanmaakt en draai die opnieuw.")
else:
    heatmap = folium.Map(location=[52.46, 4.62], zoom_start=11, tiles='CartoDB dark_matter')

    succesvol = 0
    overgeslagen = 0

    for index, run in run_data.iterrows():
        activity_id = run['activityId']
        
        try:
            details = client.get_activity_details(activity_id)
            
            if details and details.get('geoPolylineDTO') and details['geoPolylineDTO'].get('polyline'):
                gps_punten = details['geoPolylineDTO']['polyline']
                
                route = [
                    [punt['lat'], punt['lon']] 
                    for punt in gps_punten 
                    if punt.get('lat') is not None and punt.get('lon') is not None
                ]
                
                if route:
                    folium.PolyLine(
                        locations=route,
                        color='#fc4c02',
                        weight=2,        
                        opacity=0.3      
                    ).add_to(heatmap)
                    succesvol += 1
            else:
                overgeslagen += 1
                
        except Exception as e:
            # Als Garmin de verbinding weigert
            print(f"Fout bij ophalen run {activity_id}: {e}")
            overgeslagen += 1
            
        # Geef 5 runs een update in plaats van 15
        if (succesvol + overgeslagen) % 5 == 0:
            print(f"Voortgang: {succesvol + overgeslagen} van de {totaal_runs} runs verwerkt...")

    print(f"\n✅ Klaar! {succesvol} routes getekend. {overgeslagen} runs genegeerd.")

    heatmap.save("mijn_persoonlijke_heatmap.html")
    print("Kaart is opgeslagen als 'mijn_persoonlijke_heatmap.html'. Je kunt deze openen in je browser.")

    display(heatmap)
🚀 Start proces... Er staan 93 runs in de wachtrij.
Voortgang: 5 van de 93 runs verwerkt...
Voortgang: 10 van de 93 runs verwerkt...
Voortgang: 15 van de 93 runs verwerkt...
Voortgang: 20 van de 93 runs verwerkt...
Voortgang: 25 van de 93 runs verwerkt...
Voortgang: 30 van de 93 runs verwerkt...
Voortgang: 35 van de 93 runs verwerkt...
Voortgang: 40 van de 93 runs verwerkt...
Voortgang: 45 van de 93 runs verwerkt...
Voortgang: 50 van de 93 runs verwerkt...
Voortgang: 55 van de 93 runs verwerkt...
Voortgang: 60 van de 93 runs verwerkt...
Voortgang: 65 van de 93 runs verwerkt...
Voortgang: 70 van de 93 runs verwerkt...
Voortgang: 75 van de 93 runs verwerkt...
Voortgang: 80 van de 93 runs verwerkt...
Voortgang: 85 van de 93 runs verwerkt...
Voortgang: 90 van de 93 runs verwerkt...

✅ Klaar! 93 routes getekend. 0 runs genegeerd.
Kaart is opgeslagen als 'mijn_persoonlijke_heatmap.html'. Je kunt deze openen in je browser.
Make this Notebook Trusted to load map: File -> Trust Notebook
Code
heatmap.save('mijn ultra heatmap.html'
Code
!pip install scikit-learn plotly
Requirement already satisfied: scikit-learn in /opt/anaconda3/lib/python3.13/site-packages (1.7.2)
Requirement already satisfied: plotly in /opt/anaconda3/lib/python3.13/site-packages (6.3.0)
Requirement already satisfied: numpy>=1.22.0 in /opt/anaconda3/lib/python3.13/site-packages (from scikit-learn) (2.3.5)
Requirement already satisfied: scipy>=1.8.0 in /opt/anaconda3/lib/python3.13/site-packages (from scikit-learn) (1.16.3)
Requirement already satisfied: joblib>=1.2.0 in /opt/anaconda3/lib/python3.13/site-packages (from scikit-learn) (1.5.2)
Requirement already satisfied: threadpoolctl>=3.1.0 in /opt/anaconda3/lib/python3.13/site-packages (from scikit-learn) (3.5.0)
Requirement already satisfied: narwhals>=1.15.1 in /opt/anaconda3/lib/python3.13/site-packages (from plotly) (2.7.0)
Requirement already satisfied: packaging in /opt/anaconda3/lib/python3.13/site-packages (from plotly) (25.0)
Code
import pandas as pd
import plotly.express as px
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler


# We hebben runs nodig met complete data. Gooi rijen met missende waarden weg.
df_ml = df_combo.dropna(subset=['afstand_km', 'tempo_decimaal', 'averageHR']).copy()

# Dit zijn onze drie dimensies (features)
features = ['afstand_km', 'tempo_decimaal', 'averageHR']
X = df_ml[features]

# 2. DE WISKUNDE: STANDAARDISEREN
scaler = StandardScaler()
X_geschaald = scaler.fit_transform(X)

# 3. HET MACHINE LEARNING MODEL
# Verander dit getal om te kijken hoe het algoritme je data opknipt
aantal_clusters = 3

# random_state zorgt dat de wiskunde telkens dezelfde uitkomst geeft als je hem opnieuw runt
kmeans = KMeans(n_clusters=aantal_clusters, random_state=42, n_init=10)
df_ml['Cluster'] = kmeans.fit_predict(X_geschaald)

# Zet de clusters om naar tekst voor de legenda, anders denkt Plotly dat het een continue schaal is
df_ml['Cluster'] = 'Cluster ' + df_ml['Cluster'].astype(str)

# 4. DE 3D VISUALISATIE (PLOTLY)
fig = px.scatter_3d(
    df_ml, 
    x='afstand_km', 
    y='tempo_decimaal', 
    z='averageHR',
    color='Cluster',
    title=f'K-Means Clustering ({aantal_clusters} Trainingsprofielen)',
    labels={
        'afstand_km': 'Afstand (km)',
        'tempo_decimaal': 'Tempo (min/km)',
        'averageHR': 'Gem. Hartslag (bpm)'
    },
    opacity=0.8
)

fig.update_layout(scene=dict(yaxis=dict(autorange='reversed')))

cluster_profielen = df_ml.groupby('Cluster')[['afstand_km', 'tempo_decimaal', 'averageHR']].mean()

cluster_profielen = cluster_profielen.round(2)

cluster_profielen = cluster_profielen.sort_values('afstand_km')

print(cluster_profielen)


# Toon de interactieve 3D grafiek!
fig.show()
           afstand_km  tempo_decimaal  averageHR
Cluster                                         
Cluster 0        9.93            5.65     140.76
Cluster 2       11.33            4.68     168.80
Cluster 1       24.40            5.69     149.14

Code
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df_ml = df_ml.sort_values('datum')


df_ml['Volgende_Training'] = df_ml['Cluster'].shift(-1)


df_markov = df_ml.dropna(subset=['Volgende_Training'])


transitie_matrix = pd.crosstab(
    df_markov['Cluster'], 
    df_markov['Volgende_Training'], 
    normalize='index'
)

transitie_matrix = (transitie_matrix * 100).round(1)

plt.figure(figsize=(10, 6))
sns.heatmap(transitie_matrix, annot=True, fmt='g', cmap='Blues', cbar_kws={'label': 'Kans (%)'})

plt.title('Wat is de kans op de volgende training?', fontsize=14, fontweight='bold')
plt.xlabel('Voorspelde VOLGENDE Training', fontsize=12)
plt.ylabel('Jouw HUIDIGE Training', fontsize=12)

plt.show()

laatste_run = df_ml.iloc[-1]['Cluster']
voorspelling = transitie_matrix.loc[laatste_run].idxmax()
kans = transitie_matrix.loc[laatste_run].max()

print("\n--- JOUW AI VOORSPELLING ---")
print(f"Je laatste run was een: '{laatste_run}'.")
print(f"Op basis van jouw eigen data is de kans het grootst ({kans}%) dat je volgende run een '{voorspelling}' wordt.")


--- JOUW AI VOORSPELLING ---
Je laatste run was een: 'Cluster 0'.
Op basis van jouw eigen data is de kans het grootst (47.9%) dat je volgende run een 'Cluster 0' wordt.
Code
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

df_afstand = df_combo.copy()
df_afstand = df_afstand.sort_values('datum')


# np.inf betekent 'oneindig', dus alles boven de 30 km valt in het laatste bakje
bins = [0, 5, 10, 15, 20, 25, 30, np.inf]

# Geef de bakjes een leesbare naam
labels = ['0-5 km', '5-10 km', '10-15 km', '15-20 km', '20-25 km', '25-30 km', '30+ km']

# Gebruik pd.cut() om elke run in het juiste bakje te gooien
df_afstand['Afstand_Categorie'] = pd.cut(df_afstand['afstand_km'], bins=bins, labels=labels)

# 3. De Markov-logica: Verschuif de categorie om de 'volgende' afstand te bepalen
df_afstand['Volgende_Afstand'] = df_afstand['Afstand_Categorie'].shift(-1)

# Verwijder de allerlaatste run (want de toekomst weten we nog niet)
df_afstand = df_afstand.dropna(subset=['Volgende_Afstand'])

# 4. Bereken de Transitie Matrix (Kansberekening)
transitie_matrix_km = pd.crosstab(
    df_afstand['Afstand_Categorie'], 
    df_afstand['Volgende_Afstand'], 
    normalize='index' # Zet aantallen om naar procentuele kansen per rij
)

# Zet om naar percentages en rond af
transitie_matrix_km = (transitie_matrix_km * 100).round(1)

# 5. Visualiseren als Heatmap
plt.figure(figsize=(12, 8))
# cmap='YlGnBu' geeft een mooie weergave van geel (lage kans) naar donkerblauw (hoge kans)
sns.heatmap(transitie_matrix_km, annot=True, fmt='g', cmap='YlGnBu', cbar_kws={'label': 'Kans (%)'})

plt.title('Wat is de kans op afstand X na Y?', fontsize=16, fontweight='bold')
plt.xlabel('Voorspelde VOLGENDE Afstand', fontsize=12)
plt.ylabel('Jouw HUIDIGE Afstand', fontsize=12)

# Zorg dat de tekst op de assen netjes leesbaar is
plt.xticks(rotation=45)
plt.yticks(rotation=0)

plt.tight_layout()
plt.show()

# --- Print een voorbeeld voorspelling ---
laatste_afstand_km = df_afstand.iloc[-1]['afstand_km']
laatste_bakje = df_afstand.iloc[-1]['Afstand_Categorie']
voorspeld_bakje = transitie_matrix_km.loc[laatste_bakje].idxmax()
kans = transitie_matrix_km.loc[laatste_bakje].max()

print("\n--- KILOMETER VOORSPELLING ---")
print(f"Je laatste run was exact {laatste_afstand_km:.1f} km (Categorie: '{laatste_bakje}').")
print(f"Historisch gezien is de kans het grootst ({kans}%) dat je volgende run tussen de '{voorspeld_bakje}' wordt.")


--- KILOMETER VOORSPELLING ---
Je laatste run was exact 11.1 km (Categorie: '10-15 km').
Historisch gezien is de kans het grootst (32.3%) dat je volgende run tussen de '10-15 km' wordt.
Code
!pip install torch
Collecting torch

  Downloading torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl.metadata (31 kB)

Requirement already satisfied: filelock in /opt/anaconda3/lib/python3.13/site-packages (from torch) (3.20.0)

Requirement already satisfied: typing-extensions>=4.10.0 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (4.15.0)

Requirement already satisfied: setuptools<82 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (80.9.0)

Requirement already satisfied: sympy>=1.13.3 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (1.14.0)

Requirement already satisfied: networkx>=2.5.1 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (3.5)

Requirement already satisfied: jinja2 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (3.1.6)

Requirement already satisfied: fsspec>=0.8.5 in /opt/anaconda3/lib/python3.13/site-packages (from torch) (2025.10.0)

Requirement already satisfied: mpmath<1.4,>=1.1.0 in /opt/anaconda3/lib/python3.13/site-packages (from sympy>=1.13.3->torch) (1.3.0)

Requirement already satisfied: MarkupSafe>=2.0 in /opt/anaconda3/lib/python3.13/site-packages (from jinja2->torch) (3.0.2)

Downloading torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl (88.0 MB)

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 9.7 MB/s  0:00:09 eta 0:00:010:01:03

Installing collected packages: torch

Successfully installed torch-2.12.0
Code
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
import numpy as np

# We pakken de tabel die we eerder hebben schoongemaakt
df_nn = df_combo.dropna(subset=['afstand_km', 'duur_min', 'averageHR']).copy()

# We voegen je gewicht toe als feature voor de energie-berekening
df_nn['gewicht_kg'] = 61.0 

# X_train (De Input Matrix): Afstand, Hartslag en Gewicht
X_train = df_nn[['afstand_km', 'averageHR', 'gewicht_kg']].values

# y_train (De Output Vector): Daadwerkelijke tijd in minuten
y_train = df_nn['duur_min'].values

# Normaliseer de input (extreem belangrijk voor Neurale Netwerken)
scaler_X = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)

# Zet alles om naar wiskundige PyTorch Tensors
X_tensor = torch.tensor(X_train_scaled, dtype=torch.float32)
y_tensor = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)


# --- 2. DE MOTOR: HET NEURALE NETWERK ---
class FinishtijdVoorspeller(nn.Module):
    def __init__(self, input_dim):
        super(FinishtijdVoorspeller, self).__init__()
        self.layer1 = nn.Linear(input_dim, 16)
        self.act1 = nn.ReLU()
        
        self.layer2 = nn.Linear(16, 8)
        self.act2 = nn.ReLU()
        
        self.output_layer = nn.Linear(8, 1) # 1 output: de tijd in minuten
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.act2(self.layer2(x))
        x = self.output_layer(x)
        return x

# Er zijn 3 input variabelen (afstand, hartslag, gewicht)
model = FinishtijdVoorspeller(input_dim=3)


# --- 3. TRAINING: BACKPROPAGATION ---
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.05)

print("Start met het trainen van het Neurale Netwerk...\n")

# We trainen het model in 500 stapjes (epochs)
for epoch in range(500):
    model.train()
    
    # Forward pass
    voorspellingen = model(X_tensor)
    loss = criterion(voorspellingen, y_tensor)
    
    # Backward pass (Gradiënten updaten)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
Start met het trainen van het Neurale Netwerk...
Code
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np


print("Data aan het voorbereiden...")

df_nn = df_combo.copy()
df_nn = df_nn.sort_values('datum')

# Feature 2: Hoogtemeters veilig uithalen (als het veld ontbreekt, vullen we 0 in)
if 'elevationGain' in df_nn.columns:
    df_nn['hoogtemeters'] = df_nn['elevationGain'].fillna(0)
else:
    df_nn['hoogtemeters'] = 0.0

# Feature 3: Het 28-dagen volume (Hoeveel km zat er al in de benen?)
# We zetten de datum tijdelijk als de index zodat we wiskundig door de tijd kunnen rollen
df_nn = df_nn.set_index('datum')
# De .shift(1) is cruciaal! We willen niet de afstand van de race zélf meetellen in de opbouw
df_nn['volume_28d'] = df_nn['afstand_km'].rolling('28D').sum().shift(1).fillna(0)
df_nn = df_nn.reset_index()

# Feature 4: Aerobe efficiëntie (Snelheid gedeeld door inspanning)
df_nn['snelheid_kmu'] = 60 / df_nn['tempo_decimaal']
df_nn['efficientie'] = df_nn['snelheid_kmu'] / df_nn['averageHR']

# Feature 5: Fysiologische massa
df_nn['gewicht'] = 61.0

# Verwijder eventuele lege rijen
features = ['afstand_km', 'hoogtemeters', 'volume_28d', 'efficientie', 'gewicht']
df_nn = df_nn.dropna(subset=features + ['duur_min'])

# Bouw de finale X en y matrices
X_train = df_nn[features].values
y_train = df_nn['duur_min'].values

# Normaliseren (Cruciaal voor Neurale Netwerken)
scaler_X = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)

# Zet om naar PyTorch Tensors
X_tensor = torch.tensor(X_train_scaled, dtype=torch.float32)
y_tensor = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)


# ==========================================
# DEEL 2: HET NEURALE NETWERK BOUWEN
# ==========================================
class FinishtijdVoorspeller(nn.Module):
    def __init__(self, input_dim):
        super(FinishtijdVoorspeller, self).__init__()
        self.layer1 = nn.Linear(input_dim, 16)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(16, 8)
        self.act2 = nn.ReLU()
        self.output_layer = nn.Linear(8, 1)
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.act2(self.layer2(x))
        x = self.output_layer(x)
        return x

model = FinishtijdVoorspeller(input_dim=5)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.05)


# ==========================================
# DEEL 3: TRAINEN (Backpropagation)
# ==========================================
print("Netwerk architectuur staat klaar. Start training van 500 epochs...\n")

for epoch in range(500):
    model.train()
    optimizer.zero_grad()
    
    voorspellingen = model(X_tensor)
    loss = criterion(voorspellingen, y_tensor)
    
    loss.backward()
    optimizer.step()
    
    if (epoch + 1) % 100 == 0:
        print(f"Epoch {epoch+1}/500 - Loss (Foutmarge): {loss.item():.2f}")


# ==========================================
# DEEL 4: JOUW EERSTE AI-VOORSPELLING
# ==========================================
# Laten we testen met een standaard lange duurloop uit je eigen data
print("\n--- AI TEST ---")
test_afstand = 172.0    # 30 km
test_hoogtemeters = 5200 # 50 m stijgen
test_volume = 312      # 200 km getraind in de maand ervoor
test_eff = df_nn['efficientie'].mean() # Gemiddelde efficiëntie
test_gewicht = 61.0

# Maak de vector, schaal hem, en voorspel
nieuwe_race = np.array([[test_afstand, test_hoogtemeters, test_volume, test_eff, test_gewicht]])
nieuwe_race_scaled = scaler_X.transform(nieuwe_race)
nieuwe_race_tensor = torch.tensor(nieuwe_race_scaled, dtype=torch.float32)

model.eval()
with torch.no_grad():
    voorspelde_minuten = model(nieuwe_race_tensor).item()

uur = int(voorspelde_minuten // 60)
minuten = int(voorspelde_minuten % 60)
print(f"Voor duurloop van {test_afstand}km schat de AI jouw tijd op: {uur} uur en {minuten:02d} minuten.")
Data aan het voorbereiden...
Netwerk architectuur staat klaar. Start training van 500 epochs...

Epoch 100/500 - Loss (Foutmarge): 32.42
Epoch 200/500 - Loss (Foutmarge): 22.60
Epoch 300/500 - Loss (Foutmarge): 18.61
Epoch 400/500 - Loss (Foutmarge): 16.06
Epoch 500/500 - Loss (Foutmarge): 15.21

--- AI TEST ---
Voor duurloop van 172.0km schat de AI jouw tijd op: 21 uur en 22 minuten.
Code
import matplotlib.pyplot as plt

# ==========================================
# 1. DE TRAINING DRAAIEN (MET VEILIGE LEARNING RATE)
# ==========================================
model = FinishtijdVoorspeller(input_dim=5)

# FIX 1: Learning rate verlaagd naar 0.005 om wiskundige 'explosies' (NaN) te voorkomen
optimizer = optim.Adam(model.parameters(), lr=0.005)

loss_geschiedenis = []

print("Netwerk veilig aan het trainen voor de grafiek...")
for epoch in range(500):
    model.train()
    optimizer.zero_grad()
    
    voorspellingen = model(X_tensor)
    loss = criterion(voorspellingen, y_tensor)
    
    loss.backward()
    optimizer.step()
    
    loss_geschiedenis.append(loss.item())

# ==========================================
# 2. VOORSPELLINGEN OPHALEN
# ==========================================
model.eval()
with torch.no_grad():
    # FIX 2: .flatten() stript de overtollige matrix-haakjes weg
    alle_ai_voorspellingen = model(X_tensor).numpy().flatten()


# ==========================================
# 3. GRAFIEKEN TEKENEN
# ==========================================
plt.figure(figsize=(12, 5))

# Plot 1: Leercurve
plt.subplot(1, 2, 1)
plt.plot(loss_geschiedenis, color='#2ecc71', linewidth=2)
plt.title('De Leercurve van jouw AI', fontsize=14, fontweight='bold')
plt.xlabel('Trainingsstappen (Epochs)', fontsize=10)
plt.ylabel('Foutmarge (MSE)', fontsize=10)
plt.grid(axis='y', linestyle='--', alpha=0.6)

# Plot 2: Actueel vs Voorspeld
plt.subplot(1, 2, 2)

# De perfecte rode lijn
plt.plot([min(y_train), max(y_train)], [min(y_train), max(y_train)], color='#e74c3c', linestyle='--', linewidth=2, label='100% Accuraat')

# De blauwe wolk met jouw runs
plt.scatter(y_train, alle_ai_voorspellingen, color='#3498db', alpha=0.5, label='Jouw Runs')

plt.title('AI Test: Echte Tijd vs. Voorspelling', fontsize=14, fontweight='bold')
plt.xlabel('Echte Tijd in minuten (Garmin)', fontsize=10)
plt.ylabel('Voorspelde Tijd in min. (AI)', fontsize=10)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.4)

plt.tight_layout()
plt.show()
Netwerk veilig aan het trainen voor de grafiek...

Code
import pandas as pd
import matplotlib.pyplot as plt

# --- 1. JOUW FYSIOLOGISCHE CONSTANTEN ---
gewicht_kg = 61.0
leeftijd = 25 # Pas dit aan naar je werkelijke leeftijd
max_glycogeen_tank = 400 # Gram koolhydraten in jouw spieren/lever

# --- 2. DE RACE PARAMETERS (Voorbeeld: Pierenwaai 60km) ---
geschatte_tijd_uren = 30.0
gemiddelde_hartslag = 135 # Jouw geschatte aerobe race-hartslag
koolhydraten_per_uur = 60 # Gram (bijv. 2 gels van 30g per uur)

# --- 3. DE WISKUNDIGE MOTOR ---
def bereken_koolhydraat_verbruik(hr, gewicht, leeftijd):
    # Formule voor Kcal per minuut
    kcal_per_min = (-55.0969 + (0.6309 * hr) + (0.1988 * gewicht) + (0.2017 * leeftijd)) / 4.184
    
    # Hoeveel procent van deze Kcal komt uit koolhydraten? (Niet-lineaire curve)
    # Bij rust is dit laag (vetverbranding), bij hoge HR schiet dit naar 100%
    if hr < 130: perc_koolhydraten = 0.30
    elif hr < 145: perc_koolhydraten = 0.50
    elif hr < 160: perc_koolhydraten = 0.75
    else: perc_koolhydraten = 0.95
        
    kcal_koolhydraten_per_min = kcal_per_min * perc_koolhydraten
    # 1 gram koolhydraten = 4 kcal
    gram_koolhydraten_per_min = kcal_koolhydraten_per_min / 4
    
    return gram_koolhydraten_per_min

# --- 4. DE ITERATIEVE SIMULATIE ---
tijdstippen = list(range(0, int(geschatte_tijd_uren * 60) + 1, 15)) # Stappen van 15 minuten
tank_inhoud = []
huidige_tank = max_glycogeen_tank

verbruik_per_minuut = bereken_koolhydraat_verbruik(gemiddelde_hartslag, gewicht_kg, leeftijd)
inname_per_15min = koolhydraten_per_uur / 4

print("--- JOUW RACE FUEL PLANNER ---")
print(f"Geschat verbruik: {verbruik_per_minuut * 60:.1f} gram koolhydraten per uur.")
print("Eet-momenten en Tank Status:")

for minuut in tijdstippen:
    tank_inhoud.append(huidige_tank)
    
    if minuut > 0 and minuut % 30 == 0:
        print(f"[{minuut//60:02d}:{minuut%60:02d}] ACTIE: Eet/Drink! (Huidige tank: {huidige_tank:.0f}g)")
        
    # Werk de tank bij voor de volgende 15 minuten
    huidige_tank -= (verbruik_per_minuut * 15)
    huidige_tank += inname_per_15min
    
    # Je kunt geen negatieve energie hebben
    if huidige_tank <= 0:
        print(f"\nWAARSCHUWING: De tank is LEEG op minuut {minuut}! Verhoog je inname of verlaag je hartslag.")
        huidige_tank = 0

# --- 5. VISUALISATIE ---
plt.figure(figsize=(10, 5))
plt.plot(tijdstippen, tank_inhoud, color='#e67e22', linewidth=3)
plt.axhline(y=50, color='#e74c3c', linestyle='--', label='Gevarenzone (< 50g)')
plt.title(f'Koolhydraat Voorraad over {geschatte_tijd_uren} uur', fontsize=14)
plt.xlabel('Tijd in race (minuten)')
plt.ylabel('Gram Koolhydraten in Lichaam')
plt.fill_between(tijdstippen, 0, 50, color='#e74c3c', alpha=0.2)
plt.grid(True, linestyle=':', alpha=0.6)
plt.legend()
plt.show()
--- JOUW RACE FUEL PLANNER ---
Geschat verbruik: 84.7 gram koolhydraten per uur.
Eet-momenten en Tank Status:
[00:30] ACTIE: Eet/Drink! (Huidige tank: 388g)
[01:00] ACTIE: Eet/Drink! (Huidige tank: 375g)
[01:30] ACTIE: Eet/Drink! (Huidige tank: 363g)
[02:00] ACTIE: Eet/Drink! (Huidige tank: 351g)
[02:30] ACTIE: Eet/Drink! (Huidige tank: 338g)
[03:00] ACTIE: Eet/Drink! (Huidige tank: 326g)
[03:30] ACTIE: Eet/Drink! (Huidige tank: 314g)
[04:00] ACTIE: Eet/Drink! (Huidige tank: 301g)
[04:30] ACTIE: Eet/Drink! (Huidige tank: 289g)
[05:00] ACTIE: Eet/Drink! (Huidige tank: 277g)
[05:30] ACTIE: Eet/Drink! (Huidige tank: 264g)
[06:00] ACTIE: Eet/Drink! (Huidige tank: 252g)
[06:30] ACTIE: Eet/Drink! (Huidige tank: 240g)
[07:00] ACTIE: Eet/Drink! (Huidige tank: 227g)
[07:30] ACTIE: Eet/Drink! (Huidige tank: 215g)
[08:00] ACTIE: Eet/Drink! (Huidige tank: 203g)
[08:30] ACTIE: Eet/Drink! (Huidige tank: 190g)
[09:00] ACTIE: Eet/Drink! (Huidige tank: 178g)
[09:30] ACTIE: Eet/Drink! (Huidige tank: 165g)
[10:00] ACTIE: Eet/Drink! (Huidige tank: 153g)
[10:30] ACTIE: Eet/Drink! (Huidige tank: 141g)
[11:00] ACTIE: Eet/Drink! (Huidige tank: 128g)
[11:30] ACTIE: Eet/Drink! (Huidige tank: 116g)
[12:00] ACTIE: Eet/Drink! (Huidige tank: 104g)
[12:30] ACTIE: Eet/Drink! (Huidige tank: 91g)
[13:00] ACTIE: Eet/Drink! (Huidige tank: 79g)
[13:30] ACTIE: Eet/Drink! (Huidige tank: 67g)
[14:00] ACTIE: Eet/Drink! (Huidige tank: 54g)
[14:30] ACTIE: Eet/Drink! (Huidige tank: 42g)
[15:00] ACTIE: Eet/Drink! (Huidige tank: 30g)
[15:30] ACTIE: Eet/Drink! (Huidige tank: 17g)
[16:00] ACTIE: Eet/Drink! (Huidige tank: 5g)

WAARSCHUWING: De tank is LEEG op minuut 960! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 975! Verhoog je inname of verlaag je hartslag.
[16:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 990! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1005! Verhoog je inname of verlaag je hartslag.
[17:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1020! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1035! Verhoog je inname of verlaag je hartslag.
[17:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1050! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1065! Verhoog je inname of verlaag je hartslag.
[18:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1080! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1095! Verhoog je inname of verlaag je hartslag.
[18:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1110! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1125! Verhoog je inname of verlaag je hartslag.
[19:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1140! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1155! Verhoog je inname of verlaag je hartslag.
[19:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1170! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1185! Verhoog je inname of verlaag je hartslag.
[20:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1200! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1215! Verhoog je inname of verlaag je hartslag.
[20:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1230! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1245! Verhoog je inname of verlaag je hartslag.
[21:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1260! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1275! Verhoog je inname of verlaag je hartslag.
[21:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1290! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1305! Verhoog je inname of verlaag je hartslag.
[22:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1320! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1335! Verhoog je inname of verlaag je hartslag.
[22:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1350! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1365! Verhoog je inname of verlaag je hartslag.
[23:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1380! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1395! Verhoog je inname of verlaag je hartslag.
[23:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1410! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1425! Verhoog je inname of verlaag je hartslag.
[24:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1440! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1455! Verhoog je inname of verlaag je hartslag.
[24:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1470! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1485! Verhoog je inname of verlaag je hartslag.
[25:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1500! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1515! Verhoog je inname of verlaag je hartslag.
[25:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1530! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1545! Verhoog je inname of verlaag je hartslag.
[26:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1560! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1575! Verhoog je inname of verlaag je hartslag.
[26:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1590! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1605! Verhoog je inname of verlaag je hartslag.
[27:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1620! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1635! Verhoog je inname of verlaag je hartslag.
[27:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1650! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1665! Verhoog je inname of verlaag je hartslag.
[28:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1680! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1695! Verhoog je inname of verlaag je hartslag.
[28:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1710! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1725! Verhoog je inname of verlaag je hartslag.
[29:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1740! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1755! Verhoog je inname of verlaag je hartslag.
[29:30] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1770! Verhoog je inname of verlaag je hartslag.

WAARSCHUWING: De tank is LEEG op minuut 1785! Verhoog je inname of verlaag je hartslag.
[30:00] ACTIE: Eet/Drink! (Huidige tank: 0g)

WAARSCHUWING: De tank is LEEG op minuut 1800! Verhoog je inname of verlaag je hartslag.

Code
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 1. HET DOMEIN (De X-as)
# We genereren 1000 mogelijke finishtijden tussen de 4 en 9 uur
tijden = np.linspace(4, 9, 1000) 

# 2. DE PRIOR 
verwachte_tijd = 6.0  # Je mikt op 6 uur
onzekerheid_vooraf = 0.6 # Je voelt je redelijk zeker (kleine spreiding)
prior = norm.pdf(tijden, verwachte_tijd, onzekerheid_vooraf)

# 3. DE LIKELIHOOD 
actuele_pace_voorspelling = 5.8 
onzekerheid_data = 0.3 
likelihood = norm.pdf(tijden, actuele_pace_voorspelling, onzekerheid_data)

# 4. DE POSTERIOR 
posterior_ongeschaald = prior * likelihood

#oppervlakte eronder exact 1 is
posterior = posterior_ongeschaald / np.trapezoid(posterior_ongeschaald, tijden)

plt.figure(figsize=(10, 6))

plt.plot(tijden, prior, label='Prior (Theorie Vooraf)', color='#3498db', linestyle='--')
plt.plot(tijden, likelihood, label='Likelihood (Garmin Data)', color='#e67e22', linestyle=':')
plt.plot(tijden, posterior, label='Posterior (Geüpdatete Kans)', color='#2ecc71', linewidth=3)

meest_waarschijnlijke_tijd = tijden[np.argmax(posterior)]
plt.axvline(x=meest_waarschijnlijke_tijd, color='#2ecc71', alpha=0.5)

plt.title('Bayesiaanse Vorm-Tracker', fontsize=16, fontweight='bold')
plt.xlabel('Mogelijke Finishtijd (Uren)', fontsize=12)
plt.ylabel('Waarschijnlijkheid (Kansdichtheid)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

uur = int(meest_waarschijnlijke_tijd)
minuten = int((meest_waarschijnlijke_tijd - uur) * 60)
print(f"\nWiskundige Conclusie: Op basis van je training én je huidige tempo, is de meest waarschijnlijke finishtijd nu {uur} uur en {minuten:02d} minuten.")


Wiskundige Conclusie: Op basis van je training én je huidige tempo, is de meest waarschijnlijke finishtijd nu 5 uur en 50 minuten.