Wühlen in der Vergangenheit (Linux-Magazin, Februar 2011)

Das Git-Repository des Perl-Projekts enthält alle Commits seit Larry Wall es 1987 aus der Taufe hob. Das Statistik-Tool R gewinnt aus den historischen Daten überraschende Informationen und stellt sie graphisch dar.

Es hat schon etwas erhebendes, im Flugzeug sitzend und ohne jegliche Internetverbindung auf einem 200-Dollar-Laptop die vollständige Historie des Perl-Kerns vor Augen zu haben. Welche Dateien hat Larry Wall anno 1987 eingecheckt? Wer schickte den ersten Patch ein? Was war darin enthalten? Das Kommando git log zeigt, wie in Abbildung 1 dargestellt, erste Ergebnisse sofort an und benötigt nur wenige Sekunden sich bis zum Anfang des Projekts vorzuspulen, selbst auf einem untermotorisierten Netbook. Oder was kam letzten Montag hinzu? Alle Informationen sind einem 120MB großen Repository versteckt, das git von git://perl5.git.perl.org/perl.git effektiv synchronisiert, falls die Internetverbindung wieder steht. Dass Git herkömmlichen Versionskontrollsystemen wie Subversion den Marsch bläst, wundert nur Unwissende.

Abbildung 1: Das Git-Repository des Perl-Projects enthält alle Commits seit Larry Wall 1987 es aus der Taufe hob.

Geballte Information

Diese geballte Informationsladung auf engstem Raum hilft nicht nur interessierten Programmierern, die Entwicklung des Projekts mitzuverfolgen. Moderne Statistik-Tools extrahieren daraus Trends und stellen sie graphisch ansprechend dar. Und selbst mit Shell-Bordmitteln lässt sich feststellen, dass seit 1987 genau 38.206 Commits passierten:

    $ git log --oneline | wc -l 
    38206

Die Option --oneline schrumpft das Ausgabeformat auf eine Zeile pro Commit. Für eine tiefergehende Auswertung hilft dies nicht viel weiter, da wertvolle Informationen wie modifizierte Dateien, das genaue Datum oder Autoren- und Commiter-Email nicht erscheinen. Listing 1 zeigt hingegen einen Aufruf von git log, der die Ausgabe entsprechend Abbildung 2 formatiert.

Listing 1: git-log-format.sh

    1 git log --name-status --date=raw \
    2         --pretty='format:commit,%ae,%at,%ce' \
    3 	>perl-git-log.txt

Abbildung 2: Mit zusätzlichen Optionen schrumpft git-log die Ausgabe auf ein Computer-freundliches Format.

Jeder Commit enthält in diesem Format eine oder mehrere Dateien, die Git unterhalb der Kopfzeile jeweils nach einem Change-Flag (M=modified, A=added, R=removed) zeilenweise auflistet. Kopfzeilen beginnen mit "commit,...", damit der später gebaute Parser sie leicht von Dateizeilen unterscheiden kann. Nach dem Aufruf von git-log-format.sh stehen also alle interessanten Daten in der Datei perl-git-log.txt, von wo sie Listing 2 aufschnappt und in ein CSV-Format umformt, das das Statistiktool R später unterstützt.

Perl als Hilfsarbeiter

Denn auch der Perl-Snapshot kann sich nicht nur immer auf die Sprache Perl beschränken. Im Bereich der Statistik glänzt die Sprache R ([1]) mit geschwindigkeitsoptimierten Datentransformationen, einer reichen Auswahl an Grafik-Bibliotheken und einem CPAN-ähnlichen Entwicklernetzwerk namens CRAN. In R geschriebene Skripts sind erstaunlich kompakt, allerdings dauert es einige Zeit, bis Neulinge die neuen Paradigmen und Datenstrukturen durchschauen. Perl hingegen glänzt im Umwandeln von Datenformaten, und deshalb arbeitet es heute mit dem Skript log2csv in Listing 2 nur als Zubringer, indem es die Logdaten des Git-Repositories in komma-separierte Einträge nach Abbildung 3 umformt.

