CSV-Daten

Ein schönes Einsteigerprojekt ist die Verarbeitung von CSV-Dateien mit Temperaturdaten. Dabei kannst du funktionale Werkzeuge wie map, filter, reduce und ->> verwenden.

Ziel: Fahrenheit in Celsius umrechnen und Durchschnitt berechnen

Beispielinhalt einer Datei temps.csv:

1
2
3
4
"Ort","Fahrenheit"
"New York",86
"London",75
"Zürich",79

Der Code soll die Temperaturen in °C umrechnen und einen Mittelwert ausgeben. Die Formel für die Umrechnung von Fahrenheit ist:

$$T_\mathrm{Celsius} = \frac{5}{9} \left( T_\mathrm{Fahrenheit} - 32 \right)$$
Lösung

Abhängigkeit für CSV eintragen

In project.clj unter :dependencies:

1
[org.clojure/data.csv "1.0.1"]

Dann:

1
lein deps

CSV lesen und verarbeiten

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(ns hello-world.core
  (:require [clojure.data.csv :as csv]
            [clojure.java.io :as io]))

(defn fahr->celsius [f]
  (/ (* (- f 32) 5) 9))

(defn avg [xs]
  (/ (reduce + xs) (count xs)))

(defn process-csv [filename]
  (with-open [reader (io/reader filename)]
    (->> (csv/read-csv reader)
         rest
         (map #(Double/parseDouble (second %)))
         (map fahr->celsius)
         avg
         (format "Durchschnitt: %.1f °C")
          println)))
  • with-open öffnet die Datei sicher und schließt sie automatisch nach dem Lesen.
  • csv/read-csv liest die Datei zeilenweise und parst jede Zeile als Vektor von Strings.
  • rest überspringt die erste Zeile (Header).
  • map #(Double/parseDouble (second %)) rows extrahiert den zweiten Spaltenwert (Temperatur) und wandelt ihn in eine Zahl um.
  • map fahr->celsius konvertiert alle Fahrenheit-Werte in Celsius (Teil der ->>-Kette).
  • avg berechnet den Mittelwert der umgerechneten Celsius-Werte (ebenfalls in der ->>-Kette).
  • Das Ergebnis wird mit println ausgegeben.

Testaufruf in -main

1
2
(defn -main [& args]
  (process-csv "resources/temps.csv"))

Die Datei temps.csv legst du unter resources/ ab.

Dann:

1
lein run

Erwartete Ausgabe:

Durchschnitt: 26,7 °C

Du kannst auf diese Weise viele funktionale Konzepte trainieren: Mapping, Reduktion, Transformation und I/O.

Exkurs: Threading-Makros

Der ->> Operator ist eines der bekanntesten Makros in Clojure und gehört zur Familie der sogenannten Threading-Makros. Er wird verwendet, um verschachtelte Funktionsaufrufe übersichtlich zu schreiben – indem das Ergebnis jeweils als letztes Argument in den nächsten Ausdruck eingefügt wird.

Ohne -»

1
(avg (map fahr->celsius (map #(Double/parseDouble (second %)) (rest (csv/read-csv reader)))))

Mit -»

1
2
3
4
5
(->> (csv/read-csv reader)
     rest
     (map #(Double/parseDouble (second %)))
     (map fahr->celsius)
     avg)
  • -> führt als erstes Argument ein (Thread-First)
  • ->> führt als letztes Argument ein (Thread-Last)

Wofür das gut ist

  • Lesbarkeit: Datenfluss wird von oben nach unten sichtbar
  • Vermeidet Klammerhölle
  • Ideal für Pipelines wie in map, filter, reduce, avg

Clojure bietet mehrere sogenannte Threading-Makros, um verschachtelte Ausdrücke übersichtlicher zu schreiben:

MakroBeschreibungBeispiel
->Thread-First: Ergebnis wird als erstes Argument eingesetzt(-> x (f a) (g b))(g (f x a) b)
->>Thread-Last: Ergebnis wird als letztes Argument eingesetzt(->> x (f a) (g b))(g b (f a x))
as->Bindet den Zwischenwert an einen Namen (meist <>)(as-> x <> (f <>) (g 1 <> 2))
some->Thread-First, aber nur wenn nicht nil(some-> user :address :zip) ⇒ nil-safe Zugriff

Wann was verwenden?

  • -> ist ideal für Daten, die am Anfang übergeben werden (z. B. bei Maps: (-> user :address :zip))
  • ->> ist ideal für Daten, die durch Pipelines laufen (z. B. map, filter, reduce)
  • as-> ist gut, wenn du nicht nur vorne oder hinten einsetzen willst
  • some-> ist perfekt für optional verkettete Zugriffe mit nil-Sicherheit