Tipps & Tricks

Wesentliche Python-Konzepte für Datenwissenschaftler

9 min Lesezeit
Wesentliche Python-Konzepte für Datenwissenschaftler

Einführung

Die Verwendung von Python für die Datenwissenschaft sollte nicht nur „weil es alle anderen tun!“ geschehen. Die Dominanz von Python im Datenbereich ist kein Zufall. Es handelt sich um eine Sprache mit einer hochgradig ausdrucksstarken und lesbaren Syntax, die die Verwaltung von niedrigstufigem Speicher abstrahiert. Diese hohe Abstraktion hat jedoch ihren Preis: Die Standardausführung von Python ist dynamisch typisiert und interpretiert, was rohe Iterationen schmerzhaft langsam machen kann.

Um leistungsstarke Datensysteme zu schreiben, muss ein Datenwissenschaftler von den standardmäßigen prozeduralen Codierungsansätzen zu spezialisierten, vektorisierten und speicherbewussten Methoden übergehen. In diesem Artikel werden fünf wesentliche Python-Konzepte behandelt, die Ihnen helfen, von der Erstellung klobiger, langsamer Spaghetti-Codes zu blitzschnellen, produktionsreifen und funktionalen Datenpipelines überzugehen. Weitere Informationen finden Sie in unserem Artikel über leistungsstarke Python-Dekoratoren.

1. NumPy-Vektorisierung

Standard-Python-Schleifen sind langsam. Da Python eine interpretierte Sprache ist, verursacht jede Iteration einer for-Schleife erhebliche Overheadkosten: Typprüfung, dynamische Methodenaufrufe und Referenzzählung. Wenn Sie Millionen von Datenpunkten verarbeiten, summieren sich diese Mikro-Overheadkosten zu mehrsekündigen Engpässen.

Die Lösung ist die NumPy-Vektorisierung. Anstatt Elemente sequenziell im Python-Bytecode zu verarbeiten, lagert NumPy Schleifen an hochoptimierte, vorcompilierte C-Erweiterungen aus. Diese Operationen wirken auf ganze Arrays gleichzeitig und führen zusammenhängende Array-Blöcke auf Maschinenebene aus, wobei oft Single Instruction, Multiple Data (SIMD)-Befehle verwendet werden.

Die klobige Methode

Angenommen, wir haben eine Liste von einer Million Float-Werten, die rohe Sensorablesungen darstellen, und wir müssen jede Ablesung um 1,5 skalieren und einen Kalibrierungsfaktor von 10,0 anwenden. Mit einer iterativen Python-Schleife:

import time\n# Eine große Liste von 10 Millionen Sensorablesungen\nn_elements = 10_000_000\ndata_list = [float(x) for x in range(n_elements)]\n# Skalierung der Werte mit einer expliziten Python-Schleife\nstart_time = time.time()\nscaled_list = []\nfor val in data_list:\n    scaled_list.append(val * 1.5 + 10.0)\nloop_duration = time.time() - start_time\nprint(f\"Loop implementation took: {loop_duration:.6f} seconds\")

Ausgabe:

Loop implementation took: 0.378866 seconds

Die vektorisierte Methode

Hier ist die elegante, vektorisierte Alternative. Wir laden die Daten in ein zusammenhängendes NumPy-Array und führen die Arithmetik direkt auf dem Array-Objekt aus:

import numpy as np\nimport time\n# Eine große Liste von 10 Millionen Sensorablesungen\nn_elements = 10_000_000\n# Vektorisierte Methode: NumPy führt die gesamte Berechnung in vorcompilierten C-Schleifen aus\ndata_array = np.arange(n_elements, dtype=float)\nstart_time = time.time()\nscaled_array = data_array * 1.5 + 10.0\nnumpy_duration = time.time() - start_time\nprint(f\"NumPy implementation took: {numpy_duration:.6f} seconds\")\nprint(f\"Speedup: {loop_duration / numpy_duration:.1f}x faster!\")

Ausgabe:

Loop implementation took: 0.348456 seconds\nNumPy implementation took: 0.013395 seconds\nSpeedup: 26.0x faster!

