Blog

HRP Teil 5: Die letzten Berge vor dem Mittelmeer

Zwischen L’Hospitalet-pres-I’Andorre und Banyuls-sur-Mer führt der HRP über Pic Carlit und Canigou und dann über zahlreiche Hügel zum Mittelmeer

Pic Carlit von Estany de les Dugues

Auf dem letzten Abschnitt der Haute randonnée pyrénéenne (HPR) gibt es nur noch wenige richtige Berge. Der Weg wird immer einfacher, was durch die zunehmende Hitze ausgeglichen wird.

L’Hospitalet-pres-I’Andorre ist nicht gerade ein Ort mit Flair: Infrastruktur wie das Wasserkraftwerk, die Passstraße und die Bahntrasse dominiert den Ort. Der Laden ist geschlossen und im Restaurant gibt es nur Getränke. Ich brauche aber dringend Proviant für die nächsten Tage. In der Gîte bekomme ich den Tipp, dass es etwas talabwärts noch eine Sandwicherie gibt. Diese verlasse ich dann auch mit einer großen Tüte voller Pizzastücken, Sandwichs, Quiche und Keksen. Somit kann ich abends noch zu einem kleinen Stausee aufsteigen.

Einen wesentlich größeren Stausee passiere ich am nächsten Tag. Mit niedrigem Pegel sieht er nicht gerade gut aus. Umso besser ist der Blick vom kleinen Etang des Fourats auf den schroffen Pic Carlit. Ein schöner Berg, der seine Umgebung deutlich überragt. Lange bleibe ich hier sitzen, weil der Wetterbericht ein Gewitter angekündigt hat, aber das bleibt in der Ferne. Also steige ich am späten Nachmittag durch die steile Rinne quasi in einer Linie zum Gipfel auf.

Pic Carlit und Etang des Fourats
Blick vom Pic Carlit
Blick vom Pic Carlit

Der Blick von oben lohnt sich, vor allem wegen der vielen Seen auf der Ostseite. Zu diesen steige ich ab und schlage abweichend zum HRP einen Bogen von See zu See. Hier gibt es unzählige schöne Biwakplätze, ich entscheide mich für den Estany de les Dugues. Er liegt in einer Heidelandschaft mit kleinen Granitbuckeln, ein interessanter Kontrast zu den Schieferspitzen am Pic Carlit.

Während ich früh morgens weiter gehe, kommt mir eine Flut von Tagesausflüglern entgegen und die Einsamkeit ist dahin. Nun muss ich fast einen ganzen Tag lang durch eine hügelige Mittelgebirgslandschaft wandern, um zum nächsten höheren Bergrücken zu kommen. Lange geht es durch Nadelwald, dann über asphaltierte Straßen. Nicht der spannendste Tag des Treks … Immerhin kann ich in Bolquère den Rucksack mit Proviant auffüllen und in Eyne gönne ich mir im Restaurant ein zweites Mittagessen. Gerade als ich bezahlen will, fängt es heftig an zu gewittern und ich bleibe noch ein paar Stunden.

Während ich später ins Vallée d’Eyne aufsteige, schüttet es schon wieder. Trotzdem sammeln sich im oberen Teil des Tals einige Zelte an.

Es folgt eine Wanderung über einen ziemlich langen Bergrücken. Nach ein paar Gipfeln geht es ein wenig auf der spanischen Seite hinab, wo ich so früh ankomme, dass ich beschließe, noch eine Etappe dranzuhängen. Also weiter, wieder auf den Rücken hinauf, der hier eher wie ein weites Plateau ist. Natürlich erwischt mich nun wieder ein Gewitter, aber ich bekomme diesmal nur 10 Minuten Hagel ab.

Später geht es an einem langen hydrothermalen Quarzgang entlang: zackige Felsformationen ziehen sich über den Rücken Les Esquerdes de Rotjà, was mich an einen Drachenschwanz denken lässt.

So langsam werde ich doch müde, es gibt aber weit und breit kein Wasser. Also schleppe ich mich weiter, bis ich kurz vor dem Refuge de Mariailles endlich an einen Bach komme und mein Zelt aufbaue.

Vom Aufstieg zum Canigou
Canigou cheminé

Am nächsten Morgen geht es auf den Canigou, den östlichsten richtigen Berg der Pyrenäen. Er hat auf der Süd- und der Nordseite jeweils einen felsigen Talkessel und sieht in den entsprechenden Blickachsen ziemlich schroff aus. Von allen anderen Seiten ist er eher ein großer runder Buckel. Der HPR führt durch den steilen Canigou cheminé zum Gipfel hinauf. Theoretisch wäre von oben das Mittelmeer zu sehen, ich blicke stattdessen auf ein Wolkenmeer. Auf der Nordseite geht es auf einem leichten Weg wieder hinab und dann auf halber Höhe Richtung Osten. Als ich die winzige Cabane du Punatell erreiche, bin ich so müde, dass ich bleibe. Für ein Zelt gibt es keinen Platz, also beziehe ich erstmals eine der Hütten. Ein großer Fehler! Kaum ist es dunkel, fängt der Katalane, der im Stockbett unten liegt, zu fluchen an und leuchtet mit seiner Stirnlampe herum. Etwas später krabbelt etwas über meine Hand, ich schlage zu: eine Bettwanze. Kurzerhand flüchte ich nach draußen und lege mich im Schlafsack auf den Tisch der Picknickecke. Immerhin ist der Himmel jetzt wolkenfrei und voller Sterne. Der Katalane packt seinen Rucksack und wandert weiter …

Vom Canigou bis zum Strand geht es ein paar Tage auf und ab über bewaldete Hügel, wobei der HRP nun meist dem GR10 folgt. Die Szenerie ist nicht mehr so beeindruckend. Wie den meisten geht es für mich vor allem darum, den Trek zu Ende zu bringen und am Meer anzukommen.

Kloster in Arles-sur-Tech

Hinab nach Arles-sur-Tech führt der Weg an den Resten einer Seilbahn entlang, die einst Eisenerz von einer Mine ins Tal transportiert hat. Für etwas Kultur mache ich einen kurzen Abstecher zu einem Dolmen und in Arles sehe ich mir die Klosterkirche an. Das tägliche Gewitter bringt zwar etwas Abkühlung, aber es bleibt extrem schwül und beim Weg über den nächsten Pass fühle ich mich wie in einem dampfenden tropischen Regenwald. Der Weg macht hier einen unnötigen riesigen Schlenker mit zusätzlichen Auf- und Abstiegen, bis ich endlich an meinem Tagesziel ankomme: Montalba d’Amélie. Hier gibt es ein Kirchlein, ein bewohntes und ein verfallenes Haus und genug Platz zum Biwakieren.

Montalba d’Amélie

Am nächsten Tag geht es über ein Gipfelchen namens Roc de Frausa, dann mache ich eine sehr lange Siesta an einem alten Brunnenhaus. Nachmittags bin ich schon in Las Illas, wo es am Dorfrand einen offiziellen Biwakplatz mit Dusche und Toilette gibt. Zur Abwechslung gönne ich mir abends ein Menü im Restaurant.

Am nächsten Tag geht es meist über staubige Pisten. Je tiefer ich komme, desto mehr Korkeichen sehe ich, an deren Stamm der untere Meter geschält ist. Kurz vor Le Perthus passiere ich Reste eines römischen Monuments und das barocke Fort de Bellegarde.

Fort de Bellegarde

Mit 280 m ist Le Perthus der niedrigste Pass der Pyrenäen, entsprechend verlaufen hier Autobahn und Bahntrasse. Der merkwürdige Ort besteht fast nur aus einer lebhaften Straße voller Supermärkte, Restaurants und Ramschläden, wobei die Grenze auf dem Bürgersteig verläuft. Alle großen Läden sind auf der spanischen Straßenseite und das Angebot ist auf französische Schnäppchenjäger spezialisiert, die Einkaufswägen mit Schnaps, Zigaretten und Schokolade füllen. Wie Aliens dazwischen einige Wanderer mit großen Rucksäcken, auf der Suche nach etwas Proviant für das letzte kleine Stück.

Kreativ platzierte Markierung des GR10

Weit oberhalb, am Col de l’Ouillat, sammeln sich die GR10- und HRP-Wanderer für die letzte Nacht auf ihrem jeweiligen Trek. Ich stelle meinen Wecker eine Stunde früher, bei Sonnenaufgang sitze ich bereits auf dem höchsten Punkt des Tages, Pic Neulos. Leider ist es extrem dunstig, es riecht leicht nach Waldbrand. Sowohl Canigou als auch das Mittelmeer sind gerade noch zu erahnen. Immerhin ist das Meer endlich in Sicht.

Von Hügel zu Hügel geht es nun tendenziell abwärts, durch Wald und über trockene Wiesen, und das Meer wird langsam immer deutlicher sichtbar und blauer. Dann komme ich durch Weinberge und schließlich durchquere ich Banyuls-sur-Mer und erreiche am Nachmittag des 38. Tages den Kiesstrand. Nach einem Eis springe ich sofort ins Wasser. Der Strand ist allerdings viel voller, als ich erwartet hatte.

Banyuls-sur-Mer

Als Kontrastprogramm bleiben mir noch 3 Tage Barcelona. Dafür brauche ich als Erstes neue Schuhe, die (am Anfang noch nagelneuen) Wanderschuhe sind komplett zerstört.


Pyrenäen-Traverse auf dem HRP

HRP Teil 4: Auf einsamen Pfaden nach Andorra

Von Salardú nach L’Hospitalet-pres-I’Andorre führt der HRP durch die hinterste Ecke von Katalonien und durch Andorra

Gallina-Seen

Der nächste Abschnitt der Haute randonnée pyrénéenne (HPR) führt durch einen relativ unbekannten, aber schönen Teil der Pyrenäen, in einem abgelegenen Winkel von Katalonien. Das kleine Fürstentum Andorra ist dann in 1½ Tagen durchquert. Das mit der Einsamkeit ist allerdings relativ, im August ist man auch hier nicht alleine. Außerdem kommen mir jetzt immer mehr HRP-Wanderer entgegen.

Salardú

In Salardú komme ich erst spät los, weil der kleine Laden erst um 9 aufmacht – und ich muss Essen für eine Woche kaufen. Beim Aufstieg sind die romanischen Kirchen im Tal und im Hintergrund der Pico de Aneto zu sehen. Nach einem Picknick an den Seen Estanys Rosari de Baciver kraxle ich einen steilen Grashang auf den Tuc de Marimanya hinauf und lasse damit die Tagesausflügler hinter mir. Es kommt eine Landschaft mit vielen Seen und ausgedehnten Blockhalden in Sicht und bald hüpfe ich oberhalb des Estany d’Airoto ziemlich lang von Block zu Block. Ich überquere noch einen Pass und biwakiere an einem kleinen See.

Blick vom Tuc de Marimanya
Estany d’Airoto

Es folgt ein Abstieg über zum Teil halb überwucherte Pfade zum verschlafenen Dorf Alós d’Isil, dann der Aufstieg zum nächsten Pass, Col de la Cornella. Nun folgen einige Seen. Besonders schön sind die Gallina-Seen beim Abstieg vom übernächsten Pass (Col de Calberante). Am untersten biwakiere ich nahe der Blechhütte Refugi Enric Pujol.

Alós d’Isil
Oberer Gallina-See

Es folgt ein Tag mit gleich zwei Gipfeln. Früh morgens besteige ich Mont Roig, mit tollem Blick u.a. auf die Gallina-Seen.

Am Mont Roig
Am Mont Roig
Gallina-Seen von Mont Roig

Dann baue ich das Zelt ab und wandere hinab zum winzigen Dorf Noarre. Während ich mich am Nachmittag dem nächsten Pass nähere, zieht ein Gewitter auf. Daher spurte ich so schnell wie möglich die 250 Höhenmeter zum Gipfel des Pic de Certescan hinauf und wieder hinab. Ich biwakiere am Estany de Certascan, dem man wegen des braunen Streifens entlang des steilen Ufers deutlich ansieht, dass er aufgestaut ist.

Lac de Certascan

Am nächsten Morgen komme ich zum sehr hübschen See Estany Romedo de Dalt. Das wäre ein schönerer Biwakplatz gewesen … Ich mache eine lange Pause, heute bin ich ziemlich müde. Dann geht es an vielen Wasserfällen vorbei abwärts.

