Zum Hauptinhalt springen

Gemeinde-Ähnlichkeit in der Schweiz

·3 min
Karte der urbanen und alpinen Aufteilung der Schweiz

View Project   Live Demo

Executive Summary #

Auftraggeber: HSLU (Hochschulprojekt)
Der Erfolg: Erfolgreicher Pivot von einem fehlgeschlagenen Clustering-Ansatz zu einem Vektorraum-Modell. Das Resultat ist ein End-to-End-Datenprodukt, das komplexe sozioökonomische Beziehungen visualisiert.
Tech Stack: Python (Scikit-learn, Pandas), PostgreSQL, Express.js, Vue 3, Docker.

Die Herausforderung #

Regionalplanung und Ressourcenallokation in der Schweiz stützen sich oft auf “Benchmarking”—den Vergleich von Gemeinden. Der bestehende Ansatz greift meist auf geografische Nähe zurück (Vergleich mit dem Nachbarn) oder nutzt grobe kantonale Durchschnittswerte.

Dies erzeugt einen blinden Fleck in den Daten. Eine wohlhabende Industriegemeinde im Aargau steht oft vor ähnlichen Herausforderungen wie ein Vorort von Zürich, nicht wie ihr ländlicher Nachbar. Das Fehlen eines Tools, das quantitativ beantwortet, “Welche Gemeinden sind eigentlich strukturiert wie meine?”, führt zu ineffizienter Ressourcenverteilung.

Die Lösung #

Ich entwickelte eine “Similarity Engine”, die rohe sozioökonomische Daten des Bundesamtes für Statistik (BFS) verarbeitet.

Anstatt Gemeinden in willkürliche Gruppen zu zwingen, berechnet das System die paarweise Distanz zwischen jeder Gemeinde in einem 43-dimensionalen Vektorraum. Dies ermöglicht eine “Nearest Neighbour”-Suche basierend auf harten Daten (Demografie, Politik, Landnutzung) statt auf GPS-Koordinaten.

Die Ergebnisse #

  • Datenvalidierung: Das Modell rekonstruierte bekannte Phänomene wie den “Röstigraben” rein basierend auf Abstimmungsverhalten und Demografie, ohne explizites geografisches Training.
  • Performance: Ähnlichkeitsabfragen laufen in unter 1ms via indizierter SQL-Lookups auf einer 2'172 x 2'172 Matrix.
  • Granularität: Eine Abfrage für Zürich liefert Winterthur, Bern und Lausanne als Top-Treffer. Dies bestätigt, dass das Modell urbane Strukturen und soziopolitische Profile korrekt über reine geografische Nähe priorisiert.

Technische Architektur #

Das System folgt einer Standard-ETL-Pipeline, die eine containerisierte Webanwendung speist.

  1. Data Pipeline (Python): Ingestiert BFS-Datensätze, bereinigt fehlende Werte, standardisiert Features und berechnet die Kosinus-Ähnlichkeitsmatrix.
  2. Persistenz (PostgreSQL): Speichert die vorberechneten Ähnlichkeitsscores. Bidirektionale Indizierung auf source und target Spalten garantiert $O(1)$ Abrufzeit.
  3. API (Express.js): Eine leichtgewichtige REST-Schnittstelle.
  4. Frontend (Vue 3 + Leaflet): Interaktive Kartenvisualisierung.

Kernherausforderungen #

Das Clustering-Problem Anfänglich versuchte ich, Gemeinden mittels K-Means und Gaussian Mixture Models zu segmentieren. Die Resultate waren statistisch schwach (Silhouette Score: 0.045). Dies deutete darauf hin, dass Schweizer Gemeinden auf einem kontinuierlichen Spektrum existieren und nicht in diskreten “Töpfen”.

Der Fix: Kosinus-Ähnlichkeit Ich wechselte zu einem vektorbasierten Ansatz. Durch die Berechnung des Kosinus des Winkels zwischen Vektoren erfasst das System proportionale Beziehungen (z.B. das Verhältnis von Industrie- zu Landwirtschaftsfläche) statt absoluter Magnituden. Dies löste das Problem beim Vergleich grosser Städte mit kleineren Orten, die eine ähnliche Struktur aufweisen.

Implementierungsdetails #

Die Kernlogik basiert auf der Vektorisierung des Datensatzes. Da die Magnitude (Bevölkerungszahl) die Ähnlichkeit nicht verzerren soll, wird Kosinus-Ähnlichkeit gegenüber der Euklidischen Distanz bevorzugt.

def compute_similarity_matrix(df: pd.DataFrame, feature_cols: List[str]) -> pd.DataFrame:
    """
    Computes pairwise cosine similarity for all entities in the dataframe.
    
    Args:
        df: DataFrame containing municipality data.
        feature_cols: List of column names to use for the vector space.
        
    Returns:
        DataFrame: A symmetric N x N matrix where index/columns are municipality_ids.
    """
    # 1. Standardize features (Mean=0, Std=1)
    # Essential for PCA/Similarity to prevent features with large scales 
    # (e.g., average income) from dominating features with small scales (e.g., tax rate).
    scaler = StandardScaler()
    feature_matrix = df[feature_cols].to_numpy()
    scaled_features = scaler.fit_transform(feature_matrix)

    # 2. Compute Cosine Similarity (Result is N x N matrix)
    similarity_matrix = cosine_similarity(scaled_features)

    # 3. Convert back to DataFrame for readability
    return pd.DataFrame(
        similarity_matrix, 
        index=df['municipality_id'], 
        columns=df['municipality_id']
    )

Infrastruktur #

Der gesamte Stack ist in Docker Compose definiert, um Reproduzierbarkeit auf einem Linux VPS zu gewährleisten.

  • Datenbank: PostgreSQL Container mit persistentem Volume.
  • Backend: Node.js Container (Express).
  • Frontend: Nginx Container, der den statischen Vue Build ausliefert.