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. |
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.
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.
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.
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.
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.
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. |
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.
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.
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. |
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.
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 |
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.
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. |
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.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/02/Perl
"The R Project for Statistical Computing", http://www.r-project.org/
"Introduction to Scientific Programming and Simulation Using R", Owen Jones, Robert Maillardet, Andrew Robinson, Chapman and Hall/CRC, 2009
"Lattice: Multivariate Data Visualization with R (Use R)", Deepayan Sarkar, Springer, 2008
http://blog.moertel.com/articles/2007/06/21/talk-fun-with-numbers-r-and-perl-and-imdb-data
Michael Schilliarbeitet 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. |