Estany Romedo de Dalt

Kurz nachdem ich nach meiner Mittagspause am tiefsten Punkt loslaufe, fängt es an zu regnen. Bis zum Coll de Sellente bekomme ich einen mehrfachen Wechsel von Starkregen mit Blitzen und Donner und Regenpausen mit ein paar Sonnenstrahlen. Vom Pass aus ist es nicht weit zur leuchtend orangenen Blechhütte Refugi de Baborte, die ich tropfnass ansteuere. Doch leider ist sie bereits so voll wie eine Sardinendose. Zum Glück hört der Regen wenige Minuten später auf und ich baue mein Zelt am See auf.

Estany de Baborte

Am nächsten Morgen geht es durch ein flaches Hochtal, Pla de Boet (hier zweigt eine beliebte Variante des HRP via Arinsal in Andorra ab). Dann über die Port de Boet für ein paar Stunden nach Frankreich. Da schon wieder Wolken aufziehen, beeile ich mich. Nach einer frühen Mittagspause am Etang de la Soucarrane mache ich mich auf zum nächsten Pass, die Port de Rat. Hier betrete ich Andorra und damit ein Land, in dem ich noch nie war.

Es ist merkwürdig, nach Tagen in der Wildnis an der Bergstation einer Seilbahn anzukommen und die Wahl zwischen zwei Restaurants zu haben. Mit Heißhunger schlage ich mir den Bauch voll. Dann drehe ich noch eine wirklich schöne Runde auf dem Panoramaweg durch den (absolut nicht tristen) Cirque de Tristaina. Die drei Seen sehen im Abendlicht toll aus – und das schlechte Wetter hat mir tatsächlich den Gefallen getan, hinter der nächsten Bergkette zu bleiben. Eigentlich hätte ich hier gerne übernachtet, aber die Zelten-Verboten-Schilder sind nicht zu übersehen. Also steige ich noch ein gutes Stück ins Tal ab.

Cirque de Tristaina in Andorra

Der nächste Tag führt über mehrere Pässe und an Seen vorbei durch das nordöstliche Andorra. Dabei bin ich meist nur knapp über der Baumgrenze und trotzdem den Gipfeln nah. Kurz nach der Mittagspause fängt es am Pass Serra de Cabana Sorda mal wieder an, heftig zu gewittern. Eilig steige ich zur Hütte ab. Der Hagel tut derart auf dem Schädel weh, dass ich mir mir etwas unter die Kapuze stopfe, und die Schuhe füllen sich mit Wasser. Bis ich an der Hütte bin, ist das Schlimmste vorbei, trotzdem stelle ich mich noch für eine Stunde unter. Leider sind nur noch Stehplätze frei …

Um wieder warm zu werden, wandere ich noch bis zu den Juclar-Seen, wo ich ziemlich müde kurz vor Sonnenuntergang ankomme (am oberen See gibt es einen schönen Biwakplatz). Hier sind die Berge doch noch einmal schroff und gezackt.

Juclar-Seen

Nun ist es nur noch ein kurzer Aufstieg zum Col de l’Albe, an dem ich Andorra schon wieder verlasse und wieder nach Frankreich komme. Der Weg über den Pass ist wirklich schön mit all den Seen. Besonders gefällt mir der Blick zurück auf die Juclar-Seen, aber auch die Etangs de l’Albe auf der anderen Seite. Dann geht es hinab nach L’Hospitalet-pres-I’Andorre.

Etangs de l’Albe
Etangs de l’Albe

(Weiter mit HRP Teil 5.)


Pyrenäen-Traverse auf dem HRP

HRP Teil 3: Über hohe Pässe

Zwischen Parzán und Salardú befinden sich die höchsten Berge der Pyrenäen und die höchsten Pässe des HRP

Auf dem HRP am Col des Gourgs-Blancs

Der nächste Abschnitt auf der Haute randonnée pyrénéenne (HPR) führt durch eine beeindruckende Gebirgslandschaft mit Bergen wie Pico de Aneto und zahlreichen Bergseen, z.B. im Aigüestortes-Nationalpark.

Es geht aber erst einmal mit einem langweilen und schweißtreibenden Aufstieg auf einer Schotterstraße los. Mit der Mittagshitze rächt sich die Übernachtung an den Barroude-Seen … Auf der anderen Seite des Passes wandere ich auf den Posets zu, den zweithöchsten Berg der Pyrenäen. Ich übernachte auf einem Campingplatz, eine gute Gelegenheit zum Duschen und Akkus laden.

Posets

Es folgt ein langer Tag mit vielen Pässen. Morgens steige ich auf mehr oder weniger deutlichen Pfaden zum Port d’Aygues Portes auf, auf der anderen Seite geht es bis zu den Wolken hinab. Auf dieser Höhe quere ich eine Weile den Hang, an einem See und mehreren Stolleneingängen vorbei. Dann hinauf zu einem Stausee und weiter zum hübschen Lac des Isclots.

Lac des Isclots
Blick vom Lac du Mlieu zum Col des Gourgs-Blancs
Am Col des Gourgs-Blancs

Über Blockhalden und einen Gletscherrest geht es weiter. Bis ich auf dem Col des Gourgs-Blancs (2877 m) ankomme, ist früher Abend. Jetzt muss ich noch über einen weiteren Pass, einen kleinen Berg und dann hinab zum von 3000ern umgebenen Lac du Portillon (leider nur ein Stausee).

Lac Glacé
Lac du Portillon

Der nächste Tag beginnt mit dem höchsten Pass des HRP, dem Col Inférieur de Literole (2983 m). Das erste Stück hinab auf der anderen Seite gilt als schwerste Stelle des HRP, ein unangenehmer Steilhang, der unter den Füßen zerbröselt. Wenig später folgt oberhalb des Lago de Literole noch ein weiterer Pass. Hier lasse ich den Rucksack liegen und mache einen Abstecher auf den Pic Perdiguère (3222 m). Das ist der höchste Gipfel rund um den Lac du Portillon, wobei der Aufstieg auf seiner Rückseite ziemlich einfach ist.

Col Inférieur de Literole
Lac du Portillon vom Pic Perdiguère

Durch ein hübsches Hochtal geht es anschließend abwärts, in das obere Ende des Tals von Benasque hinein. Ich steige noch bis zum Refugio de la Renclusa auf, von wo ich den Pico de Aneto besteigen will.

Pico de Aneto vom Portillon Superior

Im Schein der Stirnlampe steige ich zum Portillon Superior auf, einem Pass im Grat hoch über der Hütte. Bei Sonnenaufgang habe ich dort eine grandiose Aussicht, allerdings mit ziemlich finsteren Wolken über dem Gipfel. Ich beschließe schon, lieber umzukehren, da kommt eine Gruppe hinauf und meint, dass es erst am Nachmittag regnen soll. Also gehe ich weiter, meist über Blockhalden hüpfend. Dann die Gletscherreste: Viel ist zwar nicht mehr übrig, aber richtige Steigeisen statt meiner Grödel wären angebrachter gewesen. Bis ich auf dem Gipfel bin, ist dieser in Wolken und ich sehe nicht allzu viel. Die meisten anderen sind über eine andere Route gekommen (über den Pass direkt an der Hütte und an einem kleinen See vorbei). Diese nehme ich für den Abstieg und bin auf dieser trotz Gegenanstieg viel schneller. Die glatt polierten Granitplatten sind im Vergleich zu den Blockhalden wie eine Autobahn …

Forau de Aigualluts

Zurück auf dem HRP komme ich zum Forau de Aigualluts: Ein Fluss stürzt hier in einem Wasserfall in eine schluchtähnliche Doline und verschwindet im Untergrund. Übrigens habe ich hier die Hälfte des HRP geschafft. Etwas weiter talaufwärts baue ich mein Zelt auf. Nachts gibt es ein Gewitter.

Am nächsten Morgen sieht das Wetter noch unentschieden aus. In Richtung des nächsten Passes blauer Himmel, in die andere Richtung dunkle Wolken. Als ich etwa 300 m unterhalb des Passes eine flache Talschüssel aus Granit erreiche, schwappt von unten eine Wolke hinauf. Schlagartig ist der Nebel so dicht, dass ich nicht einmal mehr den nächsten Steinmann sehen kann, geschweige denn die zwei Wanderer, die gerade noch kurz vor mir waren. Ich packe mein Handy für die Navigation aus, aber bis ich ein GPS-Signal habe, fängt es schon an zu schütten und zu gewittern. Das geht so schnell, dass ich schon nass bin, bis ich das Handy weg- und die Regensachen ausgepackt habe. Ich kauere mich in eine Ritze zwischen Felsen. Regen und Hagel prasseln auf mich ein. Die Berge bekommen weiße Tupfer und wo gerade noch trockener Fels war, gibt es plötzlich zahllose Wasserfälle. Neben mir, wo vorher ein Rinnsal floss, rauscht jetzt ein 10 m breiter, reißender Fluss. Nach 2 Stunden hört das Gewitter endlich auf und mir ist eiskalt. Ich mache einen Vorstoß, komme aber nicht weit, weil der Fluss im Weg ist. Aus dem gleichen Grund ist ein Rückzug ins Tal unmöglich. Also erst einmal einen Kaffee kochen und beobachten, wie der Pegel wieder sinkt.

Nach dem Gewitter am Col des Mulleres

Als ich wieder aufsteige, fängt es schon wieder an zu regnen. Also beeile ich mich, jetzt will ich wirklich über den Pass. Ich eile von Steinmann zu Steinmann und merke erst oben, dass ich am falschen Pass gelandet bin, auf der falschen Seite des Aussichtsgipfels Tuc de Mulleres. Immerhin reißt es jetzt etwas auf, was natürlich ziemlich gut aussieht. Das Gipfelchen spare ich mir trotzdem, ich quere den Hang über die Blöcke zur anderen Seite. Lustigerweise finde ich dabei Fußstapfen in den frischen Hagelresten. Die anderen beiden Wanderer müssen genau denselben Fehler gemacht haben! Endlich bin ich am Col des Mulleres, dem letzten hohen Pass des HRP. Ich steige noch bis zu den Seen ab und biwakiere in der Nähe einer kleinen Blechhütte.

Der nächste Tag beginnt mit Nebel und hört mit Nebel auf. Immerhin sitze ich mittags am Lac de Rius in der Sonne und breite meine nassen Sachen (insbes. Das Handy) zum trocknen aus.

Lac de Rius

Aber schon am Nachbarsee stapfe ich durch Nebel. Auf den Blick von einem kleinen Pass auf einen der schönsten Seen der Pyrenäen, Lac de Mar, hatte ich mich besonders gefreut. Ich sehe nur Nebel. Nach einer Nacht am Ufer steige ich zum Sonnenaufgang noch mal hinauf, um den Blick wenigstens im Gegenlicht zu haben.

Lac de Mar
Lac de Mar

Es folgen weitere Seen und Pässe und gute Aussichten. Hier bin ich im spanischen Aigüestortes-Nationalpark, in dem es sehr viele hübsche Seen zwischen schroffen Granitbergen gibt. Dann geht es ein langes Tal hinab nach Salardú, wo ich es mir in einer Pension bequem mache.

Aigüestortes-Nationalpark von Port de Caldes

(Weiter mit HRP Teil 4.)


Pyrenäen-Traverse auf dem HRP

HRP Teil 2: Durch den Pyrenäen-Nationalpark

Auf dem HRP von Lescun nach Parzán mit Vignemale, Gavarnie und Pic du Midi d’Osseau.

Nordwand des Vignemale

Ab Lescun windet sich die Haute randonnée pyrénéenne (HPR) mehr als eine Woche lang durch den französischen Pyrenäen-Nationalpark. Dabei komme ich bei einigen Highlights vorbei, u. a. am Vignemale, dem Felskessel von Gavarnie und dem ikonischen Pic du Midi d’Osseau.

Von Lescun steige ich bei zunehmender Hitze Richtung Osten auf, in den Nationalpark hinein. Ich komme über einen Pass, über den im 2. Weltkrieg viele Juden nach Spanien (und meist weiter nach Amerika) geflüchtet sind. Am Abend erreiche ich Refuge d’Arlet (derzeit eine Baustelle). In der Nähe, an meinem ersten (aber nicht schönstem) See baue ich mein Zelt auf.