Listing 2: log2csv

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use local::lib;
    04 use Text::CSV;
    05 
    06 my $logfile = "perl-git-log.txt.bz2";
    07 my $csvfile = "perl-git-log.csv";
    08 
    09 my $csv = Text::CSV_XS->new ( { binary => 1, eol => $/ } ) or
    10     die "Cannot use CSV: ", Text::CSV->error_diag();
    11 
    12 open my $logfh, "bzip2 -dc $logfile |" or die "$logfile: $!";
    13 open my $csvfh, ">$csvfile" or die "$csvfile: $!";
    14 
    15 my($dummy, $author, $time, $committer);
    16 
    17 $csv->print( $csvfh, ["time", "file", "author", "committer"] );
    18 
    19 while( <$logfh> ) {
    20     if( /^commit/ ) {
    21         chomp;
    22         ($dummy, $author, $time, $committer) = split /,/, $_;
    23     } elsif( /^(\w)\s+(.*)/ ) {
    24         my $file = $2;
    25         $csv->print( $csvfh, [$time, $file, $author, $committer] ) or
    26           die "print failed: ", Text::CSV->error_diag();
    27     }
    28 }
    29 
    30 close $logfh or die "$logfile: $!";
    31 close $csvfh or die "$csvfile: $!";

Abbildung 3: Die erzeugte CSV-Datei, die Commits in einzelne Dateien aufspaltet und die Grundlage zur statistischen Analyse in R darstellt.

Hierzu kämmt sich log2csv zeilenweise durch die vorher von 5MB auf 0.5MB komprimierte Logdatei perl-git-log.txt.bz2. Trifft es in Zeile 20 auf die Kopfzeile eines Commits, speichert sie dessen Eckdaten wie den Autor des Patchs, den Unix-Zeitstempel und die Email-Adresse des ausführenden Committers in drei außerhalb der while-Schleife deklarierten Variablen. Entdeckt Zeile 23 dann eine Zeile mit einem Dateivermerk (z.B. "M Dateiname"), trennt sie das vorangehende Flag mittels eines regulären Ausdrucks ab und speichert den Namen der modifizierten Datei in $file. Die Methode print des CPAN-Moduls Text::CSV in Zeile 25 reicht die durch Kommata getrennten Felder anschließend an die Ausgabedatei perl-git-log.csv weiter. Dort steht dann zu jeder modifizierten Datei im Repository die Felder Autor, Committer und Zeitstempel. Das CPAN-Modul Text::CSV (beziehungsweise die geschwindigkeitsoptimierte Version Text::CSV_XS) maskiert eventuell auftretende Sonderzeichen automatisch, damit das kommaseparierte Ausgabeformat intakt bleibt. Der Konstruktoraufruf in Zeile 9 setzt das Flag "binary", damit jegliche ASCII-Zeichen erlaubt sind. Die eol-Option legt den Zeilentrenner im Ausgabeformat fest und erhält den Wert $/ zugewiesen, also den in der jeweiligen Perl-Installation gültigen Zeilenumbruch.

Eine Testfahrt mit R

Die 8.5MB große CSV-Datei lässt sich nun mit dem Statistik-Tool R einlesen und weiterverarbeiten, nachdem dieses mit den im Abschnitt "Installation" weiter unten beschriebenen Kommandos installiert wurde. Der in Abbildung 4 gezeigte Testlauf mit R illustriert, wie das Tool von der Unix-Kommandozeile aus anläuft (der Interpreter heißt tatsächlich "R"). Nach einigen einführenden Informationen wie der aktuell laufenden Version wartet der Interpreter mit dem Prompt ">" auf Eingaben des Users.

Abbildung 4: Testlauf in R: Print-Befehl, die Datei "testprog.r" ausführen, R beenden.

Das obligatorische print("hello r") ruft die print-Funktion auf, die den ihr überreichten String in die Standardausgabe weiterleitet. Zu beachten ist, dass R bei Funktionsaufrufen stets auf Klammern beharrt und am Zeilenende eines Kommandos kein Semikolon steht. Die Ausgabe des print-Kommandos besteht aus einer Zeile, was R mit einem vorangestellten "[1]" anzeigt. Hier bestätigt sich gleich ein übler Verdacht: Arrays, oder "Vektoren", in R-Sprech, beginnen in R bei 1 und nicht bei 0. Um eines der heute vorgestellten R-Skripts abzuspielen, dient der Aufruf source("testprog.r") im R-Interpreter, wie die dritte Zeile in Abbildung 4 zeigt. Alternativ liegt der R-Distribution ein Programm namens Rscript bei, das man als #!/usr/bin/Rscript an den Kopf eines ausführbaren Skripts stellen kann, damit der Kernel beim Skriptstart automatisch den R-Interpreter aufruft und ihm den Programmcode zur Ausführung übergibt. Eine interaktive Interpreter-Session beendet der User mit der Funktion q(), worauf R nachfrägt, ob es den aktuellen Interpreterstatus auf der Festplatte ablegen soll. Antwortet der User mit 'y', schreibt R den Wert aller soweit bekannten Daten in ein Verzeichnis .RData und liest sie nach einem Neustart von R wieder ein, damit der User genau dort fortfahren kann, wo er vorher aufgehört hat.

