Gabi und Sascha
Kategorien : Alle | Berlin | Bücher | Fotografie | Java | Linkhalde | Weichware | Verfassung

Angenommen ich habe ein R data.frame DF mit den Spalten A, B und C. Ich möchte nun alle Zeilen aus DF deren Spalte B den Wert "Closed" hat und in Kombination mit Zeile A doppelt vorkommt.

Mein Lösungsweg:

  1. Ein data.frame erzeugen, der nur die Spalten A und B enthält:
    sl <- data.frame(A = DF$A, B = DF$B)

  2. Filtern nach Zeilen, die nur B == "Closed" entsprechen:
    slc <- sl[which(sl$B == 'Closed'),]

  3. Im letzten Schritt die doppelten Zeilen ermitteln: slcd <- slc[duplicated(slc),]

Noch so ein R Problem gelöst. Wieder mit dem plyr API.

Ich habe ein data.frame mit den Spalten für Jahr, Monat und Laufzeit. Für jede Jahr-Monats Kombination gibt es eine unterschiedliche Anzahl von Laufzeitwerten. Die Laufzeit der Jahr-Monatskombination möchte ich nun in einem Reihenboxplot visualisiert darstellen.


    year month duration
    2016     6      345
    2016     6      378
    2016     6      501
    2016     6      390
    2016     6      333
    2016     6      321
    2016     5      401
    2016     5      378
    2016     5      390
    2016     5      331
    2016     5      345
    2016     4      411
    2016     4      428
    2016     4      190
    2016     4      231

Für den Boxplot muss ich die Daten allerdings anders organisiert haben. Die Daten für die Laufzeitwerte eines Monats müssen in jeweils einem Vector innerhalb einer Spalte gespeichert werden. Ausserdem benötige ich nicht alle Monate, sondern nur die Daten der beiden letzten Monate (5 und 6).


      5    6
    401  345
    378  378
    390  501
    331  390
    345  333
     NA  321

Um dies zu erreichen erzeuge ich zuerst eine leere Liste:


    li <- list()

Im Anschluss durchlaufe ich die Ursprungsliste in einer Schleife und weise die jeweiligen Laufzeitwerte der neuen Liste l zu:


    for (i in 5:6) {
        li[[i-4]] <- with(dur, dur$duration[dur$month == i])
    }

Die so erzeugte Liste aus Vektoren wandel ich jetzt in einen data.frame um:


    dur.df <- ldply(li, rbind)

Das Ergebnis sieht jetzt so aus:


      1   2   3   4   5   6
    401 378 390 331 345  NA
    345 378 501 390 333 321

MIttels des transpose Befehls t(…) kann die Matrix so angepasst werden, dass aus den Zeilen Spalten werden:


    dur.dft <- t(dur.df)

Damit kann jetzt der Boxplot mittels boxplot(dur.dft)erzeugt werden:

Unsicherheit

Da for Schleifen in R eher selten sind bleibt die Unsicherheit, ob die Lösung die eleganteste ist. Fürs Erste allerdings reicht es.


CSV Datei und Code zum selber ausprobieren.

Ich habe ein kleines Programm geschrieben, um CSV Dateien mit SQL Befehlen von der Kommenadozeile ad-hoc zu untersuchen: sql4csv. Ziel war es ein Query wie das folgende von der Bash Kommandozeile ab zu setzen:


    > scq select distinct author from books.csv

Das Ergebnis wird dann wieder in Form einer CSV Datei ausgegeben:


    author  
    Douglas Adams  
    Terry Pratchet

Bei der Entwicklung habe ich Wert auf einfache Verwendung gelegt. So soll es nicht nötig sein die Werttypen (integer, numeric oder Datumsangaben) der Spalten vor der Benutzung. Vielmehr soll dies das Tools möglichst selbst erkennen und die Spaltentypen entsprechend anpassen. Damit sollen auch Abfragen wie


    > scq select ABC from xyz.csv where VALUE < 10

möglich sind.

Für integer und numeric Typen ist das auch ohne grosse Probleme möglich. Kniffelig ist es bei Datums- und Zeitabgaben. Hier gibt es zig unterschiedliche Formate. Deswegen habe ich mich für einen pragmatischen Weg der Umsetzung entschieden: es werden aktuell die drei für mich wichtigsten Datumsformate für den Gregorianischen Kalender unterstützt :-)

Datums- und Zeitformate

  • Y - steht für das Jahr. Jahre werden immer vierstellig angegeben
  • M - steht für den Monat
  • D - steht für den Tag im Monat
  • h - steht für Stunden
  • m - steht für Minuten
  • s - steht für Sekunden

Datumsangaben

YYYY-MM-TT

ISO 8601 ähnlich.

Unterstützt wird auch eine sogenannte lazy Schreibweise, bei der auf einen führende Null (0) bei Zahlen unter 10 verzichtet werden kann. Dies bedeutet, dass auch ein Datum 2016-9-6 als 6. September 2016 erkannt wird.

MM/DD/YYYY

Amerikanische Datumsschreibweise.