Bei Sonnenaufgang bin ich schon auf dem nächsten Hügel, mit Blick auf einen der schönsten Berge der Pyrenäen, dem Pic du Midi d’Osseau. Aber erst mal geht es nach Süden zum nächsten Bergrücken. In Spanien, direkt hinter der Grenze, erreiche ich den wunderschönen See Ibon de Astanés.

Ibon de Astanés

Mittags komme ich im Skiort Candanchu an. Der kleine Laden ist sogar offen, sodass ich noch mehr Essen in den Rucksack laden kann. Nach langer Siesta wandere ich hinauf zum Col des Moines. Hier hat man einen guten Blick auf den Pic du Midi d’Osseau, ein massiver Felsklotz, der die grüne Landschaft überragt.

Pic du Midi d’Osseau vom Col des Moines

Es handelt sich um einen Caldera-Vulkan aus dem Perm, auch die Berge zwischen Col des Moines und Refuge d’Anyous gehören dazu. Ist der grüne Talkessel dazwischen also die Caldera? Natürlich nicht. Alleine die Kalksteine dazwischen passen nicht. Während der jüngsten Gebirgsbildung wurden die Vulkangesteine (insbes. Andesit) in den Deckenstapel der Pyrenäen integriert und dabei in mehrere Teile zerlegt. Diese befinden sich heute mehr oder weniger entlang eines Ovals angeordnet.

Ich hatte tolle Fotos von der Umgebung des Refuge d’Ayous gesehen, von hier aus sieht der Pic du Midi d’Osseau am interessantesten aus. Da dies nur ein kurzer Abstecher ist, steuere ich die am Lac Gentau gelegene Hütte als Biwakplatz an. Schon am Nachbarsee steht an jeder Ecke ein Zelt, und am See neben der Hütte geht es wie auf einem Campingplatz zu: Etwa 150 Zelte, viele Menschen planschen im Wasser, spielen Schwedenschach oder machen ein Lagerfeuer, zwei Drohnen surren in der Luft. Naja, was habe ich den erwartet, an einem Postkartensee am Samstagabend? Ich steige auf den Pass hinter der Hütte auf, blicke über den See auf den schönen Berg und habe dort doch noch meine Ruhe.

Pic du Midi d’Osseau vom Pass oberhalb Refuge d’Ayous

Mitten in der Nacht breiten sich im Mondschein Kühe laut bimmelnd zwischen den Zelten aus. Zwei Stiere kämpfen immer wieder miteinander, wenn sie gerade nicht eine Kuh besteigen. Beides geht mit Gerenne einher, wie gesagt zwischen den Zelt. Irgendwann wird es mir zu viel, ich stehe auf und treibe die Viecher auf die leere Wiese nebenan. Natürlich dauert es nicht lange, bis sie wieder da sind und ich erneut aufstehen muss … Dieser See wird mir wirklich in ambivalenter Erinnerung bleiben.

Am Morgen bin ich so müde, dass ich erst den Wecker überhöre und dann nur im Schneckentempo vorankomme. Jedenfalls bin ich froh, sobald ich zurück auf der Route des HRP bin, dass ich den Zirkus hinter mir gelassen habe. Es geht durch den Talkessel und dann über den Pass direkt neben dem Pic du Midi d’Osseau, der wirklich von jeder Seite gut aussieht. Von hier ist auch das nächste Bergmassiv gut zu sehen, allerdings auch das tiefe Tal, in das ich vorher noch absteigen muss.

Am Refuge de Pombie bin ich erst mittags, keine Zeit für eine lange Siesta. Zum Glück ist es unten im Tal waldig. Der folgende Aufstieg durch das Hochtal zieht sich endlos und es ist viel zu heiß. Ich frage mich, wie lange das Wetter noch hält, bisher habe ich auf dem Trek kaum ein Wölkchen gesehen. Vielleicht sollte ich in die Fotos ein paar Wolken malen, damit der Himmel nicht immer so langweilig blau ist? Ein anderer Wanderer meint, dass die nächsten Tage noch heißer werden sollen.

Lac d’Arrious

An einem Pass angekommen wird es sofort wieder interessant. Innerhalb weniger Minuten komme ich an einen See, Lac d’Arrious, daneben ein Berg, der aussieht wie der Zuckerhut in klein. Auf der Passage d’Orteig habe ich 5 Minuten Klettersteig-Feeling, wenig später kommt das winzige Refuge d’Arremoulit in Sicht. Es liegt wunderschön an einem See zwischen schroffen Granitbergen.

Biwak nahe Refuge d’Arremoulit

Hier muss man sich entscheiden, wie man zum Refuge Wallon kommt: die südliche, kürzere und schnellere Variante (1 Tag) über den Stausee Embalse de Respomuso? Oder die alpinere nördliche Variante (1,5 Tage) via Refuge de Larribet? Ich entscheide mich für die nördliche, mache aber trotzdem noch einen Spaziergang auf den Pass, über den die andere Variante führt, allein wegen der Aussicht auf zwei Seen und den 3000er Pic Balaitous. Dann genieße ich erst einmal den Abend am See, mein Zelt steht direkt oberhalb auf einem Granitbuckel.

Pic Balaitous

Am Morgen folge ich den Steinmännchen zum ersten Pass, Col du Palas. Zum nächsten Pass, der Port du Lavédan, ist es nicht weit, eigentlich nur den Steilhang queren … Die Markierungen hören an einem Geröllfeld auf und man muss selbst überlegen, wie es weiter geht und wo in dem kleinen Felsriegel die Schwachstelle ist. Mir macht das Spaß, zumal der Blick auf die Seen tief unter mir grandios ist.

Aufstieg zur Port du Lavédan

Mit leichter Kraxelei überwinde ich die Scharte, dann geht es an ein paar Seen vorbei zum Refuge de Larribet, und weiter das hübsche Hochtal hinab.

Natürlich muss ich das nächste Tal wieder hinauf. Irgendwann will ich einen Blick auf den Höhenmesser werfen, aber die Uhr ist nicht am Arm. Bei der letzten Pause hatte ich sie ausgezogen, um Sonnencreme nachzuschmieren. Das war ungefähr vor einer Stunde … Also werfe ich den Rucksack hinter einen Steinblock und spurte wieder hinab. Mit Erfolg, aber nach dieser Aktion wird mein Zeitplan ziemlich knapp, schließlich wollte ich heute noch über den nächsten Pass. Die letzten paar Hundert Höhenmeter zum Col de Cambalès mache ich im Abendlicht. Beim Abstieg zu den Seen auf der anderen Seite leuchten die Berge rot auf. Ich steure einen See mit auffälliger Herzform an und finde einen schönen Biwakplatz. Essen in der Dämmerung, zum Zähneputzen brauche ich schon die Stirnlampe.

Grand lac de Cambalès
Grand lac de Cambalès

Der Abstieg zum (wegen Komplettumbau geschlossenen) Refuge Wallon geht schnell. Eine hübsche Gegend mit ein paar Kiefern auf den Wiesen. Mittags bin ich am nächsten See, es ist sehr windig und immer wolkiger. Noch zwei Pässe (zwischen denen man den Hang quert), dann hinab zum Biwakplatz am Refuge des Oulettes de Gaube. Ein wirklich beeindruckender Ort direkt unter der Nordwand des Vignemale.

Nordwand des Vignemale

Mit der Nachricht, dass es spät abends ein heftiges Gewitter mit Hagel geben soll, verbreiten die Hüttenwirte leichte Panik am Biwakplatz. Bis auf entfernten Donner passiert aber nichts. Am nächsten Tag bin ich relativ faul, ich breche spät auf und steige nur bis zum Refuge de Bayssellance auf, und gehe nach langer Pause das kleine Stück bis zum Einstieg zum Vignemale. An einer leicht ausgesetzten Passage steht ein älterer Herr, der auf den ersten Blick wie ein normaler Wanderer aussieht, mit Isomatte am Rucksack. Nur hat er keine Wanderstöcke, sondern Krücken! Auf meine Frage, was er vorhat, antwortet er, dass er zum Gletscher wolle und überlege, ob er nicht besser umdrehen solle. Ich bestärke ihn in diesem Gedanken, aber von Weitem, von meinem Biwakplatz aus, sehe ich ihn noch ziemlich lang herumstehen. Der Felsen über mir sieht ziemlich gut aus, heller Marmor, von zahlreichen dunklen Gängen durchschlagen.

Nachts kommt ein heftiger Wind auf, der mein Zelt zusammendrückt und die Heringe aus dem Boden rupft. Ich muss aufstehen und alles stabiler mithilfe von Steinen und Wanderstöcken umbauen. Ich breche in der Dämmerung mit leichtem Gepäck auf, über Marmor-Rundhöcker aufwärts. Kurz nach Sonnenaufgang erreiche ich den flach in einer weiten Schüssel liegenden Gletscher. Ich passiere eine Scharte mit Tiefblick in die Nordwand, dann folgt leichte Kletterei bis zum Gipfel. Punkt 9 Uhr bin ich wieder unten an meinem Biwakplatz und baue mein Zelt ab.

Ossoue-Gletscher am Vignemale
Blick vom Vignemale

Es folgt ein langer Abstieg in das Tal, das nach Gavarnie führt. Kurz vor dem Ort biege ich aber von der HRP-Route ab und steige (z.T. über Skipisten) in den Sattel zwischen Pic des Tentes und Pic de la Pahule auf. Letzterer ist einer der besten Aussichtspunkte rund um den Cirque de Gavarnie, was aber offensichtlich ein Geheimtipp ist, ich treffe hier nämlich niemanden. Aber zunächst gewittert es in den Bergen gegenüber, während ich in der Sonne mein Zelt aufbaue und esse. Zum Glück reißt es zum Sonnenuntergang etwas auf. Ich blicke auf eine lange Felswand mit zahlreichen Gipfeln. In diese ist der eigentliche Felskessel eingegraben, in dem die Grande Cascade zu sehen ist, einer der höchsten Wasserfälle Europas. Ganz links im Panorama der Pass, über den der HRP weiter führt. Und der Berg ganz rechts, Pic du Taillon, ist mein Ziel für den nächsten Tag. Etwas links davon fällt eine markante Scharte auf, die Brèche de Roland.

Cirque de Gavarnie vom Pic de la Pahule

Der Deckenbau der Pyrenäen ist hier gut zu sehen (siehe auch mein Buch Bewegte Bergwelt). Die Kalksteine aus der Kreide und dem Eozän, welche die Berge aufbauen, wurden in südliche Richtung geschoben. Darunter ist eine tiefere Decke mit paläozoischen Sedimenten zu sehen. Der Talboden in Gavarnie ist eine noch tiefere Einheit mit Migmatit.

Pic du Taillon vom Pic de la Pahule

Mit minimalen Höhenmetern erreiche ich über Col des Tentes, Puerto de Burauelo und Col des Sarradets das Refuge des Sarradets. Ich verstecke den großen Rucksack und steige zur Brèche de Roland auf. Auf der spanischen Seite quere ich unterhalb der Felswand bis zu einer Felssäule namens Le Doigt, dann führt der Weg hinauf auf den Pic du Taillon.

Brèche de Roland
In der Brèche de Roland
Blick vom Pic du Taillon

Zurück am Refuge des Sarradets nehme ich den steilen Pfad (L’Échelle des Sarradets), der direkt in den Cirque de Gavarnie hinab führt, ständig mit großartigem Blick auf den Wasserfall. Wobei es immer wolkiger wird.

Cirque de Gavarnie

In Gavarnie kaufe ich nur kurz ein, esse eine Pizza und steige dann noch 2 Stunden im Nebel zum Refuge des Espugettes auf. Dort bin ich gerade über den Wolken, wenn nicht gerade ein Fetzen hinauf schwappt. Ich komme gerade rechtzeitig, um den Schlusstakt eines dramatischen Sonnenuntergangs zu erleben, mit roten Strahlen hinter dem Vignemale.

Morgens nehme ich den Aussichtsgipfel Piméné mit, um den Felskessel von der anderen Seite zu sehen.

Cirque de Gavarnie vom Piméné

Über die Hourquette d’Alans geht in den benachbarten Felskessel, Cirque d’Estaubé. Ein Stück talabwärts biege ich ins nächste Tal ein und wandere auf den Cirque de Troumouse zu. Schon von Weitem frage ich mich, wo ich am nächsten Tag durch diesen Felsriegel aufsteigen soll.