R jongliert mit Daten

Das Kommando read.csv() in Abbildung 5 liest die CSV-Datei in R ein. Punkte in Funktions- oder Variablennamen in R dienen lediglich der Strukturierung und haben keine tiefergreifende syntaktische Bedeutung. Die Funktion gibt im Erfolgsfall eine Datenstruktur vom Typ Dataframe zurück, einer Art Datenbanktabelle. Die Spalten geben die innere Struktur vor und die Zeilen jeweils einen Datensatz. Die Zuweisung zu der Variablen commits erfolgt mit dem eigenartigen Operator "<-", den R-Puristen dem funktional identischen "=" vorziehen, damit man Zuweisungen nicht mit Vergleichen ("==") verwechselt.

Abbildung 5: In R lässt sich die .csv-Datei leicht einlesen und in eine Datenstruktur verwandeln.

Nachdem die kompletten Daten aus 23 Jahren Perl-Entwicklung nach einigen Sekunden Bedenkzeit in der Variablen commits liegen, gibt der Aufruf head(commits) in Abbildung 5 die ersten sechs Datenreihen auf. Entsprechend brächte tail(commits) die letzten Zeilen zum Vorschein. Um die Spalte files des Dataframe in einen Vektor zu übertragen, weist das erste Kommando in Abbildung 6 den Ausdruck commits$file der Variablen files zu. Anschließend liegen in files genau 139.442 Dateinamen, wie der Aufruf length(files) zeigt. Darunter befinden sich viele doppelte Einträge und die Funktion levels() extrahiert später einen Vektor, in der jede Datei genau einmal vorkommt. Ein length()-Aufruf bringt zutage, dass es sich um 16429 unterschiedliche Datein handelt, die seit dem Anfang der Perl-Entwicklung erzeugt, modifiziert oder gelöscht wurden.

Abbildung 6: Erste Schritte in R mit den importierten CSV-Daten aus dem Perl Git-Repository.

Frequenzzähler

Die R-Funktion table() nimmt einen Vektor entgegen und gibt eine Datenstruktur zurück, die jedem eindeutigen Element einen Zähler zuweist, der angibt, wie oft das Element im Vektor enthalten ist:

    > data=c("one", "two", "three", 
             "two", "one", "two")
    > table(data)
    data
      one three   two 
        2     1     3

Das Codestück oben zeigt außerdem, wie R mit der Funktion c() (von "concatenate") einen Vektor aus Einzelelementen zusammenbaut. Die zweite Kommandozeile in Abbildung 6 baut das eben Gelernte in einer Zeile zusammen und zeigt an, welche Dateien im Repository am häufigsten verändert wurden. Dazu klassifiziert table(files) die in den Commits aufgelisteten Dateien und legt für jede einen Zähler an, der die Anzahl ihrer Nennungen aufkumuliert. Die Funktion sort() sortiert die table()-Zähler dann in aufsteigender Reihenfolge und tail mit der Option n = 20L holt die letzten 20 Einträge, also die mit den höchsten Zählern hervor. Wie im interaktiven R-Interpreter üblich, zeigt dieser das Ergebnis schön strukturiert an, falls der Rückgabewert einer Funktion keiner Variablen zugewiesen wurde. R verfügt außerdem über eine brauchbare Hilfefunktion, falls man ein Fragezeichen, gefolgt von einer Funktion (z.B. ?tail) eingibt.

Ein Bild sagt mehr

Diese kurze Einführung in R sollte genügen, um einige interessante Graphen zu zeichnen, die die Aktivitäten im Perl-Repository erhellen. Listing 3 erzeugt aus der Datenstruktur mit den 20 meistgeänderten Dateien ein Diagramm als Datei im PNG-Format. Zeile 6 bereitet die Ausgabe der nachfolgenden plot-Funktion durch png(file="files.png") vor und leitet das Diagramm demnach in die Datei files.png um. Ohne diese Zeile, würde R ein Grafikfenster entfesseln und das Schaubild dort ohne Umschweife anzeigen. Die ab Zeile 7 aufgerufene Funktion plot aus dem R Standardrepertoire macht klar, dass R keine ellenlangen Parameterlisten braucht, um professionell aussehende Diagramme zu malen. Achsenbeschriftung, maximale und minimale Werte, für alles findet es sinnvolle Standardwerte. Dies heißt jedoch nicht, dass R unflexibel wäre, im Gegenteil, jedes Detail einer Grafik von der Achsenform, -lage, Anzahl der in der Skala eingetragenen Werte, Farben, Fonts, usw. kann der User nach Gusto umdefinieren. Parameter nehmen R-Funktionen im Format par=value, durch Kommata getrennt, entgegen. Abbildung 7 zeigt den Dataframe data im Histogrammformat.

