set.seed(42)
a <- runif(3)
set.seed(42)
b <- runif(3)
round(a, 4)[1] 0.9148 0.9371 0.2861
identical(a, b)[1] TRUE
round(lehmer(12345, 3), 6)[1] 0.096617 0.833995 0.947702
file.path beziehungsweise pathlib gebaut.Vorwissen: R-Grundlagen oder Python-Grundlagen, je nachdem, womit gearbeitet wird. Wie man die Paketstände festhält, steht unter Pakete und Umgebungen, wie man Änderungen nachvollziehbar speichert, unter Git.
| Stufe | Frage | Mittel |
|---|---|---|
| wiederholbar | liefert dasselbe Skript auf demselben Rechner dasselbe? | Seed, Neustart statt gewachsener Sitzung |
| übertragbar | läuft es auf einem anderen Rechner? | Projektordner, relative Pfade |
| dauerhaft | läuft es in einem Jahr noch? | festgehaltene Paketversionen, Container |
| nachvollziehbar | versteht jemand anders, was passiert ist? | Bericht mit eingebettetem Code, Versionsverwaltung |
Ein Skript, das bei jemand anderem nicht läuft, scheitert erfahrungsgemäss an einem absoluten Pfad wie C:/Users/mv/Desktop/Analyse/daten.csv. Er verweist auf ein Verzeichnis, das nur auf einem Rechner existiert. Ein RStudio-Projekt, ein Projektordner in VS Code oder das Verzeichnis eines Quarto-Projekts legt das Arbeitsverzeichnis auf den Projektordner fest; Pfade werden relativ dazu geschrieben und funktionieren überall, wo der Ordner als Ganzes liegt. Wer Pfade mit file.path() oder pathlib zusammensetzt statt mit Schrägstrichen, muss sich zudem nicht um Windows und Linux kümmern.
RStudio bietet an, die Objekte beim Beenden zu speichern und beim Start wiederherzustellen. Das wirkt hilfreich und verdeckt Fehler: Ein Skript, das nur läuft, weil eine Variable noch aus der letzten Woche im Speicher liegt, ist nicht reproduzierbar. Die Einstellung gehört ausgeschaltet (Tools → Global Options → Workspace). In Jupyter entsteht dasselbe Problem, wenn Zellen in beliebiger Reihenfolge ausgeführt werden. Die Probe ist in beiden Fällen dieselbe: Neustart mit leerer Umgebung und ein vollständiger Durchlauf von oben.
Eine Zahl, die von Hand aus der Konsole in ein Textdokument kopiert wurde, ist ab dem nächsten Datenstand falsch, und niemand sieht ihr das an. Quarto und R Markdown lösen das, indem Text und Rechnung in derselben Datei stehen. Beim Rendern wird der Code ausgeführt und das Ergebnis eingesetzt, in Chunks und mitten im Satz:
Diese Sammlung arbeitet genau so: Jede Ausgabe entsteht beim Bauen der Website aus dem Code, der daneben steht. Die Zahlen im Fliesstext werden danach gegen die Ausgabe geprüft, weil ein Satz, der eine Zahl von Hand nennt, wieder veralten kann.
Ausgaben in einem Jupyter-Notebook erzeugen bei jedem Speichern grosse, unleserliche Änderungen in Git. Wer sie vor dem Commit entfernt (jupyter nbconvert --clear-output --inplace analyse.ipynb), bekommt brauchbare Diffs. Quarto-Dateien (.qmd) haben das Problem nicht, weil sie reiner Text sind und die Ausgabe getrennt entsteht.
| Aufgabe | R | Python |
|---|---|---|
| Zufall wiederholbar | set.seed(42) |
rng = np.random.default_rng(42) |
| Pfad bauen | file.path("daten", "messung.csv") |
Path("daten") / "messung.csv" |
| Pfad relativ zum Projekt | here::here("daten", "messung.csv") |
Path(__file__).resolve().parent / "daten" |
| Dokument rendern | quarto render bericht.qmd |
quarto render bericht.qmd |
| Stand festhalten | renv::snapshot(), sessionInfo() |
pip freeze > requirements.txt |
| Notebook ohne Ausgaben | jupyter nbconvert --clear-output --inplace datei.ipynb |
|
| exakte Summe | sum() rechnet mit erhöhter Genauigkeit |
math.fsum() |
Drei gleichverteilte Zufallszahlen mit dem Seed 42, zweimal hintereinander: Sind sie gleich? Und liefern R und Python mit demselben Seed dieselben Zahlen? Zum Vergleich drei Werte aus einem Lehmer-Generator mit der Saat 12345, in beiden Sprachen selbst geschrieben.
| Zeile | R | Python | Erklärung |
|---|---|---|---|
| zweimal Seed 42 | identical: TRUE |
True |
Innerhalb einer Sprache ist der Zufall mit Seed vollständig wiederholbar. |
| die drei Zahlen | eine Folge | eine andere Folge | R verwendet den Mersenne-Twister, NumPy standardmässig PCG64. Derselbe Seed, verschiedene Generatoren. |
| Lehmer mit Saat 12345 | 0.096617, 0.833995, 0.947702 | dieselben Werte | Ein Generator aus einer Formel, in beiden Sprachen gleich geschrieben, liefert identische Zahlen. |
Der Lehmer-Generator rechnet s = 16807 · s mod (2³¹ − 1) und teilt durch den Modulus. Er ist statistisch schlicht, genügt aber für Beispieldaten und hat den einen Vorteil, auf den es hier ankommt: Er ist in jeder Sprache in drei Zeilen nachgebaut.
Ein Seed garantiert Wiederholbarkeit, nicht Übertragbarkeit. Wer dieselbe Auswertung in R und Python rechnet und mit set.seed(42) und default_rng(42) Daten erzeugt, vergleicht zwei verschiedene Datensätze. Diese Sammlung hat genau das anfangs auf mehreren Seiten getan, und die Interpretationen stimmten nie für beide Sprachen zugleich. Seither sind alle Beispieldaten fest eingetragen oder mit dem Lehmer-Generator erzeugt.
Mit demselben Seed liefert jede Sprache bei jedem Lauf dieselben Zahlen, R und Python aber verschiedene. Der in beiden Sprachen gleich geschriebene Lehmer-Generator liefert identisch 0.096617, 0.833995 und 0.947702.
Fünf Messwerte 12.1, 14.3, 11.8, 15.0, 13.6. Der Mittelwert soll im Ergebnissatz stehen, ohne ihn abzutippen.
| Teil | Ergebnis | Erklärung |
|---|---|---|
| R-Chunk | 13.36 | die Rechnung, sichtbar mit Code |
| Python mit f-String | Der Mittelwert liegt bei 13.36. |
Der Satz entsteht im Code und wird als Ausgabe gezeigt. |
| Ergebnissatz unten | 13.36 | eingesetzt über Inline-Code, der mit r beginnt, beim Rendern; der Quelltext steht oben unter “Berichte, die nicht veralten” |
Inline-Code ist in dieser Sammlung auf R beschränkt, weil die Seiten mit knitr gerendert werden. Mit der Jupyter-Engine bietet Quarto dasselbe für Python in der Form `{python} ausdruck`.
Ändert sich ein Messwert, ändert sich beim nächsten Rendern auch die Zahl im Satz. Ein abgetippter Wert bliebe stehen und wäre ab dann falsch, ohne dass man es ihm ansieht. Für Berichte, die mit neuen Daten wiederholt werden, ist das der eigentliche Gewinn.
Der Mittelwert der fünf Messwerte liegt bei 13.36.
Die Datei messung.csv liegt im Projekt unter daten/roh/. Wie baut man diesen Pfad unabhängig vom Betriebssystem, und wie zerlegt man ihn in Name, Endung und Ordner?
[1] "daten/roh/messung.csv"
name ordner endung ohne_endung
"messung.csv" "daten/roh" "csv" "messung"
[1] FALSE
daten/roh/messung.csv
messung.csv messung .csv daten/roh
daten/roh/messung.parquet
daten\roh\messung.csv
False False
| Grösse | Python | R | Erklärung |
|---|---|---|---|
| Pfad | daten/roh/messung.csv |
"daten/roh/messung.csv" |
Relativ zum Projektordner, ohne Laufwerk und Benutzername. |
| Name | messung.csv |
basename |
|
| ohne Endung | stem: messung |
file_path_sans_ext |
|
| Endung | .csv |
file_ext: csv |
Python behält den Punkt, R nicht. |
| Ordner | daten/roh |
dirname |
|
| andere Endung | daten/roh/messung.parquet |
etwa für die Ablage eines bereinigten Stands | |
| unter Windows | daten\roh\messung.csv |
Derselbe Code liefert dort den passenden Trenner. | |
| absolut? | False |
||
| absoluter Fremdpfad vorhanden? | False |
FALSE |
Auf dem Rechner, der diese Seite baut, gibt es diesen Ordner nicht, genau wie auf jedem anderen fremden Rechner. |
Der absolute Pfad aus der letzten Zeile ist der typische Grund, warum ein Skript beim Weitergeben scheitert. Ein relativer, gebauter Pfad beschreibt nur die Lage innerhalb des Projekts und funktioniert überall, wo der Ordner als Ganzes liegt. pathlib bietet dazu die Zerlegung in Name, Stamm und Endung, die beim Umbenennen und Umwandeln von Dateien laufend gebraucht wird.
Der Pfad
daten/roh/messung.csvwird mitfile.pathbeziehungsweisePath(...) / ...gebaut und unter Windows automatisch mit Rückwärtsstrich geschrieben. Der absolute Pfad auf einen fremden Benutzerordner existiert auf dem Build-Rechner nicht.
Drei Additionen mit denselben Zahlen in verschiedener Klammerung, einmal mit 0.1, 0.2, 0.3 und einmal mit 1e16, 1, -1e16. Liefern sie dasselbe, und was tun die eingebauten Summenfunktionen?
| Rechnung | Python | R | Erklärung |
|---|---|---|---|
(0.1 + 0.2) + 0.3 |
0.60000000000000009 | dieselbe Zahl | Das Zwischenergebnis 0.30000000000000004 trägt seinen Fehler weiter. |
0.1 + (0.2 + 0.3) |
0.59999999999999998 | dieselbe Zahl | Andere Reihenfolge, anderer Rundungsfehler. |
| gleich? | False |
FALSE |
Die Addition von Gleitkommazahlen ist nicht assoziativ. |
1e16 + 1 − 1e16 schrittweise |
0.0 | 0 | 1e16 + 1 lässt sich nicht darstellen und wird zu 1e16; die 1 ist verloren. |
| eingebaute Summe | sum: 1.0 |
sum: 1 |
Python rechnet seit Version 3.12 mit Fehlerausgleich, R summiert intern mit erhöhter Genauigkeit. Beide finden die 1 wieder. |
| exakte Summe | math.fsum: 1.0 |
garantiert korrekt gerundet |
Dasselbe Programm ist also nur dann bit für bit reproduzierbar, wenn auch die Reihenfolge der Operationen gleich bleibt. Bei paralleler Berechnung, bei anderer Sortierung der Daten oder bei einer neuen Bibliotheksversion kann sich die letzte Stelle ändern. Das ist kein Fehler, sondern der Grund, warum Ergebnisse mit Toleranz verglichen werden (siehe Python-Grundlagen, Beispiel 5) und warum man in Berichten nicht mehr Stellen nennt, als die Daten tragen.
(0.1 + 0.2) + 0.3und0.1 + (0.2 + 0.3)unterscheiden sich in der siebzehnten Stelle, in R wie in Python. Schrittweise addiert geht die 1 in1e16 + 1 − 1e16verloren; die eingebauten Summenfunktionen beider Sprachen liefern 1.
Eine Funktion multipliziert mit einem Faktor, der ausserhalb von ihr definiert ist. Zwischen zwei Aufrufen wird der Faktor weiter oben im Skript geändert, etwa in einer Notebook-Zelle, die später noch einmal ausgeführt wurde.
| Aufruf | Ergebnis | Erklärung |
|---|---|---|
| erster Aufruf | 6 | Der Faktor ist 2. |
| nach der Änderung | 30 | Derselbe Aufruf, ein anderes Ergebnis. Die Funktion liest den Faktor erst beim Aufruf. |
| mit Argument | 6 | Alles, was die Funktion braucht, steht im Aufruf. |
Im Skript sieht man die Ursache, weil die Zeilen untereinander stehen. In einer gewachsenen Sitzung oder einem Notebook, dessen Zellen in anderer Reihenfolge liefen, sieht man sie nicht: Dieselbe Zelle liefert heute 30 und nach einem Neustart 6. Zwei Gegenmittel ergänzen sich. Funktionen bekommen alles, was sie brauchen, als Argument. Und vor jedem Ergebnis, das weitergegeben wird, steht der Neustart mit vollständigem Durchlauf von oben.
Derselbe Aufruf
skaliere(3)lieferte 6 und nach einer Änderung des äusseren Faktors 30. Mit dem Faktor als Argument hängt das Ergebnis nur noch vom Aufruf ab.
Ein Skript liest Daten über read.csv("C:/Users/mv/Desktop/Analyse/daten.csv") und läuft auf dem eigenen Rechner einwandfrei. Was spricht trotzdem dagegen?
read.csv ist veraltet
R rechnet mit set.seed(42), Python mit np.random.default_rng(42). Die Mittelwerte der erzeugten Daten unterscheiden sich. Was ist los?
Warum sollte RStudio den Arbeitsspeicher beim Beenden nicht sichern?
Zwei Läufe derselben Auswertung, einmal seriell und einmal parallel, unterscheiden sich in der 16. Nachkommastelle. Ist das ein Fehler?
Eine Zahl im Bericht wurde aus der Konsole kopiert. Was ist das Risiko?
r beginnt, setzt die Zahl bei jedem Rendern neu ein.