Im Cirque de Troumouse

Der Cirque de Troumouse ist ganz anders als die anderen Felskessel, man fühlt sich nicht eingeschlossen, sondern hat im inneren ein Gefühl von Weite. Die Felswände (überwiegend Kalkstein und Schiefer aus dem Devon) umgeben in einem Halbkreis mit 2 km Durchmesser ein hügeliges Plateau (Migmatit).

Cirque de Troumouse vom Col de la Sède

Ich biwakiere an einem ausgetrockneten See mitten im Cirque. Der Aufstieg zum Col de la Sède ist viel weiter links, als ich erwartet hatte, wirklich am Rand des Felsriegels. Lange genieße ich von oben den Blick in den Cirque, bevor ich zum nächsten Pass wandere. Von dort sehe ich tief unter mir mein heutiges Ziel, die Lacs de Barroude. Um ans untere Ende der Felswand zu kommen, muss ich allerdings einmal um den benachbarten Berg herum, über zwei weitere Pässe. Am See angekommen hätte ich eigentlich genug Zeit, um noch bis Parzán abzusteigen. Aber wieso sollte ich ins Tal, wenn ich noch eine Nacht an einem schönen Ort bleiben kann?

Lacs de Barroude
Lacs de Barroude

Am nächsten Morgen steige ich dann ins hübsche Barrosa-Tal ab. Hier ist deutlich eine der Überschiebungen der Pyrenäen zu sehen: Der obere Teil der Felswände besteht aus Schiefer und Kalkstein aus dem Devon (stellenweise darunter Schwarzschiefer, Silur), der untere Teil aus Granit, stellenweise noch mit Sandstein (Perm/Trias) direkt unter der Verwerfung.

Barrosa-Tal

Dann noch ein nerviger Marsch auf einer stark befahrenen Straße bis zum Supermarkt von Parzán. Hier kaufe ich nicht nur Essen für eine Woche, sondern auch Sekundenkleber. Damit will ich den rapiden Verschleiß meiner Schuhe bremsen. (Weiter mit HRP Teil 3.)


Pyrenäen-Traverse auf dem HRP

HRP Teil 1: Vom Strand zu den Bergen

Zwischen Hendaye und Lescun führt der HRP über grüne Hügel und schließlich durch eine skurrile Karstlandschaft

Grüne Hügel dominieren den ersten Abschnitt des HRP

Von Hendaye (an der Atlantikküste direkt an der Grenze Frankreich/Spanien) bis Lescun führt die Haute randonnée pyrénéenne (HPR) die meiste Zeit über saftig grüne Hügel. Wasserstellen sind in diesem Abschnitt rar, außerdem kämpft man hier vermutlich wahlweise mit schweißtreibender Hitze (so wie ich) oder mit Orientierungslosigkeit im Nebel. Oft sind steile Hänge mit einem dichten Gestrüpp aus hohem Farn bewachsen. Es geht auch durch Wälder mit knorrigen Bäumen oder über grasige Hochflächen. Oft folgt der Weg der Grenze von Grenzstein zu Grenzstein. Immer wieder sehe ich kleine Bunker aus dem Krieg. Friedlicher und deutlich älter sind Steinkreise und Dolmen. Jede Menge Pferde und Schafe laufen frei herum. Erst ganz am Schluss dieses Abschnitts erreiche ich richtige Berge mit Kalkfelsen.

Ich war spät abends mit dem TGV in Hendaye angekommen. Zu Sonnenaufgang beginne ich mit einem Spaziergang zu den Klippen am Ende des Strands (am Strand am Krankenhaus vorbei, dahinter ist der Zugang zu einem Parkplatz, wo der Weg beginnt).

Klippen bei Hendaye

Dann springe ich kurz in den Atlantik, bevor ich barfuß durch den Sand zum offiziellen Startpunkt laufe. Hier steht ein großes Schild mit einer Karte des GR10, der hier ebenfalls beginnt und dem ich heute die erste Tageshälfte folge. Der HRP wird nicht erwähnt …

Strand von Hendaye

Noch ein Frühstück in einem Café, Baguette kaufen und los. Zunächst folge ich einem Ästuar (auf der anderen, spanischen Seite ist der Flughafen von San Sebastian), dann geht es aufwärts. Ich lasse den Ort hinter mir und tauche in die grüne Hügellandschaft des Baskenlands ein. Ich habe tiefblauen, wolkenfreien Himmel. Nach sanftem Auf und Ab geht es steil aufwärts auf den Choldokogagna, mit tollem Blick zurück auf die Küste. Auf der anderen Seite ist die deutlich höhere La Rhune zu sehen, mein nächstes Ziel. Allerdings ist es so heiß, dass ich mühsam meinen Rucksack von Schattenplatz zu Schattenplatz schleppe, und von einem Pass zum nächsten. Dann auf einer steilen Piste den Berg hinauf. Der Blick von oben ist so gut, dass sogar eine Zahnradbahn hinauf fährt, nur leider wurde der Gipfel mit ziemlich hässlicher Infrastruktur vollgestellt. Über steile Pfade geht es wieder hinab. Als ich endlich mit müden Beinen am Col de Lizuniaga ankomme, baue ich mein Zelt auf der Wiese neben dem geschlossenen Restaurant auf, wo bereits andere Zelte stehen.

Blick zurück vom Choldokogagna

Am nächsten Morgen regnet es. Damit hätte ich nach dem wolkenfreien Tag wirklich nicht gerechnet. Nach dem Frühstück im Zelt hört der Regen schon wieder auf, der Nebel löst sich auf und das Wetter ist wieder so strahlend wie gestern. Ich wandere von einem Pass zum nächsten, wobei ich lange Zeit dem spanischen GR11 folge. Mal geht es durch angenehm kühle Wälder, dann wieder über einen Rücken mit Ausblicken auf die Nachbarhügel. Schließlich steige ich ins Tal zum Dorf Arizkun ab. Nach einer Pause mit Eis und Limo steige ich auf der anderen Talseite auf. Ich will die Kühle des Abends nutzen, um etwa die halbe Strecke zum nächsten Ort zu laufen. Am höchsten Punkt, dem Gipfel des Burga, habe ich einen tollen Blick aus dem Zelt.

Mein Zelt auf dem Burga

Am Morgen gehe ich früh los. Während ich eine Lichtung überquere, fällt plötzlich ein Schuss. Ganz in meiner Nähe flattert ein Vogel davon, dann taucht ein Jäger aus dem Farn auf. Wahrscheinlich dachten wir beide: „Was für ein Idiot!“

Aldudes

Bald erreiche ich das sehr hübsche Dorf Aldudes. Zum Glück wusste ich bereits, dass der Dorfladen Montags geschlossen ist, sonst müsste ich jetzt einen Tag warten. Nun geht es auf einen Rücken, dem ich mehr oder weniger folge und dabei zunehmend an Höhe gewinne. Die Redoute de Lindux ist mein erster Gipfel über 1000 m, aber das wird in den nächsten Tagen ganz normal. Von der Puerta de Ibañeta (Col de Roncevaux) geht es unschön auf bzw. nahe einer kleinen Straße knapp 400 m aufwärts, immerhin mit Blick auf das Kloster Roncevaux. Oben treffe ich auf den Jakobsweg (hier ist der höchste Punkt des Jakobswegs und auch meines heutigen Tages). Tatsächlich kommen mir nun einige Pilger entgegen, bevor ich abbiege.

Eigentlich wollte ich bei den Azpegi Cromlechs biwakieren, ein paar Steinkreisen aus Stein-/Bronzezeit. Allerdings ist das absolut kein guter Platz zum Zelten, statt Aussicht gibt es Disteln. Und die Steinkreise sind enttäuschend winzig, auch wenn ich nicht so etwas wie auf den Orkneys erwartet habe. Also gehe ich noch ein Stück weiter zum Col d’Orgambidé, wo ich gerade noch die Berge in Licht der untergehenden Sonne fotografieren kann. Der dortige Steinkreis ist allerdings zeitgenössisch …

Der nächste Vormittag ist anstrengend und verlangt einige Improvisation. Zunächst steige ich teilweise weglos den Steilhang zum Bach hinab, auf der anderen Seite steil wieder hinauf. Dann geht es durch ein Trockental hinab. Weiter unten fließt hier ein Bach, den ich an jeder Talbiegung von Stein zu Stein hüpfend überquere. An der Mündung in ein Flüsschen geht es geradeaus weiter, weglos quäle ich mich einen steilen Grashang hinauf. Dann quere ich den Hang unterhalb der Gipfel, und obwohl ich ziemlich langsam vorankomme, überhole ich einen HRP-Wanderer in Trailrunner-Outfit. Laut Führer soll es im Wäldchen neben dem Pass Wasser geben, ich finde aber nur einen Schlammpfuhl, in dem ein paar Pferde stehen. Für das Mittagessen bleibt mir noch ein allerletzter Schluck, über mir kreisen die Geier (kein Scherz, die sehe ich auf dem Trek immer mal wieder).

Cromlechs von Okabe

Bald erreiche ich die Cromlechs von Okabe. Auch diese Steinkreise sind ziemlich klein, aber sehr schön auf einer grasigen Hochfläche gelegen. Nun muss ich in ein Tal absteigen, in dem ich endlich das Wasser ausfüllen kann. Ich gönne mir nur eine kurze Pause, schließlich will ich rechtzeitig mein nächstes Ziel erreichen: den Laden in der Nähe des Col Bagargui. Ich komme gerade noch rechtzeitig, bevor er schließt, und es gibt alles, was das Herz eines Wanderers begehrt. Im Restaurant esse ich noch zu Abend, bevor ich auf den nächsten Bergrücken aufsteige. Hier biwakiere ich mit tollem Blick auf meinen morgigen Gipfel, Pic d’Orhy. Weiter entfernt sind erstmals die höheren Bergzacken zu sehen.

Pic d’Orhy

Den Pic d’Orhy erreiche ich am nächsten Mittag, und damit den ersten Gipfel über 2000 m. Die Aussicht in die unterschiedlichen Richtungen ist ein merkwürdiger Kontrast: nach Westen bis zum Horizont die grünen Hügel, über die ich gekommen bin, im Osten, hinter den letzten paar Hügeln, die felsigen Kalksteinberge, hinter denen Lescun liegt.

Ein Stück muss ich bis dort aber noch laufen. Ich wandere weiter über den Grenzkamm, muss aber kurz in die Nähe der Cabane d’Ardané absteigen, um Wasser zu tanken. Ich steige dann gleich wieder auf und biwakiere auf einem Pass unterhalb der Porte de Belhay. Hier ist die Landschaft vergleichbar mit den Voralpen, mit Kühen auf steilen Grashängen und kleinen Felsen an spitzen Bergen.

Am nächsten Tag wird die Landschaft nochmals deutlich interessanter. Vom höchsten Punkt aus, noch früh am Morgen, öffnet sich ein tolles Panorama mit den zackigen Bergen, während das Tal im Mittelgrund im Dunst liegt. In dieses Bild wandere ich nun hinein, zum tiefer gelegenen Refugio de Belagua. Als zweites Frühstück bestelle ich Torttilla und einen Café Cortado. Dann fülle ich die Wasserflaschen, da ein langes Stück ohne Wasserstellen kommt.

Wenig später wandere ich durch eine interessante Karstlandschaft, die mich sehr an den Velebit erinnert. Die Kalksteine sind heftig von Karren durchfurcht. Es geht durch ein Trockental, dessen untersten Ende eine rundum von höherem Terrain umgebende Senke ist. Weiter oben wachsen immer weniger Kiefern und es sieht zunehmend alpiner aus. Die Hänge bestehen aus nacktem Gestein, obwohl sie nicht allzu steil sind.

Ich komme auf eine Art Plateau, das nur noch aus Kalksteinhöckern und Dolinen besteht. Auf der anderen Seite des Passes steige ich zur Source de Marmitou ab, ein wirklich schöner Biwakplatz. Umgeben von hohen Felswänden murmelt das Wasser aus einer Wiese, direkt neben einem Labyrinth aus Bouldern.

Nahe Source de Marmitou

Früh morgens wandere ich durch dieses Hochtal abwärts. Nach einer Steilstufe habe ich einen besonders malerischen Blick zurück auf einen Wasserfall und den zwar kleinen, aber besonders spitzen Pic de la Breque.