Wie bei der ISO 8601 Schreibweise wird auch hier eine lazy Schreibweise ohne führende Null unterstützt.

DD.MM.YYYY

Entspricht der numerischen DIN 5008.

Wie bei der ISO 8601 Schreibweise wird auch hier eine lazy Schreibweise ohne führende Null unterstützt.

Zeitangaben

Zeiten werden immer mit einem Doppelpunkt getrennt. Dabei unterstützt sql4csv nur die Reihenfolge hh:mm:ss. Dieses Format allerdings auch lazy, also ohne führende Null und die Sekunde kann ebenfalls weggelassen werden. In diesem Fall wird die Sekunde intern auf Null gesetzt..

Zeitstempel

Zeitstempel sind eine Kombination aus Datum und Zeit. sql4csv erlaubt alle Kombinationen von Datums- und Zeitangaben. Dabei kann als Separator das ISO 8601 T verwendet werden oder ein Leerzeichen.

Beispiele

  • 2016-7-3 1:16
  • 8/30/1969T23:5:07
  • 11.11.2011 11:11:11

Implementierung

sql4csv wurde in Java 8 implementiert. Ich habe es auch geschrieben, um die neuen Sprachfeatures von Java 8 in der Praxis zu lernen und anzuwenden. So kommen intensiv Lambdas zum Einsatz und auch Methodenreferenzen. Die gesamte Datums- und Zeitbearbeitung basiert auf dem java.time API.

Als Datenbank verwende ich Derby als in-memory Datenbank. Ich verwende nicht SQLite - wie dies einige andere Implementierungen für ein solches Tool tun - da dies entweder eine vorhandene Installation von SQLite voraussetzt oder ich die Binaries für die unterschiedlichsten Plattformen mitliefern müsste. Das ist zu kompliziert und wird der Einfachheit des Werkzeuges nicht gerecht.

Probleme

SQL Queries und die Bash als Kommandozeileninterpreter vertragen sich nur mässig miteinander. Beispielsweise interpretiert die Bash das Asterisk Zeichen (*) anders als in SQL vorgesehen. Und diese Interpretation kann auch nicht einfach ausgeschaltet werden. Ein Query wie


    > scq select * from books.csv where author = 'Douglas Adams '

ist nicht möglich. Die Bash ersetzt das * Zeichen durch die Dateinamen des aktuellen Verzeichnisses. Als Abhilfe kann das Hilfstool scq-cols verwendet werden:


    > scq select $(scq-cols books.csv) from books.csv where author = 'Douglas Adams'

Das ist nicht schön, aber eine pragmatische Lösung. Nach einigen malen benutzen hatte ich mich daran gewöhnt.

Ebenso müssen runde Klammern (z.B. für Gruppierungen) und spitze Klammern <> für grösser und kleiner Vergleiche durch eine Backslash (\) maskiert werden. Insgesamt kann durch diese Einschränkungen die Lesbarkeit des Queries beeinträchtigt werden.

Download und Installation

Im sql4csv Repository auf Github findet ihr eine Download und Installationsanleitung, um das Tools selbst einmal ausprobieren zu können.

Mit meinem kleinen R Projekt geht es weiter. Ich hatte die folgende Situation: Ich habe die Zeitpunkte mehrere Statusübergänge eines Issues in einer denormalisierten Tabelle. Dabei kommt der Status created pro Issue exakt einmal vor. Zusätzlich habe ich noch die Dauer, die ein Issue in dem jeweiligen Status war. Ich will nun die Dauer und den created Zeitpunkt behalten. Alle anderen Zeitpunkte und auch sämtliche Status benötige ich im weiteren Verlauf nicht mehr.

Die Ausgangstabelle X sieht ungefähr so aus:


      IssueID                Date  Status Duration
            A 2016-08-09T14:33:09 created        0
            A 2016-08-19T09:21:11    open   845282
            A 2016-08-19T16:01:53  closed    24042
            A 2016-08-21T16:06:12    open   172800
            A 2016-08-25T08:13:45  closed   345600
            B 2015-09-23T12:22:21 created        0
            B 2015-09-25T07:11:01    open   172800
            B 2015-10-01T14:05:28  closed   518400

Ziel ist eine Tabelle, in der Duration der einzelnen Issues aufsummiert sind und das Date von created erhalten bleibt als CreationDate. Also in etwa so:


    IssueID,CreationDate,Duration
    A,2016-08-09T14:33:09,1387724
    B,2015-09-23T12:22:21,691200

Stellt sich heraus: das ist mit R nicht trivial zu lösen, aber auch nicht unmöglich.

1. Schritt

Im ersten Schritt erstelle ich ein Subset ausschliesslich dür den Status created:


    created.datetime <- subset(X, Status == "created", select = c("IssueID", "Date"))

2. Schritt

Im zweiten Schritt wird in die Tabelle X eine neue Spalte CreationDate eingefügt mit den Werten aus der Tabelle created.datetime:


    X$CreationDate <- created.datetime[match(X$IssueID, created.datetime$IssueID), 2]

3. Schritt

