Die Reaktion auf die Herausforderungen, die beim Arbeiten mit Data Science Notebooks auftreten, ist oft frustrierend. Ein Notebook „stirbt“, sobald der Befehl „Kernel neu starten und alles ausführen“ nicht mehr funktioniert. In der Regel bemerkt dies niemand sofort. Die Analyse ist abgeschlossen, das Diagramm wurde in eine Präsentation eingefügt und die Datei wurde weitergegeben. Doch dann fragt jemand nach einer Zahl, und beim Öffnen des Notebooks zeigt sich, dass eine Zelle aufgrund eines umbenannten oder gelöschten Spaltennamens nicht mehr ausführbar ist. Die Ausgabewerte zeigen noch die alten Zahlen, was den Anschein erweckt, dass das Notebook in Ordnung ist, während es tatsächlich nicht mehr funktionsfähig ist.
Die Gewohnheiten, die dies verhindern, sind einfach umzusetzen. Im Folgenden werden wir diese Gewohnheiten anhand eines realen Datensatzes anwenden und alles unter 100 Zeilen Pandas halten.
Der Datensatz
In diesem Artikel verwenden wir eine Tabelle mit dem Namen olympics_athletes_events, die in einer Interviewfrage verwendet wurde. Diese Tabelle enthält eine Zeile pro Athlet und pro Veranstaltung, was für die spätere Analyse von Bedeutung ist. Sie umfasst 352 Zeilen, die 336 Athleten über 15 Spiele und 167 Veranstaltungen abdecken. Das bedeutet, dass 11 Athleten mehr als einmal erscheinen und einer sogar sechs Mal. Die Medaillenspalte ist für 120 Zeilen ausgefüllt, während eine leere Zelle bedeutet, dass der Athlet in dieser Veranstaltung keine Medaille gewonnen hat.
Hier ein Beispiel:
| id | name | sex | age | height | team | noc | year | sport | medal |
|---|---|---|---|---|---|---|---|---|---|
| 3520 | Guillermo J. Amparan | M | Mexico | MEX | 1924 | Athletics | |||
| 35394 | Henry John Finchett | M | Great Britain | GBR | 1924 | Gymnastics | |||
| 21918 | Georg Frederik Ahrensborg Clausen | M | 28.0 | Denmark | DEN | 1924 | Cycling | ||
| 110345 | Marinus Cornelis Dick Sigmond | M | 26.0 | Netherlands | NED | 1924 | Football | ||
| 999998 | John Testman | M | 30.0 | 180.0 | Canada | CAN | 2004 | Athletics | Bronze |
Nun wollen wir die Gewohnheiten erkunden, die Ihr Notebook am Leben erhalten.
Gewohnheit 1: Konfigurationen in die erste Zelle einfügen
Alle Pfade, Seeds, Schwellenwerte und magischen Zahlen gehören in die erste Zelle. Nichts anderes.
```
from pathlib import Path
import pandas as pd
DATA_PATH = Path("olympics_athletes_events.csv")
RANDOM_SEED = 42
NATURAL_KEY = ["id", "games", "event"]
SENTINEL_ID_FLOOR = 900_000 # real athlete ids in this extract stop at 134205
VALID_SEXES = {"M", "F"}
VALID_MEDALS = {"Gold", "Silver", "Bronze"}
AGE_RANGE = (10, 75)
HEIGHT_RANGE_CM = (120, 230)
WEIGHT_RANGE_KG = (25, 220)
MIN_ATHLETES_PER_SPORT = 5
```
Hier passieren zwei wichtige Dinge. Jemand, der das Notebook sechs Monate später liest, kann alle Annahmen in 15 Zeilen sehen, ohne scrollen zu müssen. Und wenn sich die Datei verschiebt oder der Schwellenwert ändert, gibt es genau einen Ort, an dem Änderungen vorgenommen werden müssen.
Gewohnheit 2: Eine Funktion pro Zelle schreiben
Die Regel, die Notebooks rettet: Eine Zelle definiert eine Funktion oder ruft eine auf. Sie tut beides nicht und verändert niemals eine Variable, die eine andere Zelle bereits erstellt hat.
```
def load_raw(path: Path) -> pd.DataFrame:
"""Read the Olympics CSV with no cleaning applied."""
return pd.read_csv(path)
def clean(df: pd.DataFrame) -> pd.DataFrame:
"""Drop test rows and duplicate entries, then encode 'no medal' explicitly."""
out = df[df["id"] < SENTINEL_ID_FLOOR].copy()
out = out.drop_duplicates(subset=NATURAL_KEY, keep="first")
out["medal"] = out["medal"].fillna("None")
out["sex"] = out["sex"].astype("category")
out["season"] = out["season"].astype("category")
return out.reset_index(drop=True)
def add_features(df: pd.DataFrame) -> pd.DataFrame:
"""Add decade, is_medalist and bmi. Never mutates the input frame."""
out = df.copy()
out["decade"] = (out["year"] // 10) * 10
out["is_medalist"] = out["medal"].ne("None")
out["bmi"] = bmi(out["weight"], out["height"])
return out
```
Das .copy() in der ersten Zeile jeder Funktion ist der ganze Trick. Sobald keine Funktion mehr in ihr Argument schreibt, spielt die Reihenfolge der Zellen keine Rolle mehr. Das mehrmalige Ausführen von add_features ergibt fünfmal dasselbe DataFrame, sodass das klassische Notebook-Problem, bei dem die dritte Ausführung andere Zahlen als die erste liefert, nicht mehr auftreten kann.
Beachten Sie, was clean mit medal macht. Pandas interpretiert das Leerzeichen als NaN, aber ein leeres Medaillenfeld bedeutet, dass der Athlet teilgenommen hat und nicht platziert wurde. Das ist ein tatsächlicher Wert, also schreiben wir "None" und hören auf, es als fehlende Daten zu behandeln.
Gewohnheit 3: Die Daten validieren, bevor man ihnen vertraut
Schreiben Sie auf, was wir über die Daten glauben, und lassen Sie das Notebook dies überprüfen.
```
def validate_raw(df: pd.DataFrame) -> list[str]:
"""Return a list of contract violations. An empty list means the data is usable."""
problems = []
expected = {"id", "sex", "age", "height", "weight", "year", "sport", "event",
"medal", "games", "team"}
missing = expected - set(df.columns)
if missing:
problems.append(f"missing columns: {sorted(missing)}")
return problems
dupes = df.duplicated(subset=NATURAL_KEY).sum()
if dupes:
problems.append(f"{dupes} duplicate rows on {NATURAL_KEY}")
sentinels = df.loc[df["id"] >= SENTINEL_ID_FLOOR, "id"]
if len(sentinels):
found = sorted(int(i) for i in sentinels.unique())
problems.append(f"{len(sentinels)} sentinel ids: {found}")
bad_sex = set(df["sex"].dropna().unique()) - VALID_SEXES
if bad_sex:
problems.append(f"unexpected sex values: {bad_sex}")
bad_medal = set(df["medal"].dropna().unique()) - VALID_MEDALS
if bad_medal:
problems.append(f"unexpected medal values: {bad_medal}")
for col, (lo, hi) in [("age", AGE_RANGE),\
("height", HEIGHT_RANGE_CM),\
("weight", WEIGHT_RANGE_KG)]:
out = df[col].dropna()
n = ((out < lo) | (out > hi)).sum()
if n:
problems.append(f"{n} {col} values outside {lo}-{hi}")
return problems
```
Wenn wir es auf die Rohdatei anwenden:
```
loaded 352 rows, 15 columns
validation: ["3 duplicate rows on ['id', 'games', 'event']", '2 sentinel ids: [999998, 999999]']
after cleaning: 347 rows
```
Beide Ergebnisse sind eine Minute wert.
Die Duplikatsprüfung erfolgt auf ["id", "games", "event"], nicht nur auf id. Wenn nur id überprüft wird, werden 16 Zeilen markiert, und jede von ihnen ist ein Fehlalarm, da id einen Athleten identifiziert und ein Athlet an mehreren Veranstaltungen in denselben Spielen teilnimmt. Das richtige Schlüsselwort zu wählen, ist das Wichtigste bei der Datenvalidierung.
Die Sentinel-IDs sind die eingefügten Zeilen. Zwei Datensätze für „John Testman“, IDs 999998 und 999999, 30 Jahre alt, 180 cm, 75 kg, einer Goldmedaille im Jahr 2000 und einer Bronze im Jahr 2004. Jemandes Testfixture wurde mit dem Extrakt versendet. Sie machen 0,6 % der Datei aus und sind unsichtbar in df.head(), df.describe() und jeder Nullzählung. Sie haben auch beide Medaillen im Bereich Athletics gewonnen, was die Medaillenquote im Bereich Athletics von 0,245 auf 0,213 senkt. Eine Überprüfung, die nur 3 Zeilen benötigt, hat einen Fehler von 13 % in der Hauptzahl entdeckt.
Gewohnheit 4: Tests, die im Notebook leben
Wir benötigen kein pytest, um ein Notebook zu testen. Wir brauchen ein Fixture, das klein genug ist, um es zu verstehen, und eine Zelle voller Assertions.
```
def _fixture() -> pd.DataFrame:
return pd.DataFrame({
"id": [1, 1, 2],
"games": ["1924 Summer"] * 3,
"event": ["Rings", "Rings", "Rings"],
"sex": ["M", "M", "F"],
"age": [24.0, 24.0, None],
"height": [180.0, 180.0, 165.0],
"weight": [81.0, 81.0, 55.0],
"year": [1924, 1924, 1924],
"season": ["Summer"] * 3,
"sport": ["Gymnastics"] * 3,
"medal": ["Gold", "Gold", None],
"team": ["Denmark"] * 3,
})
def run_tests() -> None:
fx = _fixture()
assert len(clean(fx)) == 2, "clean() must drop the duplicate entry"
assert clean(fx)["medal"].tolist() == ["Gold", "None"], "missing medal becomes 'None'"
before = fx.copy()
add_features(fx)
pd.testing.assert_frame_equal(fx, before) # add_features must not mutate
feat = add_features(clean(fx))
assert feat["is_medalist"].tolist() == [True, False]
assert round(feat.loc[0, "bmi"], 1) == 25.0
assert feat["decade"].unique().tolist() == [1920]
assert validate_raw(fx) == [f"1 duplicate rows on {NATURAL_KEY}"]
assert "missing columns" in validate_raw(pd.DataFrame({"id": [1]}))[0]
planted = pd.concat([fx, fx.iloc[[2]].assign(id=999999)], ignore_index=True)
assert any("sentinel" in p for p in validate_raw(planted))
assert 999999 not in clean(planted)["id"].values
print("all 10 checks passed")
all 10 checks passed
```
Drei Zeilen gefälschter Daten, 10 Assertions, unter einer Sekunde Laufzeit. Die assert_frame_equal-Überprüfung ist die, die sich bewährt: Sie schlägt lautstark fehl, wenn jemand eine Zeile zu add_features hinzufügt, die in df anstatt in out schreibt.
Platzieren Sie diese Zelle direkt unter den Funktionsdefinitionen und führen Sie sie bei jeder Sitzung aus. Eine rote Assertion um 9 Uhr ist günstiger als ein falsches Diagramm um 16 Uhr.
Gewohnheit 5: Dokumentation, die ausgeführt wird
Kommentare veralten, weil nichts sie überprüft. Docstring-Beispiele tun dies nicht, weil doctest sie ausführt.
```
def bmi(weight_kg: float, height_cm: float) -> float:
"""Body mass index in kg/m2.
>>> round(bmi(81.0, 180.0), 1)
25.0
>>> round(bmi(55.0, 165.0), 1)
20.2
"""
return weight_kg / (height_cm / 100) ** 2
```
In einem Notebook findet doctest.testmod() nichts, also rufen Sie die funktionale Version auf:
```
import doctest
doctest.run_docstring_examples(bmi, globals(), name="bmi", verbose=True)
Finding tests in bmi
Trying:
round(bmi(81.0, 180.0), 1)
Expecting:
25.0
ok
Trying:
round(bmi(55.0, 165.0), 1)
Expecting:
20.2
ok
```
Jetzt können die Dokumentation und das Verhalten nicht mehr widersprüchlich sein. Ändern Sie die Formel, um Meter zu verwenden, und der Docstring schlägt beim nächsten Ausführen fehl.
Gewohnheit 6: Das Notebook als Skript ausführen
Die letzte Zelle verknüpft die Funktionen miteinander. Diese Zelle beweist, dass das Notebook weiterhin funktioniert.
```
def medal_rate_by_sport(df: pd.DataFrame,
min_athletes: int = MIN_ATHLETES_PER_SPORT) -> pd.DataFrame:
"""Medal rate per sport, restricted to sports with enough entries."""
grouped = (df.groupby("sport", observed=True)
.agg(entries=("id", "size"),
medals=("is_medalist", "sum"),
mean_age=("age", "mean"))
.query("entries >= @min_athletes"))
grouped["medal_rate"] = (grouped["medals"] / grouped["entries"]).round(3)
grouped["mean_age"] = grouped["mean_age"].round(1)
return grouped.sort_values("medal_rate", ascending=False)
def main() -> pd.DataFrame:
raw = load_raw(DATA_PATH)
print(f"loaded {len(raw)} rows, {raw.shape[1]} columns")
problems = validate_raw(raw)
print("validation:", problems or "clean")
df = add_features(clean(raw))
print(f"after cleaning: {len(df)} rows")
print("\nspot check:")
print(df.sample(3, random_state=RANDOM_SEED)[["name", "year", "sport", "medal", "bmi"]])
report = medal_rate_by_sport(df)
print(f"\nmedal rate by sport ({len(report)} sports):")
print(report)
return report
if __name__ == "__main__":
run_tests()
doctest.run_docstring_examples(bmi, globals(), name="bmi", verbose=True)
main()
medal rate by sport (22 sports):
entries medals mean_age medal_rate
sport
Canoeing 5 5 29.6 1.000
Judo 5 5 24.6 1.000
Rugby 9 9 26.0 1.000
Hockey 5 4 23.0 0.800
...
Speed Skating 6 0 24.3 0.000
```
Der Block if __name__ == "__main__" ist der Teil, den die Leute oft überspringen. Damit gibt jupyter nbconvert --to script analysis.ipynb eine Datei aus, die sauber importiert werden kann und als normales Diff überprüft wird. Ohne ihn wird jede Anweisung auf oberster Ebene beim Import ausgeführt.
Die Stichprobenüberprüfung ist ebenfalls wichtig. Das Drucken von 3 gesäten Zeilen neben der Gesamtsumme zeigt uns tatsächliche Werte, und ein NaN im BMI in dieser Stichprobe zeigt uns sofort, dass Höhe und Gewicht bei den meisten älteren Datensätzen fehlen. 226 von 352 Zeilen haben keine Höhe.
Fazit
Sechs Gewohnheiten, und die Gesamtkosten betragen vielleicht 30 Minuten für das erste Notebook und 5 Minuten für jedes weitere:
- Konfiguration in Zelle 1, einschließlich des Seeds.
- Funktionen, die ihre Eingaben kopieren und niemals globale Variablen ändern.
- Eine Validierungszelle, die festhält, was wir glauben und dies überprüft.
- Ein kleines Fixture und eine Zelle mit Assertions, die bei jeder Sitzung ausgeführt wird.
- Docstring-Beispiele, damit die Dokumentation ausgeführt wird.
- Ein
main()hinter einem Namensschutz, damit das Notebook in ein Skript exportiert wird.
All dies sorgt dafür, dass das Notebook im November immer noch dieselben Zahlen liefert, wenn jemand es öffnet. Die Datei der Olympischen Spiele, mit der wir begonnen haben, sah sauber aus und hatte 3 Duplikate sowie 2 eingefügte Testathleten, und der einzige Grund, warum wir das wissen, ist eine Zelle, die 3 Minuten zum Schreiben benötigte.
Starten Sie den Kernel neu. Führen Sie alles aus. Wenn es funktioniert, ist das Notebook lebendig!
Nate Rosidi ist Data Scientist und in der Produktstrategie tätig. Er ist auch Dozent für Analytics und Gründer von StrataScratch, einer Plattform, die Data Scientists bei der Vorbereitung auf ihre Interviews mit echten Interviewfragen von Top-Unternehmen unterstützt. Nate schreibt über die neuesten Trends im Arbeitsmarkt, gibt Interviewtipps, teilt Data Science-Projekte und behandelt alles rund um SQL.
Mehr zu diesem Thema
- Spezifikationstechnik: Die neue Fähigkeit nach der Prompt-Technik
- Nach einem Interview ignoriert? 5 Ressourcen, die Ihnen helfen, wieder auf die Beine zu kommen
- Warum die meisten Menschen SMOTE missbrauchen und wie man es richtig macht
- Warum Modell-Destillation zur wichtigsten Technik in der Produktion von KI wird
- Ein End-to-End Data Science Projekt mit Grok Build und Grok 4.6 erstellen
- Python für Data Science (Kostenloser 7-tägiger Mini-Kurs)
Quellen: kdnuggets
Bildquelle: KI generiert
Dieser Text wurde mit Hilfe von künstlicher Intelligenz in Zusammenarbeit mit unserer Redaktion erstellt.
🚀