Pic de la Breque

Schließlich erreiche ich das Dorf Lescun, sehr schön gelegen mit den Bergen im Hintergrund. Doch ich mache mich gleich wieder auf die Socken, nachdem ich meinen Rucksack mit Lebensmitteln befüllt habe. (Weiter mit HRP Teil 2.)


Pyrenäen-Traverse auf dem HRP

Runkeeper GPS-Tracks mit Python analysieren (Teil 2)

Alle Runkeeper-GPS-Tracks (GPX-Dateien) in einen einzigen GeoDataFrame importieren und mit Python analysieren und auf eine Karte plotten

In Teil 1 dieser Serie habe ich einen einzelnen GPS-Track mit Geopandas und Folium untersucht. Der nächste Schritt besteht darin, den Code, der den GeoDataFrame vorbereitet, in eine Funktion zu verschieben. Dann lese ich alle GPX-Dateien eines bestimmten Ordners ein und übergebe jeden GPS-Track an meine Funktion.

Jupyter Notebook auf Github

Funktion zur Vorbereitung des GeoDataFrames

Nachdem ich einige Module importiert und den Ordner mit den GPX-Dateien festgelegt habe, definiere ich eine Funktion, die ziemlich genau dasselbe tut wie Teil 1. Ich habe Code hinzugefügt, um seltsame Ausreißer zu behandeln: Nach Pausen/Stopps (z. B. beim Warten an einer Ampel) war das von Runkeeper verwendete GPS manchmal etwas verwirrt und wusste nicht wirklich, wo ich war. In einem Fall war der erste Punkt nach einem Stopp 1,6 km von der tatsächlichen Position entfernt, was zu einem Liniensegment mit einer Geschwindigkeit von mehr als 400 km/h führte – und zu einer Gesamtstrecke, die um 3,3 km zu lang war.

import pandas as pd
import geopandas as gpd
import numpy as np
import os
from shapely.geometry import LineString
import folium

folder = "gpx/"

def prepare_dataframes(gdf, track_name):
    """ Calculate distance, speed etc. from raw data of gpx trackpoints. 
    Return two GeoDataframes: points and (connecting) lines.
    """
    gdf.index.names = ['point_id'] 
    gdf['time'] = pd.to_datetime(gdf['time'])
    gdf.dropna(axis=1, inplace=True)
    gdf.drop(columns=['track_fid', 'track_seg_id', 'track_seg_point_id'], inplace=True)

    # Use local UTM to get geometry in meters
    gdf = gdf.to_crs(gdf.estimate_utm_crs())

    # shifted gdf gives us the next point with the same index
    # allows calculations without the need of a loop
    shifted_gdf = gdf.shift(-1)
    
    gdf['time_delta'] = shifted_gdf['time'] - gdf['time'] 
    gdf['time_delta_s'] = gdf['time_delta'].dt.seconds
    gdf['dist_delta'] = gdf.distance(shifted_gdf)
    
    # In one track, after making a pause, I had a weird outlier 1.6 km away of my real position. 
    # Therefore I replace dist_delta > 100 m with NAN. 
    # This should be counted as pause.
    gdf['dist_delta'] = np.where(gdf['dist_delta']>100, np.nan, gdf['dist_delta'])

    # speed in various formats
    gdf['m_per_s'] = gdf['dist_delta'] / gdf.time_delta.dt.seconds 
    gdf['km_per_h'] = gdf['m_per_s'] * 3.6
    gdf['min_per_km'] = 60 / (gdf['km_per_h'])
    
    # We now might have speeds with NAN (pauses, see above)
    # Fill NAN with 0 for easy filtering of pauses
    gdf['km_per_h'].fillna(0)


    # covered distance (meters) and time passed
    gdf['distance'] = gdf['dist_delta'].cumsum()
    gdf['time_passed'] = gdf['time_delta'].cumsum()
    
    # Minutes instead datetime might be useful
    gdf['minutes'] = gdf['time_passed'].dt.seconds / 60

    # Splits (in km) might be usefull for grouping
    gdf['splits'] = gdf['distance'] // 1000

    # ascent is = elevation delta, but only positive values
    gdf['ele_delta'] = shifted_gdf['ele'] - gdf['ele']  
    gdf['ascent'] = gdf['ele_delta']
    gdf.loc[gdf.ascent < 0, ['ascent']] = 0

    # Slope in %
    gdf['slope'] = 100 * gdf['ele_delta'] / gdf['dist_delta']   
   
    # slope and min_per_km can be infinite if 0 km/h
    # Replace inf with nan for better plotting
    gdf.replace(np.inf, np.nan, inplace=True)
    gdf.replace(-np.inf, np.nan, inplace=True)

    # Ele normalized: Startpoint as 0
    gdf['ele_normalized'] = gdf['ele'] - gdf.loc[0]['ele']
    
    # Back to WGS84 (we might have tracks from different places)
    gdf = gdf.to_crs(epsg = 4326)
    shifted_gdf = shifted_gdf.to_crs(epsg = 4326)
    
    # Create another geodataframe with lines instead of points as geometry.
    lines = gdf.iloc[:-1].copy() # Drop the last row
    lines['next_point'] =  shifted_gdf['geometry']
    lines['line_segment'] = lines.apply(lambda row: 
        LineString([row['geometry'], row['next_point']]), axis=1) 
    lines.set_geometry('line_segment', inplace=True, drop=True)
    lines.drop(columns='next_point', inplace=True)
    lines.index.names = ['segment_id'] 
    
    # Add track name and use it for multiindex
    gdf['track_name'] = track_name
    lines['track_name'] = track_name
    gdf.reset_index(inplace=True)
    gdf.set_index(['track_name', 'point_id'], inplace=True)
    lines.reset_index(inplace=True)
    lines.set_index(['track_name', 'segment_id'], inplace=True)
    return gdf, lines

Jetzt können wir die GPX-Dateien öffnen und unsere GeoDataFrames mit Daten befüllen.

# Prepare empty Geodataframes
points = gpd.GeoDataFrame()
lines = gpd.GeoDataFrame()

# And populate them with data from gpx files
for file in os.listdir(folder):
    if file.endswith(('.gpx')):
        try:
            rawdata = gpd.read_file(folder + file, layer='track_points')
            track_points, track_lines = prepare_dataframes(rawdata, file)
            points = pd.concat([points, track_points])
            lines = pd.concat([lines, track_lines])
        except:
            print("Error", file)
lines.head()

Sind die Liniensegmente vergleichbar?

Einfache Diagramme wären aussagekräftiger, wenn jedes Liniensegment eine ähnliche Länge in Bezug auf Zeit oder Entfernung hätte. Meine Schlussfolgerung nach der Untersuchung verschiedener Diagramme von 'dist_delta', 'time_delta' und Geschwindigkeit: Die Frequenz der Messungen ist normalerweise ein Punkt alle 3 bis 6 Sekunden. Runkeeper scheint die Frequenz entsprechend der aktuellen Geschwindigkeit zu ändern und erhält ziemlich regelmäßige Abstände zwischen den Messpunkten (ein Punkt alle 9 bis 12 m). Die Ausnahme sind Abschnitte mit sehr niedrigen Geschwindigkeiten von 0 bis 1,5 km/h, d. h. Pausen. Ich denke, dies macht die Liniensegmente vergleichbar genug, um sie für einfache Diagramme zu verwenden. Wir können davon ausgehen, dass jedes Segment etwa 10 m lang ist oder eine Pause darstellt. Wir könnten darüber nachdenken, Segmente mit sehr niedrigen Geschwindigkeiten herauszufiltern.

Manchmal hatte ich absurd hohe Geschwindigkeiten, wenn das GPS-Signal nicht genau war. Dies geschah unter Brücken oder direkt nach einer Pause/einem Stopp. Auch diese können wir herausfiltern.

Einfache Statistik

Mit ein paar Zeilen Code können wir einige grundlegende Statistiken über unsere Spuren erhalten. Versuchen Sie das Folgende:

Anstieg in Metern:
lines.groupby('track_name')['ascent'].sum()

Auch interessant:
lines.groupby('track_name')['ascent'].sum().describe()

Distanz in Metern:
lines.groupby('track_name')['distance'].sum().describe()

Geschwindigkeit:
lines.groupby('track_name')['km_per_h'].describe()

Nützliche Informationen über jeden Lauf extrahieren

Ich erstelle einen Pandas-Dataframe mit allgemeinen Informationen über jeden Lauf, wie Entfernung, Steigung, Durchschnittsgeschwindigkeit. Ich berechne auch die Zeit der Pausen (alle Intervalle mit sehr niedriger Geschwindigkeit, ich setze den Schwellenwert auf 1,5 km/h).

runs = pd.DataFrame({
              'distance': lines.groupby('track_name')['dist_delta'].sum(),
              'ascent': lines.groupby('track_name')['ascent'].sum(),
              'start_time': points.groupby('track_name')['time'].min(),
              'end_time': points.groupby('track_name')['time'].max(),
              'median_km_h' : lines.groupby('track_name')['km_per_h'].median()
              })

runs['total_duration'] = runs['end_time'] - runs['start_time']

# Pauses (speed <1.5 km/h)
runs['pause'] = lines[
      lines['km_per_h']<1.5
      ].groupby('track_name')['time_delta'].sum()
runs['pause'] = runs['pause'].fillna(pd.Timedelta(0))

# Duration without pauses
runs['duration'] = runs['total_duration'] - runs['pause']
runs['minutes'] = runs.duration.dt.seconds / 60

# Speed
runs['m_per_s'] = runs['distance'] / runs.duration.dt.seconds 
runs['km_per_h'] = runs['m_per_s'] * 3.6
runs['min_per_km'] = 60 / runs['km_per_h']

# Distance in km
runs['distance'] = runs['distance'] / 1000 

Wir können runs auch in einen GeoDataFrame umwandeln und die Geometrie der gesamten Strecke hinzufügen:

# Add Geometry of the (complete) runs
runs = gpd.GeoDataFrame(runs, geometry=lines.dissolve(by='track_name')['geometry'])

Für eine einfache Statistik:
runs.describe()

Einfache Abfragen

Sie wollen vermutlich den schnellsten oder längsten Lauf oder ähnliche Informationen suchen.

Die 5 längsten Läufe:

runs.sort_values(by='distance', ascending=False).head(5)

Die 5 schnellsten Läufe:

runs.sort_values(by='km_per_h', ascending=False).head(5)

Die schnellsten Läufe mit einer Länge zwischen 8 und 12 km:

runs[(runs['distance']>=8) & (runs['distance']<=12)].sort_values(by='km_per_h', ascending=False).head(5)

Berichte über Monate und Jahre

Wie viele Aktivitäten hatte ich letztes Jahr? Welche Strecke habe ich im März insgesamt zurückgelegt? Wir können eine Tabelle erstellen, um solche Fragen zu beantworten.

Bericht pro Jahr:

per_year = pd.DataFrame({
              'count': runs.groupby(runs.start_time.dt.year)['distance'].count(),
              'total_distance': runs.groupby(runs.start_time.dt.year)['distance'].sum(),
              'distance_median': runs.groupby(runs.start_time.dt.year)['distance'].median(),   
              'distance_mean': runs.groupby(runs.start_time.dt.year)['distance'].mean(),  
              'distance_max': runs.groupby(runs.start_time.dt.year)['distance'].max(),    
              'total_ascent': runs.groupby(runs.start_time.dt.year)['ascent'].sum(),
              'ascent_median': runs.groupby(runs.start_time.dt.year)['ascent'].sum(),
              'ascent_max': runs.groupby(runs.start_time.dt.year)['ascent'].max(),
              'median_km_h' : runs.groupby(runs.start_time.dt.year)['km_per_h'].median(),
              'mean_km_h' : runs.groupby(runs.start_time.dt.year)['km_per_h'].mean(),
              })

per_year.index.name = 'year'

per_year

Bericht pro Monate:

per_month = pd.DataFrame({
              'count': runs.groupby([runs.start_time.dt.year, 
                                     runs.start_time.dt.month])['distance'].count(),
              'total_distance': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['distance'].sum(),
              'distance_median': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['distance'].median(),   
              'distance_mean': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['distance'].mean(),  
              'distance_max': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['distance'].max(),    
              'total_ascent': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['ascent'].sum(),
              'ascent_median': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['ascent'].sum(),
              'ascent_max': runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['ascent'].max(),
              'median_km_h' : runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['km_per_h'].median(),
              'mean_km_h' : runs.groupby([runs.start_time.dt.year, 
                                    runs.start_time.dt.month])['km_per_h'].mean(),
              })