Listing 3: file-plot.r

    1 commits <- read.csv("perl-git-log.csv")
    2 files <- commits$file
    3 data=tail(sort(table(files)), n = 20L)
    4 data=rev(data)
    5 png(file="files.png")
    6 plot(data, type="h", main="File Commits in Perl Git Repo", 
    7      xlab="Most modified files", 
    8      ylab="Number of commits per file")

Abbildung 7: Meistmodifizierte Dateien als Graph.

Zahn der Zeit

Um die Aktivität im Repository über die vergangenen 23 Jahre aufzuzeigen, erweitert Listing 4 die im Unix-Sekunden-Format vorliegende Zeitstempelkolumne commits$time um in eine neue Spalte commits$year, die das 4-stellige Jahr des jeweiligen Zeitstempels anzeigt. Hierzu wandelt die eingebaute R-Funktion as.POSIXlt() den Unix-Zeitstempel unter Angabe des Referenzdatums 1970-01-01 in den nativen Datumstyp POSIXlt (POSIX "local time") um. Aus diesem extrahiert dann die Funktion format() unter Angabe des Platzhalters "%Y" die vierstellige Jahreszahl des jeweiligen Datums. Diese neue Kolonne mit 140.000 Jahreszahlen transformiert die Funktion table() dann in eine Datenstruktur, die zu jeder Jahreszahl einen Zähler enthält. Die in Zeile 14 aufgerufene Funktion plot() ist dann so schlau, die Datenstruktur ohne weitere Angaben in das Diagramm in Abbildung 8 zu pferchen. Listing 4 gibt lediglich noch die beiden Achsenbeschriftungen "Year" und "Files Modified" vor.

Wer genau hinsieht, bemerkt, dass die Linien die eingezeichneten Datenpunkte nicht berühren, sondern kurz vorher aus- und hinterher wieder einsetzen. Dies ist ein Merkmal der Option type="b", wer statt dessen ununterbrochene Linien bevorzugt, nimmt "o". Weitere Optionen finden sich auf der Manualseite, die auf das Kommando ?plot hin im R-Interpreter erscheint. Dort erfährt der Interessierte dann auch, dass plot() keineswegs nur table()-Ausgaben druckt, sondern auch mit zwei Vektoren für die x- und y-Werte des Graphen arbeitet. Allgemein versucht R, zu erraten, was der User meinen könnte und nimmt diesem dabei oft erstaunlich viel Arbeit ab.

Listing 4: files-per-year.r

    01 commits <- read.csv("perl-git-log.csv")
    02 
    03 commits$year <- format(
    04   as.POSIXlt(
    05     commits$time, 
    06     origin="1970-01-01"), 
    07   "%Y"
    08 )
    09 
    10 files.per.year <- table( commits$year )
    11 
    12 png(file="files-per-year.png")
    13 plot( files.per.year, 
    14       xlab="Year", ylab="Files Modified", type = "b" )

Abbildung 8: Anzahl der pro Jahr modifizierten Dateien

Ein aufwändiges Bild sagt noch mehr

Abbildung 10 zeigt die zehn fleißigsten Perl-Autoren und ihre Aktivitäten über die im Repository erfassten 23 Jahre Perl. Da es Git 1987 noch nicht gab, wurden die Daten natürlich rückwirkend aus dem bis dato benutzten Versionskontrollsystem eingespielt. Listing 5, dessen R-Code das Mehrfachdiagramm erzeugt, sucht zunächst die fleißigsten Autoren und merkt sich nur diejenigen, die für mehr als 5000 File-Commits verantwortlich zeichnen. Die Funktion subset() erledigt dies elegant mittels subset(au,au > 5000) in Zeile 14. Die Variable au ist vom Datentyp table mit allen Autoren aller Commits. Die als zweiter Parameter hereingereichte Bedingung filtert alle nicht darauf passenden Einträge aus. Als Spezialität von R gelten Vektoroperationen wie au > 5000, die nicht nur kurz und bündig, sondern auch hocheffizient Massenoperationen vornehmen.

