12.4.6. Statistiken - Sortieren (A)#
Zur Ermittlung der Quantile muss Julia die Messwerte also erst einmal sortieren.
12.4.6.1. Sortieralgorithmen#
Julia startet damit, die Messwerte zu sortieren.
Sie ist ganz Feuer und Flamme, da sie im Studium viel über Sortieralgorithmen gelernt hat. Sie erinnert sich: Es gibt viele unterschiedliche Ansätze zum Sortieren.
Sie findet dazu eine Übersichtstabelle:
Algorithmus / Familie |
Kurzidee |
Best Case |
Average Case |
Worst Case |
Extra Space |
Stabil |
In-place |
Praxis / Bemerkung |
|---|---|---|---|---|---|---|---|---|
Bubble Sort |
Tauscht benachbarte Elemente |
O(n) |
O(n²) |
O(n²) |
O(1) |
Ja |
Ja |
Lehrzwecke |
Selection Sort |
Wählt jeweils Minimum |
O(n²) |
O(n²) |
O(n²) |
O(1) |
Nein |
Ja |
Minimaler Speicher |
Insertion Sort |
Fügt Elemente in sortierte Teilliste ein |
O(n) |
O(n²) |
O(n²) |
O(1) |
Ja |
Ja |
Kleine / fast sortierte Daten |
Merge Sort |
Divide & Conquer + Merge |
O(n log n) |
O(n log n) |
O(n log n) |
O(n) |
Ja |
Nein |
Stabile Referenz |
Quick Sort |
Partition um Pivot |
O(n log n) |
O(n log n) |
O(n²) |
O(log n) |
Nein |
Ja |
Sehr schnell, aber Worst-Case |
Heap Sort |
Heap-Struktur |
O(n log n) |
O(n log n) |
O(n log n) |
O(1) |
Nein |
Ja |
Garantierte Laufzeit |
Counting Sort |
Zählt Vorkommen |
O(n+k) |
O(n+k) |
O(n+k) |
O(k) |
Ja |
Nein |
Kleine Wertebereiche |
Radix Sort |
Ziffernweise Sortierung |
O(n·k) |
O(n·k) |
O(n·k) |
O(n+k) |
Ja |
Nein |
Ganzzahlen / Strings |
Timsort (Familie) |
Adaptive Merge + Insertion |
O(n) |
O(n log n) |
O(n log n) |
O(n) |
Ja |
Nein |
Industriestandard |
PowerSort (Merge-Policy) |
Nahezu optimale Merge-Reihenfolge |
O(n) |
O(n log n) |
O(n log n) |
O(n) |
Ja |
Nein |
CPython ≥ 3.11 |
Sie recherchiert weiter und findet heraus:
Python sortiert Listen seit Jahren mit einer sehr ausgefeilten, stabilen Merge-Sort-Familie (Timsort).
Ab Python 3.11 wurde vor allem die Merge-Strategie verbessert (PowerSort).
Julia würde trotzdem gerne selbst bestimmen können, welcher Algorithmus verwendet wird. Also implementiert sie ein kleines Strategie-Pattern:
„Sortieren“ ist austauschbar.
Der Rest des Programms bleibt gleich.
from dataclasses import dataclass
class SortStrategy:
def sort(self, values):
raise NotImplementedError
@dataclass(frozen=True)
class BubbleSort(SortStrategy):
def sort(self, values):
a = list(values) # kopieren, Input nicht verändern
n = len(a)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
swapped = True
if not swapped:
break
return a
@dataclass(frozen=True)
class SelectionSort(SortStrategy):
def sort(self, values):
a = list(values)
n = len(a)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if a[j] < a[min_idx]:
min_idx = j
if min_idx != i:
a[i], a[min_idx] = a[min_idx], a[i]
return a
@dataclass(frozen=True)
class PythonSort(SortStrategy):
def sort(self, values):
return sorted(values)
Mini-Demo: Alle Strategien liefern dasselbe Ergebnis.
values = [82, 91, 12, 92, 63, 9, 28, 55, 96, 97]
strategies = [BubbleSort(), SelectionSort(), PythonSort()]
for s in strategies:
print(type(s).__name__, "->", s.sort(values))
BubbleSort -> [9, 12, 28, 55, 63, 82, 91, 92, 96, 97]
SelectionSort -> [9, 12, 28, 55, 63, 82, 91, 92, 96, 97]
PythonSort -> [9, 12, 28, 55, 63, 82, 91, 92, 96, 97]
12.4.6.2. Performance: Lernalgorithmen vs. Python-Sort#
Julia muss sich entscheiden, welchen Algorithmus sie jetzt verwendet. Deshalb führt sie einen einfachen Performance-Test durch.
Exkurs: Performance messen (Profiling)
„Performance“ kann sich auf verschiedene Dinge beziehen – z.B. Laufzeit, Speicherverbrauch (RAM) oder I/O-Verhalten (Warten auf Dateien/Netzwerk). Welche Kennzahl relevant ist, hängt vom Programm ab.
Einfache Zeitmessung in Python
Die Idee ist folgende:
Wir messen den Startzeitpunkt:
start = time.perf_counter()(alternativ:time.time()).Wir messen den Endzeitpunkt:
end = time.perf_counter().Die Dauer ist dann entsprechend:
dauer = end - start.
Profiling Profiling ist mehr als eine einzelne Zeitmessung: Es hilft Ihnen zu verstehen, welche Funktionen wie viel Zeit (oder Speicher) verbrauchen – also wo der Engpass wirklich liegt.
CPU-Profiling (Standardbibliothek):
cProfile(+ Auswertung mitpstats)Visualisierung: z.B.
snakeviz(zeigtcProfile-Ausgaben grafisch)Sampling-Profiler (geringer Overhead): z.B.
py-spy(läuft auch an laufenden Prozessen)Line-by-line: z.B.
line_profiler(sehr konkret, aber mehr Overhead)Speicher/Allokationen:
tracemalloc(Standardbibliothek), optionalmemory_profiler
Tipp: Messen Sie mit realistischen Eingaben und achten Sie darauf, ob Ihr Programm eher CPU-bound (Rechnung) oder I/O-bound (Warten) ist.
Der Performance-Test ist so aufgebaut, dass jeder Algorithmus 100-mal für die Messdaten der Station Paris getestet wird. Dann werden die Ergebnisse in einem Boxplot gegenübergestellt.
Hinweis
Für die Darstellung verwenden wir seaborn. Das ist eine externe Bibliothek. Julia findet das hier in Ordnung, weil der Performance-Test nur der Analyse dient und nicht Teil der ausgelieferten Software ist.
12.4.6.3. Daten einlesen#
Wie in den vorherigen Teilkapiteln liest Julia die CSV-Datei direkt aus dem Internet ein. Dafür reicht urllib aus der Standardbibliothek – es muss nichts installiert werden.
from urllib.request import urlopen
def _non_empty_lines(text):
return [ln.strip() for ln in text.strip().splitlines() if ln.strip()]
def _ensure_comma_delimiter(lines):
"""
Zwischenlösung: Wir unterstützen NUR Komma.
Wenn es nach Semikolon aussieht, geben wir eine klare Fehlermeldung.
"""
if not lines:
raise ValueError("CSV ist leer.")
first_line = lines[0]
if "," not in first_line and ";" in first_line:
raise ValueError(
"Unerwartetes Trennzeichen ';'. Dieser Parser erwartet Komma ',' als Trennzeichen."
)
def _parse_header(header_line):
header = [h.strip() for h in header_line.split(",")]
if not header or header[0] != "datetime":
raise ValueError(
"Ungültiger Header: erste Spalte muss 'datetime' heißen (Komma ',' als Trennzeichen)."
)
stations = header[1:]
if not stations:
raise ValueError("Header enthält keine Stationsspalten.")
return header, stations
def _parse_no2_value(value, station, line):
"""
Parsen eines einzelnen Messwerts.
- Leere Felder geben wir als None zurück (d.h. Messwert fehlt).
- Nicht-leere Felder müssen in float umwandelbar sein.
"""
value = value.strip()
if value == "":
return None
try:
return float(value)
except ValueError as e:
raise ValueError(
f"Ungültiger Messwert {value!r} für {station!r} in Zeile: {line!r}."
) from e
def parse_air_quality_csv_v3(csv_text):
lines = _non_empty_lines(csv_text)
_ensure_comma_delimiter(lines)
header, stations = _parse_header(lines[0])
result = {s: {"time": [], "no2": []} for s in stations}
for line in lines[1:]:
if "," not in line and ";" in line:
raise ValueError(
"Unerwartetes Trennzeichen ';' in den Datenzeilen. Dieser Parser erwartet Komma ','."
)
parts = [p.strip() for p in line.split(",")]
if len(parts) > len(header):
raise ValueError(
f"Zu viele Spalten in Zeile: {line!r} (erwartet {len(header)}, gefunden {len(parts)})."
)
if len(parts) < len(header):
parts = parts + [""] * (len(header) - len(parts))
t = parts[0]
for idx, station in enumerate(stations, start=1):
parsed = _parse_no2_value(parts[idx], station, line)
if parsed is None:
continue
result[station]["time"].append(t)
result[station]["no2"].append(parsed)
return result
url = "https://gitlab.lrz.de/fk03ingenieurinformatik/ingenieurinformatik-buch-deploy-lrz/-/raw/master/data/air_quality_no2.csv"
with urlopen(url) as response:
csv_text = response.read().decode("utf-8")
data = parse_air_quality_csv_v3(csv_text)
12.4.6.4. Performance-Test mit Echtdaten#
import time
import matplotlib.pyplot as plt
import seaborn as sns
def time_sort_once(strategy, values):
# Achtung: Listen sind mutable -> jede Messung auf einer frischen Kopie
sample = list(values)
t0 = time.perf_counter()
strategy.sort(sample)
return time.perf_counter() - t0
# Reale Messdaten: wir vergleichen auf der Station Paris.
paris_no2 = data["Paris"]["no2"]
strategies = [BubbleSort(), SelectionSort(), PythonSort()]
print(f"Station: Paris | Größe n={len(paris_no2)}")
# 100 Messungen pro Algorithmus
n_runs = 100
names = []
times_ms = []
for s in strategies:
name = type(s).__name__
for _ in range(n_runs):
dt = time_sort_once(s, paris_no2)
names.append(name)
times_ms.append(dt * 1000)
plt.figure(figsize=(8, 4))
sns.boxplot(x=names, y=times_ms)
plt.title(f"Sortier-Performance (n={len(paris_no2)}, je {n_runs} Läufe)")
plt.xlabel("Algorithmus")
plt.ylabel("Zeit (ms)")
plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[4], line 13
10 return time.perf_counter() - t0
12 # Reale Messdaten: wir vergleichen auf der Station Paris.
---> 13 paris_no2 = data["Paris"]["no2"]
15 strategies = [BubbleSort(), SelectionSort(), PythonSort()]
17 print(f"Station: Paris | Größe n={len(paris_no2)}")
KeyError: 'Paris'
Exercise 12.3 (Aufgabe)
Implementieren Sie einen weiteren Sortieralgorithmus selbst und fügen Sie ihn dem Performance-Test hinzu. Visualisieren den Algorithmus mithilfe eines Struktogramms.
Wichtig
Sortieralgorithmen können prüfungsrelevant sein. Sie müssen die Algorithmen nicht alle lernen, aber Sie sollten in der Lage sein, mindestens einen Algorithmus selbstständig (also ohne list.sort() oder sorted()) zu implementieren.
Julia entscheidet sich: Für reale Daten nutzt sie die Python-Implementierung (schnell, getestet, robust). Ihre Implementierungen behält sie - vielleicht sind diese ja später noch irgendwann nutzbar.