per_month.index.names = ['year', 'month']

per_month

Die folgende Alternative zeigt auch Monate ohne Aktivität an (nützlich zum Plotten):

freq = runs.set_index('start_time').groupby(pd.Grouper(freq="M"))

freq_month = pd.DataFrame({
              'count': freq['distance'].count(),
              'total_distance': freq['distance'].sum(),
              'distance_median': freq['distance'].median(),   
              'distance_mean': freq['distance'].mean(),  
              'distance_max': freq['distance'].max(),    
              'total_ascent': freq['ascent'].sum(),
              'ascent_median': freq['ascent'].sum(),
              'ascent_max': freq['ascent'].max(),
              'median_km_h' : freq['km_per_h'].median(),
              'mean_km_h' : freq['km_per_h'].mean(),
              })

freq_month.index.name = 'month_dt'
freq_month['year'] = freq_month.index.year
freq_month['month'] = freq_month.index.month

freq_month

Plot: freq_month['count'].plot(kind='bar')

Plots mit Seaborn

Nun können Sie Ihre Kenntnisse der Datenvisualisierung ausleben. In diesem Beitrag zeige ich nur einige Beispiele, viele weitere finden Sie im Jupyter-Notebook.

Profile der GPS-Tracks:

sns.relplot(x="distance", y="ele", data=lines, kind="line", hue="track_name");

Sie können auch eine normalisierte Version ausprobieren, bei der alle Tracks bei Null beginnen:

sns.relplot(x="distance", y="ele_normalized", data=lines, kind="line", hue="track_name");

Hängt die Geschwindigkeit mit der Steigung zusammen? (Beachten Sie, dass ich unrealistisch hohe Geschwindigkeiten herausfiltere)

sns.relplot(x="slope", y="km_per_h", data=lines[lines['km_per_h']<30], hue="track_name");

Eine heatmap der Geschwindigkeit:

heatmapdata1 = lines.reset_index()
heatmapdata1 = heatmapdata1.pivot(index='track_name', columns='segment_id', values='km_per_h')
heatmapdata1.fillna(value=0, inplace=True)

# Set vmax to filter out unrealistic values
sns.heatmap(heatmapdata1, vmin=0, vmax=20, xticklabels=False)

Eine Heatmap von ele_delta ist ebenfalls interessant. Ich setze Minimal- und Maximalwerte, ansonsten wäre das Ergebnis fast komplett schwarz.

heatmapdata2 = lines.reset_index()
heatmapdata2 = heatmapdata2.pivot(index='track_name', columns='segment_id', values='ele_delta')
heatmapdata2.fillna(value=0, inplace=True)
sns.heatmap(heatmapdata2, xticklabels=False, center=0, vmin=-2, vmax=2)

Ein Histogramm der Länge der Läufe:

sns.displot(x="distance", data=runs, binwidth=1);

Versuchen Sie auch Geschwindigkeit vs. Länge

sns.relplot(x="distance", y="km_per_h", data=runs, hue="track_name")

Geschwindigkeit (und Länge) vs. Datum:

import matplotlib.dates as mdates

g = sns.relplot(x="start_time", y="km_per_h", data=runs, size="distance", sizes=(2,300))
g.ax.set_xlabel("date")
g.ax.set_ylabel("km/h")
g.ax.xaxis.set_major_formatter(
    mdates.ConciseDateFormatter(g.ax.xaxis.get_major_locator()))


und:

g = sns.relplot(x="start_time", y="distance", data=runs, size="km_per_h")
g.ax.set_xlabel("date")
g.ax.set_ylabel("distance")
g.ax.xaxis.set_major_formatter(
    mdates.ConciseDateFormatter(g.ax.xaxis.get_major_locator()))

Länge vs. Minuten:

sns.jointplot(x="distance", y="minutes", data=runs, kind="reg")

Wir können auch die Monatsberichte plotten. Für schönere Diagramme ersetze ich Jahr und Monat durch einen Datetime-Index.

per_month_dt = per_month.reset_index()
per_month_dt['month'] =  pd.to_datetime(per_month_dt['year'].astype('str') + '-' + per_month_dt['month'].astype('str') + '-1')
per_month_dt.drop(columns='year', inplace=True)
per_month_dt.set_index('month', inplace=True)

Und jetzt können Sie die monatliche Gesamtstrecke zeichnen:

g = sns.relplot(x="month", y="total_distance", data=per_month_dt)
g.ax.xaxis.set_major_formatter(
    mdates.ConciseDateFormatter(g.ax.xaxis.get_major_locator()))

Oder den Median der Geschwindigkeit für jeden Monat:

g = sns.relplot(x="month", y="median_km_h", data=per_month_dt)
g.ax.xaxis.set_major_formatter(
    mdates.ConciseDateFormatter(g.ax.xaxis.get_major_locator()))

Interaktive Karte mit Folium

Schließlich können wir Folium verwenden, um alle GPS-Tracks auf einer interaktiven Karte darzustellen (siehe Screenshot am Anfang meines Beitrags). Die Karte hat Tooltips, die die tatsächliche Geschwindigkeit, Kilometerzahl, Zeit usw. jedes Streckenabschnitts anzeigen. Die Läufe eines jeden Jahres können ein- und ausgeschaltet werden, und man kann eine von zwei verschiedenen Basiskarten wählen.

Für aussagekräftige Tooltips muss ich die tatsächlichen Streckenabschnitte (lines) anstelle der kompletten Läufe (runs) verwenden. Zuerst erstelle ich einen bereinigten GeoDataFrame ohne Datetime (sonst versagt Folium) und mit gerundeten Werten (für schönere Tooltips).

lines_condensed = lines[['ele_delta', 'dist_delta', 'geometry', 'distance', 'km_per_h', 'min_per_km', 'minutes', 'slope', 'time_delta_s']].dropna().copy()

lines_condensed['date'] = lines['time'].dt.strftime("%d %B %Y")
lines_condensed['year'] = lines['time'].dt.year
lines_condensed['month'] = lines['time'].dt.month

lines_condensed.reset_index(level=1, inplace=True)
lines_condensed['total_distance'] = runs['distance']
lines_condensed['total_minutes'] = runs['minutes']

lines_condensed['distance'] = lines_condensed['distance']/1000
lines_condensed['distance'] = lines_condensed['distance'].round(2)
lines_condensed['total_distance'] = lines_condensed['total_distance'].round(2)
lines_condensed['total_minutes'] = lines_condensed['total_minutes'].round(2)
lines_condensed['minutes'] = lines_condensed['minutes'].round(2)
lines_condensed['min_per_km'] = lines_condensed['min_per_km'].round(2)
lines_condensed['km_per_h'] = lines_condensed['km_per_h'].round(2)
lines_condensed['slope'] = lines_condensed['slope'].round(3)

Wir brauchen eine Style-Funktion, und ich weise jedem Lauf eine zufällige Farbe zu.

# style function
def style(feature):
        return {
            # 'fillColor': feature['properties']['color'],
            'color': feature['properties']['color'],
            'weight': 3,
        }


for x in lines_condensed.index:
    color = np.random.randint(16, 256, size=3)
    color = [str(hex(i))[2:] for i in color]
    color = '#'+''.join(color).upper()
    lines_condensed.at[x, 'color'] = color

Ich verwende den letzten (jüngsten) Standort als Zentrum der Karte.

location_x = lines_condensed.iloc[-1]['geometry'].coords.xy[0][0]
location_y = lines_condensed.iloc[-1]['geometry'].coords.xy[1][0]

Grupieren nach Jahr:

grouped = lines_condensed.groupby('year')

Endlich können wir die Karte plotten. (Die FeatureGroups habe ich her erklärt.)

m = folium.Map(location=[location_y, location_x], zoom_start=13, tiles='cartodbpositron')
folium.TileLayer('Stamen Terrain').add_to(m)

# Iterate through the grouped dataframe
# Populate a list of feature groups
# Add the tracks to the feature groups
# And add the feature groups to the map

f_groups = []

for group_name, group_data in grouped:
    f_groups.append(folium.FeatureGroup(group_name))
    track_geojson = folium.GeoJson(data=group_data, style_function=style).add_to(f_groups[-1])
    track_geojson.add_child(
          folium.features.GeoJsonTooltip(fields=['date', 'distance', 'total_distance', 'minutes', 'total_minutes', 'min_per_km', 'km_per_h' ], 
                                   aliases=['Date', 'Kilometers', 'Total km', 'Minutes', 'Total min', 'min/km', 'km/h'])
        )
    f_groups[-1].add_to(m)

folium.LayerControl().add_to(m)

m

Heatmap mit Folium

Folium kann auch eine interaktive Heatmap plotten. Das Plugin heatmap benötigt eine Liste von Orten, die wir aus unserem GeoDataFrame mit den Punkten erhalten können. Es lohnt sich, mit den Parametern zu spielen, vor allem radius und blur.

# Create a list of the locations from points
locations = list(zip(points['geometry'].y, points['geometry'].x))

hm = folium.Map(tiles='cartodbdark_matter')

# Add heatmap to map instance
# Available parameters: HeatMap(data, name=None, min_opacity=0.5, max_zoom=18, max_val=1.0, radius=25, blur=15, gradient=None, overlay=True, control=True, show=True)
folium.plugins.HeatMap(locations).add_to(hm)

hm

Runkeeper GPS-Tracks mit Python analysieren (Teil 1)

Einen GPS-Track (GPX-Datei) mit Python untersuchen und auf eine interaktive Karte plotten

Fitness-Tracking-Apps wie Runkeeper sammeln eine Menge interessanter Daten. Mit ein paar Python-Kenntnissen können die Läufe noch detaillierter ausgewertet werden als in der App. Im ersten Teil dieser Serie werde ich einen einzelnen GPS-Track (GPX-Datei) mit Python untersuchen, dabei verwende ich Geopandas und Folium. Manche Ideen stammen von diesem Blogbeitrag, aber ich habe einen eleganteren Weg gefunden, ohne durch den GeoDataFrame zu iterieren. Außerdem füge ich nützliche Tooltips zur Karte hinzu. Im zweiten Teil ändere ich den Code, um einen GeoDataFrame mit allen meinen GPS-Tracks zu erhalten. Dieser eignet sich hervorragend zum Plotten und Analysieren der Läufe. Teil 2 enthält ein Jupyter-Notebook, das Sie mit Ihren eigenen GPS-Tracks verwenden können.

Download the GPS Tracks

Um die mit Runkeeper aufgenommenen GPS-Tracks herunterzuladen, muss man sich auf https://runkeeper.com/ einloggen. Dann auf das Zahnrad-Icon klicken, „Account Settings“ wählen und dann „Export Data“. Man bekommt ein ZIP, das die GPS-Tracks als einzelne GPX-Dateien enthält. (Vor ein paar Monaten musste ich 2 Tage warten, bis der endgültige Download-Link erschienen ist. Letzte Woche funktionierte es sofort.)

GPX-Dateien in Geopandas öffnen

Geopandas kann GPX-Dateien öffnen und parsen (dabei wird GDAL verwendet). Um einen GeoDataFrame mit allen Punkten entlang des Tracks zu bekommen:

import pandas as pd
import geopandas as gpd
import numpy as np
from shapely.geometry import LineString

file = "gpx/2021-01-16-110027.gpx"

gdf = gpd.read_file(file, layer='track_points')
gdf.head()


Zu Höhenangaben von Runkeeper siehe Mit Python einem GPS-Track die Höhe hinzufügen. Wir sehen viele Spalten mit „None“-Werten, die wir löschen können. Beachten Sie die Spalten „track_fid“, „track_seg_id“ und „track_seg_point_id“: Sie werden von GDAL verwendet, um verschiedene Tracks, Segmente und Trackpoints zu identifizieren, die sich in einer einzelnen GPX-Datei befinden können. Runkeeper beginnt nach einer Pause ein neues Track-Segment, daher können wir nicht einfach track_seg_point_id als Index verwenden. Ich werde den von Geopandas erstellten Index als „point_id“ benennen und die anderen Spalten löschen. Das Ergebnis enthält die Pausen. Ich wandle die Zeit zu Datetime.