Durch die Vektorisierung der Arithmetik können wir einen erheblichen Leistungszuwachs mit saubererem, prägnanterem Code erzielen. Die Schleife wird aus dem Python-Raum entfernt und vollständig im Hochgeschwindigkeits-C-Raum ausgeführt.

2. Broadcasting: Mathematische Regeln für nicht übereinstimmende Dimensionen

In der linearen Algebra erfordern Matrixoperationen in der Regel, dass beide Operanden die exakt gleiche Form haben. In der Datenwissenschaft müssen wir jedoch häufig Operationen an Arrays mit unterschiedlichen Dimensionen durchführen, wie das Subtrahieren von Mittelwerten der Merkmalskolonnen aus einem Datensatz oder das Normalisieren von Zeilenwerten.

Anstatt Daten zu duplizieren, um übereinstimmende Formen zu erzwingen, verwendet NumPy eine Reihe von mathematischen Regeln, die als Broadcasting bezeichnet werden. Broadcasting ermöglicht elementweise Operationen an Arrays unterschiedlicher Formen, indem das kleinere Array virtuell entlang der fehlenden oder eindimensionalen Dimensionen erweitert wird, ohne Daten im Speicher zu kopieren.

Die Broadcasting-Regeln lauten:

  • Wenn die Arrays nicht den gleichen Rang (Anzahl der Dimensionen) haben, wird die Form des Arrays mit niedrigerem Rang mit 1s vorangestellt, bis beide Formen die gleiche Länge haben.
  • Zwei Dimensionen sind kompatibel, wenn sie gleich sind oder wenn eine von ihnen 1 ist.
  • Wenn sie kompatibel sind, verhält sich das Array so, als ob es entlang der Dimension der Größe 1 gestreckt wird, um die Form des anderen Arrays zu erreichen.

Die klobige Methode

Angenommen, wir haben eine 3×4-Merkmalsmatrix (3 Proben, 4 Merkmale) und möchten die Mittelwerte der Spalten subtrahieren, um die Merkmale zu „entmitteln“:

import numpy as np\nfeatures = np.array([\n    [10.0, 20.0, 30.0, 4.0],\n    [12.0, 24.0, 36.0, 8.0],\n    [14.0, 28.0, 42.0, 12.0]\n])\n# Mittelwert jeder Merkmalskolonne (Form: (4,))\ncol_means = np.mean(features, axis=0)\n# Verwendung von geschachtelten Schleifen zur manuellen Entmittelung\ndemeaned_clunky = np.zeros_like(features)\nfor idx in range(features.shape[0]):\n    for col_idx in range(features.shape[1]):\n        demeaned_clunky[idx, col_idx] = features[idx, col_idx] - col_means[col_idx]\n# Alternative: Tiling des Arrays, um übereinstimmende Formen zu erzwingen\ntiled_means = np.tile(col_means, (features.shape[0], 1))\ndemeaned_tiled = features - tiled_means

Die pythonische Methode

Mit Broadcasting führen wir die Subtraktion direkt durch. NumPy richtet automatisch die (3, 4) Merkmalsmatrix mit dem (4,) Mittelwertarray aus, indem es die Form des Mittelwerts als (1, 4) behandelt:

import numpy as np\nfeatures = np.array([\n    [10.0, 20.0, 30.0, 4.0],\n    [12.0, 24.0, 36.0, 8.0],\n    [14.0, 28.0, 42.0, 12.0]\n])\ncol_means = np.mean(features, axis=0)\n# Pythonische Subtraktion über automatisches Broadcasting\ndemeaned_broadcasting = features - col_means\n# Division jeder Zeile durch ihre Zeilensumme\n# row_sums hat die Form (3,) -> um (3, 4) durch (3,) zu dividieren, erweitern wir die Form auf (3, 1) mit np.newaxis\nrow_sums = np.sum(features, axis=1)\nnormalized_features = features / row_sums[:, np.newaxis]\nprint(\"Demeaned:\\n\", demeaned_broadcasting)\nprint(\"\\nNormalisierte Zeilen:\\n\", normalized_features)

Ausgabe:

Demeaned:\n [[-2. -4. -6. -4.]\n [ 0. 0. 0. 0.]\n [ 2. 4. 6. 4.]]\nNormalisierte Zeilen:\n [[0.15625 0.3125 0.46875 0.0625 ]\n [0.15 0.3 0.45 0.1 ]\n [0.14583333 0.29166667 0.4375 0.125 ]]

Broadcasting eliminiert doppelte Werte und das Kopieren von Speicher. Im Hintergrund führt NumPy die Subtraktionsschleifen mit C-Geschwindigkeit aus, ohne ein geteiltes Zwischenmatrix zu erstellen, was die Speicherbandbreite schont und die Operationen beschleunigt.

3. Die Methoden .pipe() und .assign() von Pandas: Saubere, funktionale Pipelines

Die Datenvorbereitung in Pandas degeneriert oft zu sequenziellen Spaghetti-Codes. Entwickler erstellen mehrere Zwischen-DataFrames (df1, df2 usw.), ändern Variablen vor Ort oder verketten Klammern. Dies führt zu Code, der schwer zu lesen, schwer zu testen und notorisch anfällig für die gefürchtete SettingWithCopyWarning ist.

Modernes Pandas ermutigt dazu, sich von prozeduralen Mutationen zu entfernen und hin zu funktionalen, deklarativen Datenpipelines. Durch die Nutzung von .assign() zur Erstellung von Merkmalen und .pipe() für wiederverwendbare Mehrspaltenoperationen können Sie Schritte in einer einzigen Pipeline verketten.

Die klobige Methode

Nehmen wir einen Rohdatensatz über Kundenverkäufe, der das Herausfiltern von Ausreißern, das Standardisieren von Zeichenfolgen, das Imputieren von Werten und das Berechnen von Verkaufssteuern erfordert:

import pandas as pd\nimport numpy as np\nraw_data = {\n    'Customer_ID': [101, 102, 103, 104, 105],\n    'Age': [25, -5, 47, 120, 31],\n    'Country': ['usa', 'CANADA', 'usa', 'Germany', 'canada'],\n    'Raw_Spend': [120.50, 450.00, 80.00, np.nan, 300.00]\n}\ndf = pd.DataFrame(raw_data)\n# Sequenzielle Zwischenmutationen\ndf_clean = df.copy()\n# 1. Ungültige Alterswerte herausfiltern\ndf_clean = df_clean[(df_clean['Age'] >= 0) & (df_clean['Age'] < 100)]

Die pythonische Methode

Wenn wir dies als ein funktionales Methodenkettungsproblem betrachten, können wir den Schritt zur Standardisierung der Länder in eine wiederverwendbare Hilfsfunktion einwickeln und eine einzige, saubere, eigenständige Pipeline erstellen.

import pandas as pd\nimport numpy as np\nraw_data = {\n    'Customer_ID': [101, 102, 103, 104, 105],\n    'Age': [25, -5, 47, 120, 31],\n    'Country': ['usa', 'CANADA', 'usa', 'Germany', 'canada'],\n    'Raw_Spend': [120.50, 450.00, 80.00, np.nan, 300.00]\n}\ndf = pd.DataFrame(raw_data)\n# Wiederverwendbare benutzerdefinierte Transformationsfunktion für .pipe()\ndef standardize_countries(dataframe: pd.DataFrame) -> pd.DataFrame:\n    df_out = dataframe.copy()\n    df_out['Country'] = df_out['Country'].str.upper().str.strip()\n    return df_out\n# Elegante funktionale Pipeline\n\ndf_clean_pipeline = (\n    df.query(\"Age >= 0 and Age < 100\")\n    .pipe(standardize_countries)\n    .assign(Taxed_Spend=lambda x: x['Raw_Spend'] * 1.155)\n)\nprint(df_clean_pipeline)

Ausgabe:

   Customer_ID  Age Country  Raw_Spend  Taxed_Spend\n0          101   25     USA      120.50     138.5750\n2          103   47     USA       80.00      92.0000\n4          105   31  CANADA     300.00     345.0000