Listing 5: authors-by-year.r

    01 library("lattice")
    02 
    03 commits <- read.csv("perl-git-log.csv")
    04 
    05 commits$year <- format(
    06   as.POSIXlt(
    07     commits$time, 
    08     origin="1970-01-01"), 
    09   "%Y"
    10 )
    11   # Authors with more than 5000 
    12   # file commits
    13 au=table(commits$author)
    14 au = sort(subset( au, au > 5000 ))
    15 
    16 files.by.auth.year = 
    17       table( commits$author, commits$year )
    18 files.by.auth.year = 
    19       as.data.frame( files.by.auth.year )
    20 names( files.by.auth.year ) = 
    21       c("author", "year", "files")
    22 
    23 files.by.auth.year = subset( 
    24   files.by.auth.year,
    25   files.by.auth.year$author %in% names(au)
    26 )
    27 
    28 png(file="authors-by-year.png")
    29 xyplot( files ~ year | author, 
    30     data = files.by.auth.year, 
    31     layout = c(1, 5), 
    32     scales = list(x = list(rot = 45)), 
    33     type = "l",
    34     xlab = "Year",
    35     ylab = "File Commits",
    36     title = "Authors with > 5000 Commits"
    37 )

Zeile 17 ruft table() mit zwei Parametern, commits$author und commits$year, auf und erzeugt damit eine Datenstruktur, die allen Kombinationen aus Autor und Jahr einen Zähler zuordnet. Zum einfacheren Zeichnen formt Zeile 18 mittels as.data.frame() einen Dataframe und Zeile 20 weist den bis dato noch unbenamten Kolumnen durch einen linksseitigen names()-Aufruf die Namen "author", "year" und "files" zu. Zu diesem Zeitpunkt enthält die Variable files.by.auth.year noch die Daten aller Autoren, aber Zeile 23 extrahiert daraus die Untergruppe der vorher in au ermittelten fünf fleißigsten Autoren und weist das Ergebnis wieder files.by.auth.year zu. Das hintere Ende des Zwischenergebnisses zeigt Abbildung 9.

Abbildung 9: Das hintere Ende des Dataframes kurz vor dem Plotten.

Die etwas komplexere Grafik zeichnet diesmal nicht plot(), sondern die Funktion xyplot() aus der Grafik-Library lattice verantwortlich, die Zeile 1 mit library("lattice") vorher eingebunden hat. Dies importiert übrigens auch die Manualseiten dieser R standardmäßig beiliegenden Library, sodass ?xyplot anschließend wertvolle Informationen über die erstaunliche Menge an Parametern, die diese Funktion verträgt, ans Licht bringt.

Am wichtigsten ist der erste Parameter, der im Format

    y ~ x | g

vorliegt, wobei x und y jeweils einen Vektor mit x- bzw. y-Werten enthalten und g die verschiedenen Gruppen angibt, für die jeweils ein eigenes Diagramm zu zeichnen ist. Im vorliegenden Fall weisen alle drei in den Dateframe files.by.auth.year, der mit im Parameter data übergeben wird. Das Layout legt mit c(1,5) fest, dass pro Display fünf Diagramme übereinander liegen, mit jeweils einem pro Reihe.

Da die Jahreszahlen am unteren Ende der Grafik dicht gedrängt stehen und sich aufgrund ihrer Länge ins Gehege kämen, dreht der scales-Parameter sie in Zeile 32 kurzerhand um 45 Grad. type=l legt den Linientyp der Grafiken fest und xlab bzw ylab bestimmen die Achsenlegende.

Jedes Panel in Abbildung 10 ist dann einem der fünf fleißigsten Autoren zugeordnet und die Histogrammbalken zeigen jeweils die Anzahl der vom Committer modifizierten Dateien innerhalb des jeweiligen Jahres an. So lässt sich die Zeitspanne ermitteln, in denen legendäre Perl-Autoren wie Gurusamy Sarathy, die sich heute aus dem aktiven Geschäft verabschiedet haben, tätig waren.

Abbildung 10: Committer mit mehr als 5000 File Commits und ihre aktiven Jahre.

Installation

Ubuntu installiert den R-Interpreter mit dem Kommando

    sudo apt-get install r-base-core

und die zur Aufbereitung der Daten genutzten Perl-Module stehen ebenfalls schon fertig bereit, als libtext-csv-perl und libtext-csv-xs-perl. Eine wahre Fundgrube an Information, so ein Git-Repo.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/02/Perl

[2]

"The R Project for Statistical Computing", http://www.r-project.org/

[3]

"Introduction to Scientific Programming and Simulation Using R", Owen Jones, Robert Maillardet, Andrew Robinson, Chapman and Hall/CRC, 2009

[4]

"Lattice: Multivariate Data Visualization with R (Use R)", Deepayan Sarkar, Springer, 2008

[5]

http://blog.moertel.com/articles/2007/06/21/talk-fun-with-numbers-r-and-perl-and-imdb-data

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.