# Better use the geodataframe index as point_id
gdf.index.names = ['point_id'] 
# And turn time to Datetime
gdf['time'] = pd.to_datetime(gdf['time'])

# Drop useless columns
gdf.dropna(axis=1, inplace=True)
gdf.drop(columns=['track_fid', 'track_seg_id', 'track_seg_point_id'], inplace=True)

gdf.head()

Informationen aus dem GPS-Track extrahieren

Wie lange hat der Lauf gedauert (einschließlich Pausen)?

total_time = gdf.iloc[-1]['time'] - gdf.iloc[0]['time']
total_time

Timedelta(‚0 days 00:42:26‘)

Jetzt berechnen wir die Entfernung von Punkt zu Punkt. Zunächst müssen wir die Geometrie auf UTM umstellen, um Koordinaten in Metern statt in Grad zu erhalten. Mit neueren Versionen von geopandas und pyproj ist es einfach, die beste UTM-Zone zu ermitteln:

gdf = gdf.to_crs(gdf.estimate_utm_crs())

Jetzt erstelle ich einen zweiten GeoDataFrame, dessen Index um -1 verschoben ist. Mit diesem Trick ist es einfach, die Entfernung von einem Punkt zum nächsten zu berechnen.

shifted_gdf = gdf.shift(-1)
gdf['time_delta'] = shifted_gdf['time'] - gdf['time']  
gdf['dist_delta'] = gdf.distance(shifted_gdf)

Hinweis: Die letzte Zeile von shifted_gdf besteht aus NaN-Werten. Daher erhalten wir für die letzte Zeile von gdf auch NaN als distance (vom letzten Trackpoint gibt es keinen Abstand zum nächsten Trackpoint).

Nun können wir die Geschwindigkeit in verschiedenen Formaten berechnen:

# speed in various formats
gdf['m_per_s'] = gdf['dist_delta'] / gdf.time_delta.dt.seconds 
gdf['km_per_h'] = gdf['m_per_s'] * 3.6
gdf['min_per_km'] = 60 / (gdf['km_per_h'])

Die zurückgelegte Strecke (in Metern) und die verstrichene Zeit:

gdf['distance'] = gdf['dist_delta'].cumsum()
gdf['time_passed'] = gdf['time_delta'].cumsum()

Weitere nützliche Spalten:

# Minutes instead datetime might be useful
gdf['minutes'] = gdf['time_passed'].dt.seconds / 60

# Splits (in km) might be usefull for grouping
gdf['splits'] = gdf['distance'] // 1000

# ascent is elevation delta, but only positive values
gdf['ele_delta'] = shifted_gdf['ele'] - gdf['ele']  
gdf['ascent'] = gdf['ele_delta']
gdf.loc[gdf.ascent < 0, ['ascent']] = 0

# Slope in %
# (since ele_delta is not really comparable)
gdf['slope'] = 100 * gdf['ele_delta'] / gdf['dist_delta']

# Ele normalized: Startpoint as 0
gdf['ele_normalized'] = gdf['ele'] - gdf.loc[0]['ele']

# slope and min_per_km can be infinite if 0 km/h
# Replace inf with nan for better plotting
gdf.replace(np.inf, np.nan, inplace=True)
gdf.replace(-np.inf, np.nan, inplace=True)

# Note the NaNs in the last line
gdf.tail()

Einfache Statistik:

gdf.describe()

Der (gesamte) Aufstieg in Höhenmetern:

gdf['ascent'].sum()

Wir können die Geometrie wieder zu WGS84 konvertieren. Dies ist besonders dann sinnvoll, wenn wir Tracks aus verschiedenen Regionen vergleichen wollen (Teil 2).

gdf = gdf.to_crs(epsg = 4326)
shifted_gdf = shifted_gdf.to_crs(epsg = 4326)

Jetzt sind Ihre Visualisierungsfähigkeiten gefragt. Z.B. ein einfaches Profil:

sns.relplot(x="distance", y="ele", data=gdf, kind="line")
plt.ylim(0, 50)

Anmerkung: Ich habe für die y-Achse Grenzen gesetzt, da Berlin ziemlich flach ist und ein geringfügiges Auf und Ab stark übertrieben wäre.

Die Punkte mit Linien verbinden

Wir wollen keine Punkte auf der Karte darstellen, sondern die Linien, die diese Punkte verbinden. Ich erstelle einen weiteren GeoDataFrame mit Linien anstelle von Punkten als Geometrie. Beachten Sie, dass dadurch Informationen wie Zeit und Höhe des letzten Punktes wegfallen. Die Lambda-Funktion verwendet LineString (aus dem Modul shapely), um eine Verbindungslinie zu erstellen. Dann verwenden wir die Linie als neue Geometrie des GeoDataFrame.

lines = gdf.iloc[:-1].copy() # Drop the last row
lines['next_point'] =  shifted_gdf['geometry']
lines['line_segment'] = lines.apply(lambda row: LineString([row['geometry'], row['next_point']]), axis=1) 

lines.set_geometry('line_segment', inplace=True, drop=True)
lines.drop(columns='next_point', inplace=True)
lines.index.names = ['segment_id'] 

Wie unterscheiden sich die Splits?

Unsere Spalte "splits" (Abschnitte von 1 km Länge) ist praktisch, um Geschwindigkeit, Steigung usw. entlang der Strecke zu vergleichen. Bin ich am Ende müde geworden? Der folgende Code erstellt eine Tabelle mit vielen interessanten Details für jeden Split, z. B. Mittelwert, Median und Standardabweichung der Geschwindigkeit, Steigung und die für den Split benötigte Zeit.

lines.groupby('splits').aggregate(
     {'km_per_h': ['mean', 'median', 'max', 'std'],
     'min_per_km': ['mean', 'median', 'max', 'std'],
     'ascent': 'sum',
     'time_delta' : 'sum' })

Karte mit Folium

Mit Folium können wir eine interaktive Karte unseres Tracks zeichnen, einschließlich einem praktischen Tooltip mit Informationen zu jedem Linienabschnitt (siehe Screenshot am Anfang dieses Beitrags).

import folium
import branca.colormap as cm
from folium import plugins

# The approx. center of our map
location = lines.dissolve().convex_hull.centroid

Ich möchte Markierungen für jeden Kilometer einzeichnen. Wir können den ersten Punkt eines jeden Splits verwenden.

splitpoints = gdf.groupby('splits').first()

Folium mag keine Zeitstempel, diese Spalten müssen gelöscht werden. Ich entferne auch Zeilen mit der Geschwindigkeit NaN.

lines_notime = lines.drop(columns=['time', 'time_delta', 'time_passed'] )
lines_notime.dropna(inplace=True)

Werte runden, für hübschere Tooltips:

lines_notime['distance'] = (lines_notime['distance']/1000).round(decimals=3)
lines_notime['km_per_h'] = lines_notime['km_per_h'].round(decimals=1)
lines_notime['minutes'] = lines_notime['minutes'].round(decimals=2)
lines_notime['slope'] = lines_notime['slope'].round(decimals=2)

Und schließlich können wir die Karte zeichnen, wobei die Geschwindigkeit als Farbe dargestellt wird. Die Style-Funktion weist jedem Liniensegment Farbe und Tooltip zu. Für die Marker (die "Meilensteine" der Splits) verwende ich folium.plugins.BeautifyIcon, dieses Plugin kann Zahlen auf den Marker plotten.

m = folium.Map(location=[location.y, location.x], zoom_start=15, tiles='cartodbpositron')

# Plot the track, color is speed
max_speed = lines['km_per_h'].max()
linear = cm.LinearColormap(['white', 'yellow', 'red'], vmin=0, vmax=max_speed)
route = folium.GeoJson(
     lines_notime,                         
     style_function = lambda feature: {
            'color': linear(feature['properties']['km_per_h']),
            'weight': 3},
            tooltip=folium.features.GeoJsonTooltip(
            fields=['distance', 'ele', 'km_per_h', 'minutes', 'slope'], 
            aliases=['Distance', 'Elevation', 'Speed', 'Minutes', 'Slope'])
     ) 

m.add_child(linear)
m.add_child(route) 

# Add markers of the splitpoints
for i in range(0,len(splitpoints)):
    folium.Marker(
        location=[splitpoints.iloc[i].geometry.y, splitpoints.iloc[i].geometry.x],
       # popup=str(i),
        icon=plugins.BeautifyIcon(icon="arrow-down",
                                  icon_shape="marker", 
                                  number=i, 
                                  border_color= 'white', 
                                  background_color='lightgreen',
                                  border_width=1)
    ).add_to(m)

m

... und wir bekommen die ganz oben gezeigte Karte.

Zur nächsten Stufe

Es ist schön, einen einzigen GPS-Track auszuwerten. Aber es wäre noch schöner, die Daten aller GPS-Tracks zu analysieren. Ich werde dies im zweiten Teil tun.

JupyterLab Spickzettel

Markdown Syntax

# Überschrift 1
## Überschrift 2 etc.

Hyperlink: [Link Text](Link URL)

**fette Schrift**
*kursive Schrift*
> blockquote

Geordnete Liste:
1. Bla
2. Bla

Ungeordnete Liste:
– Bla
– Bla

`Code steht in Akzenten`

Bild: ![alt text](image.jpg)

Siehe auch: Extended Synthax. Zum Beispiel:

~~Durchgestrichen~~
Tiefgestellt: H~2~O
Hochgestellt: H^2^

Code-Block mit Syntax Highlighting (das sind Akzente):

```python
s = "das ist python code"
print(s)
```

Latex in Markdown

Im Absatz steht die Latex-Formel $5-4\alpha$ in Dollarzeichen.

Freigestellte Formel:

$$f(x) = \sum\limits_1^k x^2 – \frac{1}{x-1}$$

Neues conda environment in JupyterLab anzeigen

Im environment ipykernel installieren:

(my-env)$ conda install ipykernel

Den Kernel registrieren:

(my-env)$ ipython kernel install --user --name=my-env

Im Browser den Tab mit JupyterLab neu laden, dann wird der Kernel angezeigt.

Einen Kernel aus der Liste entfernen:

jupyter kernelspec uninstall my-env

Extensions

Ich verwende folgende Plugins (per conda installieren, nicht in Jupyterlab):

Mit Python einem GPS-Track die Höhe hinzufügen

Wie kann einer GPX-Datei die Höhe hinzugefügt werden? Mit Python ist das dank elevation, geopandas, rasterio und den freien SRTM-Daten relativ leicht.

Leider sind GPS-Geräte ziemlich ungenau, was die Höhe angeht: Der vertikale Fehler ist etwa bei ± 15 m (bei einem guten GPS-Signal und freie Sicht auf den Himmel). Für ein Profil entlang eines GPS-Tracks ist die gemessene Höhe unbrauchbar. Tracking-Apps wie Runkeeper umgehen das Problem, indem sie jeden Punkt an einen Webservice schicken, der die Höhe „von der Karte“ zurückgibt, welche dann von der App in die GPX-Datei eingetragen wird (siehe auch Runkeeper GPS-Tracks mit Python analysieren Teil 1 und Teil 2). Wer ungenaue Höhendaten einer GPX-Datei ersetzen oder die fehlende Höhe ergänzen will, kann das aber auch leicht mit Python erledigen. Basierend auf den frei zugänglichen SRTM-Daten können wir leicht die Höhe von Punkten ermitteln und das Ergebnis als GPX speichern.

Die SRTM-Daten wurden von einem Space Shuttle aus per Radar gemessen. Das Modul elevation lädt die Daten unseres Gebiets aus dem Netz herunter und erstellt aus den einzelnen Kacheln eine einzige Datei, die als Geotiff abgespeichert wird. Das ist einfach ein TIF-Bild mit Georeferenz in den Metadaten. Es hat (wie ein Graustufen-Bild) nur einen Kanal und der Wert jedes Pixels ist die Höhe in Meter. Die Auflösung der Datei beträgt 1 Bogensekunde, also ungefähr 1 Pixel alle 30 m. Das Geotiff mit unserem DEM (digital elevation model) können wir mit rasterio öffnen und dann den Wert einzelner Koordinaten auslesen.

Vorbereitung

import pandas as pd
import geopandas as gpd
import numpy as np
import os
import elevation
import rasterio

track_in = 'gpx/2021-05-22-163014.gpx' 
track_out = 'test.gpx'