Im dritten und letzten Schritt verwende ich das plyr API, um die Tabelle zu reduzieren. Das API wurde zuvor installiert und mittels library(plyr) eingebunden.

Der entsprechende Code sieht wie folgt aus:


    reduced <- ddply(X,
                     c("IssueID", "CreationDate"),
                     function(t) data.frame(Duration = sum(t$Duration))
    )

Der Übersichtlichkeit halber habe ich den Code in 3 Zeilen dargestellt.

Das Ergebnis sieht wie folgt aus:


    > reduced
      IssueID        CreationDate Duration
    1       A 2016-08-09 14:33:09  1387724
    2       B 2015-09-23 12:22:21   691200

Fazit

Ich bin immer mehr begeistert von R. In Java hätte ich für diesen 3-Zeiler sicherlich ein vielfaches an Code produziert.


Zum selber ausprobieren der Sourcecode zum download.

Ich arbeite mich gerade ein wenig in R ein, da ich Daten aus CSV Dateien statistisch aus­werten möchte. Dabei habe ich viel mit Datums­angaben zu tun. Im Folgenden ein paar Notizen die ich mir bisher gemacht habe

Datentyp in Spalte eines data.frame ändern

Beispiel: in einem data.frame existiert die Spalte Date. Beim Einlesen aus einer CSV Datei wird diese als kategorialer String erkannt.


    Date,Status,Solution
    2016-08-09T14:33:09,created,fixed
    2015-08-09T14:33:09,blocked,won't fix

Einlesen: df <- read.csv("date.csv", header=T)

Mittels des Kommandos


    df$Date <- as.Date(df$Date)

werden alle Werte umgewandelt.

Datetime umwandeln

Die R Klasse Date repräsentiert nur ein Datum. Für Datetime sind die beiden Klassen POSIXct und POSIXlt vorhanden. Beide halten Daten unter­schiedlich, liefern ansonsten allerdings ähnliche Funk­tionalität.

Umgewandelt wird wie folgt:


    df$Date <- as.POSIXct(strptime(df$Date, "%Y-%m-%dT%H:%M:%S"))

Als Defaultformat wird das ISO 8601 lang Format (mit Binde­strichen und Doppel­punkten) verwendet. Aller­dings mit dem Unter­schied, dass nicht der Standard­delimiter T verwendet wird, sondern ein Leer­zeichen. Beispiel: 2016-08-09 14:33:09. Aus diesem Grund kommt die Funktion strptime zum Einsatz, der das entsprechende Muster ("%Y-%m-%dT%H:%M:%S") mit T als Delimiter über­geben wird. Wird dies nicht in dieser Art und Wiese gemacht, wird nur das Datum ermittelt und die Zeit auf 00:00:00 gesetzt.

Jahr aus Date ermitteln


    df <- within(df, YEAR <- as.numeric(format(df$Date, "%Y")))

Fügt gleichzeitig eine neue Spalte (YEAR) in den data.frame ein. Dies liesse sich auch mittels df$YEAR <- as.numeric(format(df$Date, "%Y")) erledigen. Der Vorteil von transform ist, das mehrere Änderungen gleichzeitig durchgeführt werden können:


    df <- within(df, {YEAR <- as.numeric(format(df$Date, "%Y")),
                      MONTH <- as.numeric(format(df$Date, "%m"))
                     })

Wochentag (numerisch) mit Basis 1 für Montag berechnen


    df <- within(df, DOW <- ifelse((d = as.numeric(format(df$Date, "%w"))) == 0, 7, d))

Funktion holt aus einem Datum eines data.frames den Wochentag (as.numeric(format(df$Date, "%w"))) und weisst ihn einer Hilfsvariablen zu. Diese wird verglichen mit 0, da POSIX Date eine 0 für Sonntag zurück gibt. Ist der Wert 0 wird eine 7 zurück geliefert, ansonsten der Wert der Hilfsvariablen.

Kalenderwoche ermitteln

  • Mittels der Funktion format(Sys.Date(), "%V") kann die ISO 8601 Kalenderwoche ermittelt werden.
  • Mittels der Funktion format(Sys.Date(), "%U") kann die US Kalenderwoche ermittelt werden.

Ausgabe ist chr. Mittels as.numeric(…) in Zahl umwandeln.

Snippet um neue ISO WEEK Column in df ein zu fügen


    df <- within(df, WEEK <- as.numeric(format(df$Date, "%V")))

Subset aus data.frame

Ein Subset aus Zeilen eines data.frame, bei denen die Zellen einer Column einem Wert entsprechen. Zwei mögliche Lösungen:


    rows1 <- df[df$Status == "created", ]
    rows2 <- subset(df, Status == "created")

Vorteil der subset Funktion: Mittels des Parameters select können Spalten definiert werden, die in rows2 übernommen werden sollen:


    rows2 <- subset(df, Status == "created", select = c("Date", "Solution"))


Neben R verwende ich auch noch Bash Kommandos und sql4csv für die Bearbeitung von CSV Dateien. Für die Datenerhebung verwende ich unter anderen jan.