Die Methodenkettung stellt sicher, dass der Zustand Ihres ursprünglichen DataFrames niemals versehentlich verändert wird, wodurch Nebenwirkungen verhindert werden. .assign() behandelt die Spaltenzuweisungen, indem sie eine Lambda-Funktion erhält, bei der x den aktiven Zustand des DataFrames zu diesem Zeitpunkt in der Kette bezeichnet, während .pipe() es ermöglicht, benutzerdefinierte Operationen sauber zu modularisieren.

4. Lambda-Funktionen für Datenumwandlungen

Feature Engineering erfordert häufig kleine, spezifische Transformationen, wie das Formatieren von Zeichenfolgen, das Aufteilen von Werten oder das Anwenden von bedingten Anweisungen. Das Schreiben benutzerdefinierter benannter Funktionen (mit def) für diese einfachen Berechnungen fügt Ihrem Skript unnötigen Boilerplate-Code hinzu.

Ein eleganterer Ansatz ist die Verwendung von Lambda-Funktionen innerhalb von Pandas' .map() und .apply(). Lambda-Funktionen sind anonyme, wegwerfbare Funktionen, die spontan ohne Namen definiert werden und sich perfekt für schnelle Datenzuordnungen und saubere Inline-Transformationen eignen.

Die klobige Methode

Angenommen, wir haben einen Datensatz von Mitarbeitern, und wir müssen ihren Remote-Arbeitsstatus zuordnen und ihre Nachnamen analysieren. Ein häufiger Fehler ist das Schreiben manueller Schleifen oder die Verwendung von iterrows():

import pandas as pd\ndf = pd.DataFrame({\n    'employee_name': ['john doe', 'jane smith', 'bob johnson'],\n    'department_code': ['IT_01', 'HR_02', 'IT_03'],\n    'is_remote': [1, 0, 1]\n})\n# Zeilenweise Iteration (langsam und umständlich verwaltet)\ndf_clunky = df.copy()\ndf_clunky['remote_status'] = None\ndf_clunky['last_name'] = None\nfor index, row in df_clunky.iterrows():\n    # Parsing des Remote-Status\n    if row['is_remote'] == 1:\n        df_clunky.at[index, 'remote_status'] = \"Remote\"\n    else:\n        df_clunky.at[index, 'remote_status'] = \"Office\"\n    # Parsing und Kapitalisierung des Nachnamens\n    name_parts = row['employee_name'].split()\n    df_clunky.at[index, 'last_name'] = name_parts[1].capitalize()

Die pythonische Methode

Hier ist der saubere, deklarative Ansatz unter Verwendung von Inline-Lambda-Transformationen. Wir wenden Inline-anonyme Logik an, um Spalten sofort zu transformieren, indem wir .map() für einfache Konvertierungen und .apply() für benutzerdefinierte Zeichenfolgenoperationen verwenden:

import pandas as pd\ndf = pd.DataFrame({\n    'employee_name': ['john doe', 'jane smith', 'bob johnson'],\n    'department_code': ['IT_01', 'HR_02', 'IT_03'],\n    'is_remote': [1, 0, 1]\n})\n# Lambdas, die in map() und apply() geschachtelt sind\ndf_opt = df.assign(\n    remote_status=lambda d: d['is_remote'].map(lambda val: \"Remote\" if val == 1 else \"Office\"),\n    last_name=lambda d: d['employee_name'].apply(lambda name: name.split()[-1].capitalize()),\n    dept_level=lambda d: d['department_code'].apply(lambda code: code.split('_')[-1])\n)\nprint(df_opt[['employee_name', 'last_name', 'remote_status', 'dept_level']])

Ausgabe:

   employee_name last_name remote_status dept_level\n0       john doe      Doe        Remote        01\n1     jane smith    Smith       Office        02\n2   bob johnson  Johnson        Remote        03

Durch die Verwendung von Lambdas können Sie selbstständige Transformationen schreiben, die Ihre Logik eng an die Spaltenzuweisungen binden. Durch die Kombination von Lambda mit .map() und .apply() beseitigen Sie umständliche geschachtelte Schleifen und halten Ihren Code schön lesbar.

5. Speicherverwaltung mit DataFrames: Optimierung der dtypes

Standardmäßig, wenn Pandas einen Datensatz importiert (z. B. aus CSV- oder


Quellen: kdnuggets

Bildquelle: KI generiert

KI Snack