dem_file = '2021-05-22-163014.tif' 
demfolder = "dem" # Wo das DEM gespeichert wird

GPX-Track mit Geopandas öffnen

Wir können einfach Geopandas verwenden, um die GPX-Datei zu öffnen (siehe auch hier).

gdf = gpd.read_file(track_in, layer='track_points')

Dieser Befehl verwendet intern GDAL. Mit layer='track_points' bekommen wir alle Trackpoints des Tracks, für Wegpunkte bräuchten wir layer='waypoints'.

GDAL gibt einige nutzlose Spalten mit „None“-Werten zurück, wir können sie entfernen. Und ‚time‘ sollte in datetime sein (wichtig, wenn wir als GPX speichern wollen).

gdf.dropna(axis=1, inplace=True)
gdf['time'] = pd.to_datetime(gdf['time'])

gdf.head()

Zu den Spalten ‚track_fid‘ und ‚track_seg_id‘: Sie werden von GDAL verwendet, um verschiedene Tracks und Tracksegmente zu referenzieren, die eventuell in der GPX-Datei enthalten sind. Diese ID-Werte sind in der GPX-Datei nicht zu finden, wenn wir sie in einem Texteditor öffnen. Es ist jedoch wichtig, diese Spalten zu behalten, um den Track wieder in eine GPX-Datei speichern zu können.

Wir brauchen auch die Grenzen unserer Region. Ich füge etwas Rand hinzu, um das DEM für andere Aufgaben in der Region verwenden zu können.

# Get the bounds of the track
minx, miny, maxx, maxy = gdf.dissolve().bounds.loc[0]

# You might want to add some margin if you want to use the DEM for other tracks/tasks
minx, miny, maxx, maxy = bounds  = minx - .05, miny - .05, maxx + .05, maxy + .05

Das DEM mit elevation herunterladen

# Make sure the dem folder does exist 
# (otherwise the module elevation fails)
if not os.path.exists(demfolder):
    os.mkdir(demfolder)

# And use an absolute path, 
# the module elevation does not work with relative paths.    
dem_file = os.path.join(os.getcwd(), demfolder, dem_file)

# Download DEM 
elevation.clip(bounds=bounds, output=dem_file)

# Clean temporary files
elevation.clean()

Das DEM mit rasterio öffnen und sampeln

Die Datei mit rasterio öffnen und die Daten des ersten (und einzigen) Kanals in ein Numpy-Array laden:

dem_data = rasterio.open(dem_file)
dem_array = dem_data.read(1)

Hinweis: Unser DEM verwendet bereits dasselbe CRS wie unsere GPS-Daten (WGS84 = EPSG 4326), wir brauchen also keine Reprojektion. Wir können das CRS und die Größe des DEMs überprüfen: dem_data.crs, dem_data.width, dem_data.height, dem_data.bounds. Die Anzahl der Kanäle gibt dem_data.count.

Das Wichtigste passiert in der nächsten Zeile: dem_data.index() nimmt die Koordinaten (Längengrad, Breitengrad) und gibt die dazugehörigen Indexwerte unseres Arrays zurück. Wir können diese Indexwerte verwenden, um den Wert aus dem Array zu erhalten. Hinweis: GPX verwendet das Tag <ele> </ele> für Höhe, ‚ele‘ muss der Spaltenname sein. Und wir erhalten ein int, aber es muss ein float sein, um den GeoDataFrame als GPX-Datei zu speichern.

gdf['ele'] = dem_array[dem_data.index(gdf['geometry'].x, gdf['geometry'].y)]

# Note that we get ele values as int. 
# To save as GPX, ele must be as float.
gdf['ele'] = gdf['ele'].astype(float)

gdf.head()

Natürlich sind unsere Daten noch in 1-m-Schritten und ein Profil sieht entsprechend stufig aus (siehe unten). Ich habe mein Ergebnis mit der von Runkeeper angegebenen Höhe verglichen: Es gibt nur geringe Unterschiede. Ich vermute, dass sie auch die SRTM-Daten verwenden, aber interpolieren (und einen leichten Weichzeichner einsetzen, um das DEM zu glätten?). Wir können das auch, siehe unten. Aber erst einmal speichern wir unsere Daten als GPX-Datei.

GeoDataFrame als GPX speichern

Geopandas macht es sehr einfach, unseren GeoDataFrame als GPX-Datei zu speichern:

gdf.to_file(track_out, 'GPX', layer='track_points')

Wichtig: Das funktioniert nicht, wenn die Datei bereits existiert! Sie müssen ggf. zuerst eine bestehende Datei löschen.

Wenn wir mit Wegpunkten gearbeitet haben, müssen wir layer='waypoints' verwenden. (Nicht vergessen: ‚time‘ muss in datetime sein, und um die Punkte als Track zu speichern, benötigen wir ‚track_fid‘ und ‚track_seg_id‘.)

Extra: Ein Blick auf das DEM

Wir können auch einen kurzen Blick auf unser DEM werfen. Verwandeln wir es in eine Karte und zeichnen die Strecke ein:

import matplotlib.pyplot as plt

vmin = dem_array.min()
vmax = dem_array.max()
extent = minx, maxx, miny, maxy

fig, ax = plt.subplots()
cax = plt.imshow(dem_array, extent=extent, 
                  cmap='Spectral_r', vmin=vmin, vmax=vmax)
fig.colorbar(cax, ax=ax)
gdf.plot(ax=ax, markersize=1, zorder=11)

Ein histplot der Höhen:

import seaborn as sns
sns.displot(dem_array.ravel())

Glätten und skalieren des DEM

Um ein glatteres Profil entlang der Strecke zu erhalten, können wir unser DEM glätten und hochskalieren. Hinweis: Die hochskalierten Daten sind nicht unbedingt genauer. Zuerst müssen wir unser Array in Float umwandeln. Dann können wir einen leichten Weichzeichner anwenden, um die Stufen im Terrain zu glätten. Natürlich sollten die gerundeten Werte immer noch mit den ursprünglichen int-Werten übereinstimmen (sigma=0,33 funktioniert in meinem Beispiel gut, der Wert muss ggf. angepasst werden).

dem_array_float = dem_array.astype(float)

from scipy import ndimage

# Blur to reduce the steps in the DEM.
# With sigma 0.33 I am still very close to the original.
dem_array_float = ndimage.gaussian_filter(dem_array_float, sigma=0.33)

Jetzt können wir unser Array hochskalieren, um alle paar Meter ein Pixel zu erhalten. Hinweis: scipy.ndimage.zoom() mit order=3 (Standard) verwendet kubische Interpolation. Wir brauchen auch eine neue Transformationsmatrix. Sie wird von rasterio verwendet, um den Koordinaten (Breitengrad, Längengrad) die jeweiligen Pixelindizes zuzuweisen.

upscale_factor = 5

# Upscale the array
dem_array_float = ndimage.zoom(dem_array_float, upscale_factor).round(decimals=1)


# Scale the transform matrix
transform = dem_data.transform * dem_data.transform.scale(
        (dem_data.width / dem_array_float.shape[-1]),
        (dem_data.height / dem_array_float.shape[-2])
    )

Jetzt können wir die Höhendaten für unsere Trackpunkte ermitteln:

gdf['ele_resample'] = dem_array_float[rasterio.transform.rowcol(transform, gdf['geometry'].x, gdf['geometry'].y)]

Wir können ein Profil zeichnen (nachdem wir die Entfernung entlang der Strecke berechnet haben):

Die Abbildung zeigt einen Teil meines Tracks: blau unter Verwendung der Höhe des ursprünglichen DEM mit int-Werten, orange mit dem geglätteten und hochskalierten DEM. Da ich einen Runkeeper-Track verwendet habe, kann ich die Differenz zwischen meinen Modellen und der von Runkeeper angegebenen Höhe berechnen. Mit describe() bekomme ich:

Mit dem hochskalierten DEM liegt meine Höhe bei mehr als 50 % aller Trackpoints innerhalb von ± 1 m des von Runkeeper angebenen Wertes. Ziemlich beeindruckend! Es gibt einige Ausreißer, aber ich weiß nicht, welches Modell näher am realen Gelände ist.

Folium und Geopandas: FeatureGroup für kategorische Daten

Wie kategorische Daten auf einer mit Folium erstellten Karte einzelnen Ebenen (FeatureGroup) zugeordnet werden können.

Folium ist eine Python-Bibliothek zum Erstellen von interaktiven Karten. Die geplotteten Marker oder Polygone können mit folium.FeatureGroup() einzelnen Ebenen zugeordnet werden, die mit einem Mausklick an- und ausgeschaltet werden können.

Alle Beispiele, die ich im Netz gefunden habe, definieren jede einzelne Ebene explizit mit einem Befehl wie:

 feature_group1 = FeatureGroup(name='Foo')

Wenn ich kategorische Daten in einem Geopandas.GeoDataFrame habe, dann möchte ich sicher nicht per Hand für jede Kategorie eine Ebene erstellen. Zum Glück können wir das automatisieren, indem wir jede initialisierte FeatureGroup einfach an eine Liste anhängen. Hier ein Beispiel mit Vulkanen.

Ein kurzer Blick auf die Daten:

 volcanoes.info()


RangeIndex: 1233 entries, 0 to 1232
Data columns (total 13 columns):
 #   Column             Non-Null Count  Dtype   
---  ------             --------------  -----   
 0   number             1233 non-null   int64   
 1   name               1233 non-null   object  
 2   country            1233 non-null   object  
 3   vtype              1233 non-null   object  
 4   evidence           1233 non-null   object  
 5   last_eruption      1233 non-null   object  
 6   region             1233 non-null   object  
 7   subregion          1233 non-null   object  
 8   elevation          1233 non-null   int64   
 9   rocks              1198 non-null   object  
 10  tectonic_setting   1230 non-null   object  
 11  last_eruption_int  787 non-null    float64 
 12  geometry           1233 non-null   geometry
dtypes: float64(1), geometry(1), int64(2), object(9)
memory usage: 125.4+ KB

Ich möchte nun eine Karte, die nach Gesteinstyp gruppiert ist.

grouped = volcanoes.groupby('rocks')

Da die Farbpaletten von Seaborn besonders schön sind, erzeuge ich damit eine Liste mit Farben im HTML-Hex-Format. Die Anzahl der benötigten Farben ist len(grouped).

import seaborn as sns
pal = sns.color_palette("husl", len(grouped)).as_hex() 

Da ich in diesem Beispiel nicht möchte, dass auch die Basiskarte (das Tilelayer) im LayerControl angezeigt wird, initialisiere ich die Karte ohne tiles und füge ein TileLayer mit control=False hinzu:

m = folium.Map(tiles=None)
folium.TileLayer('cartodbpositron', control=False).add_to(m)

Nun iterieren wir durch den gruppierten GeoDataFrame. Für jede Kategorie wird eine FeatureGroup erzeugt, die an eine Liste angehängt wird. Dann werden Marker für entsprechende Vulkane der zuletzt initialisierten FeatureGroup hinzugefügt.

f_groups = []

for group_name, group_data in grouped:
    f_groups.append(folium.FeatureGroup(group_name))
    color = pal.pop()
    
    for i in range(0,len(group_data)):

        # html for popup of markers
        html=f"""
            <h2>  {group_data.iloc[i]['name']} </h2>
            <small>
            <p> Country: {group_data.iloc[i]['country']}  <br/>
            Elevation: {group_data.iloc[i]['elevation']}  <br/>
            Last Eruption: 
            {group_data.iloc[i]['last_eruption']}  <br/>
            Rocks: {group_data.iloc[i]['rocks']}  <br/>
            Tectonic Setting: 
            {group_data.iloc[i]['tectonic_setting']}  </p>
            </small>
            """

        iframe = folium.IFrame(html=html, width=300, height=200)
        popup = folium.Popup(iframe, max_width=650)

        # Add markers to last FeatureGroup    
        folium.CircleMarker(
            location=[group_data.iloc[i].geometry.y, 
                      group_data.iloc[i].geometry.x],
            radius=5,
            popup=popup,
            tooltip=group_data.iloc[i]['name'],
            fill_color=color,
            stroke = False, 
            fill_opacity = 1,
        ).add_to(f_groups[-1])

    # Add last featureGroup to Map
    f_groups[-1].add_to(m)

folium.LayerControl().add_to(m)

m