Nicht nur hippe Gadgets, auch ältere Geräte im Haushalt wie Personenwaagen oder Laserdrucker erfassen wertvolle Daten. Manuell extrahiert und grafisch ansprechend über die Zeit aufbereitet bieten sie interessante Einblicke in Gewohnheiten und Befindlichkeiten des Perlmeisters.
Big Data erfasst unsere Bewegungen mittels Mobiltelefon, so genannte Wearables messen Körperparameter wie Puls und Blutdruck, Sensoren rund ums Eigenheim melden, wer kommt und geht. Wer gerne mit solchen Daten spielt, dürfte diese Möglichkeiten bei dienstälteren Geräten wie etwa einer Personenwaage vermissen, die zwar klaglos ihren Dienst tut, aber keinen Webserver mit API bietet. Mit einem einfachen Low-Tech-Ansatz und halbautomatischer Methode holt der findige Datenfreund auch aus diesen Oldtimern genug Informationen, um ansprechende Grafiken zu zeichnen, historische Nutzerdaten zu erfassen und daraus zukünftige Trends abzuleiten.
Mein Multifunktionsdrucker MFC-7450 von Brother bietet zwar keine digitale Schnittstelle zu seiner Reportfunktion, mich interessiert aber trotzdem, wie viele Seiten Papier so pro Monat durchrattern. Daraus kann ich ableiten, wann ich neue Laserkartuschen nachbestellen oder ob ich in meinem Haushalt eventuell lautstark verschwenderischen Umgang mit wertvollen Ressourcen anprangern muss.
Halbautomatik erinnert
Die Anzahl der gedruckten Papierseiten kann ich zwar nicht automatisch auslesen, aber wenn ich mich einmal im Monat durch das Menü auf dem Display hangel, um den lebenslangen Papierzähler des Druckers abzulesen (Abbildung 1) und den aktuellen Stand aufzuschreiben, lassen sich daraus atemberaubende Verbrauchskurven malen. Die Krux ist freilich, regelmäßig daran zu denken, den Zähler abzulesen, aber dazu gibt es zum Glück automatische Erinnerer.
Hierzu nutze ich den in [2] vorgestellten Tickler-Mechanismus und den digitalen Turbo-Notizblock Evernote, der mir einmal im Monat den Eintrag mit den bislang notierten Zählerständen schickt. Ich füge den neuesten Stand hinzu und schicke das Ganze wieder schlafen (Abbildung 2). Ein entsprechender wandernder Kalendereintrag und eine Dropbox-Datei oder ein Google-Spreadsheet tun es natürlich auch – Hauptsache, der Operateur erfasst die Daten regelmäßig und speichert sie permanent in der Cloud.
Exportieren und ausfieseln
Evernotes »Export« -Funktion extrahiert Notizen (Abbildung 3) im Enex-Format (einem XML-Dialekt) und das Perl-Skript in Listing 1 fieselt die Einträge heraus, die im Format Datum/Zählerstand in »<LI>« -Elementen stehen. Der Aufruf »en-extract note.enex« erzeugt Daten im CSV-Format, die Skriptsprachen später leicht maschinell weiterverarbeiten können. Hierzu sucht das CPAN-Modul HTML::TreeBuilder::Xpath mit dem Xpath-Ausdruck »//li« nach »<LI>« -Elementen in den XML-Daten, um die in Listen abgelegten Datumsangaben und Papierzählerstände auszufiltern.
Listing 1
en-extract
01 #!/usr/bin/perl -w
02 use strict;
03 use HTML::TreeBuilder::XPath;
04 use Encode qw( _utf8_on );
05
06 my $data = join "", <>;
07 _utf8_on( $data );
08 my $tree= HTML::TreeBuilder::XPath->new;
09 $tree->parse( $data );
10
11 my( @content ) = $tree->findvalues("//li");
12
13 for my $line ( @content ) {
14 $line =~ s/[#\x{c2}\x{a0}\s]+/ /g;
15 next if $line !~ /^\d/;
16 my( $date, $val ) = split " ", $line;
17 next if !defined $val;
18 print "$date,$val\n";
19 }
Dieser Ansatz spart viel Zeit beim Programmieren, denn der Xpath-Parser sucht einfach nach Listenelementen in beliebiger Verschachtelungstiefe. Das Skript braucht sich also nicht darum zu kümmern, welche XML-Elemente zum Ziel führen, da nur der tief verschachtelte Inhalt interessiert. Evernote fügt gerne irgendwelche närrischen Sonderzeichen in die Notizen ein, die Zeile 14 wieder verwirft. Auch etwaige Kommentare fliegen raus, da Zeile 15 darauf besteht, dass jede Datenzeile mit einer Ziffer beginnt.
Ohne Murren mit UTF-8
Die »split« -Funktion in Zeile 16 spaltet Datumsangabe und Zählerstand in zwei Elemente und Zeile 18 druckt beide kommasepariert aus. Zum Einlesen der exportierten Evernote-Datei von der Standardeingabe dient der Operator »<>« , die danach aufgerufene Funktion »_utf8_on« aus dem CPAN-Modul Encode macht dann einen UTF-8-String daraus. In dem vorliegenden Fall liegen dort zwar keine UTF-8-, sondern schlichte Ascii-Daten, aber der Xpath-Parser nörgelt herum, falls ihm ein Ascii-String übergeben wird, also gibt das klügere Skript nach.
Die so aufpolierten Daten stellen eine Zeitserie dar (Time Series, Abbildung 4), denn sie tragen Funktionswerte über die Zeit auf, und zu deren grafischer Darstellung bieten allerlei Produkte mehr oder weniger ausgereifte Schnittstellen, sogar Excel-Spreadsheets halten entsprechende Funktionen für Büromäuse parat. Einige CPAN-Module widmen sich dem Thema, etwa Chart::Clicker [3] oder das Google-Chart-API [4], aber wie frühere Ausgaben des Perl-Snapshots belegen, geht es in der Sprache R wohl am schnellsten mit nur wenigen Zeilen Code [5].
R mächtiger als Perl
Listing 2 zeigt das kurze R-Skript, das die CSV-Daten von der Standardeingabe einliest und sie in dem Dataframe »data« ablegt. Zeile 7 dreht die Reihenfolge der Daten, die chronologisch absteigend vorliegen (der menschliche Erfasser ist faul und schreibt den neuen Zählerstand immer an den Kopf der Datei), in eine chronologische Reihenfolge und weist das Ergebnis wieder »data« zu. Zeile 6 liest die Datumsangaben in der ersten Spalte ein und konvertiert sie in das R eigene Datumsformat. Die »png()« -Funktion legt für die Ausgabe des Graphen das PNG-Format fest, die »plot()« -Funktion in Zeile 11 bewirkt mit »value~date« , dass der Graph die Variable »value« über der Zeitachse mit dem Datum darstellt. Typ »”l”« zeichnet Linien zwischen den Datenpunkten und der Parameter »col« legt dafür die Farbe Blau fest.
Listing 2
timeseries.r
01 #!/usr/bin/env Rscript
02 args<-commandArgs(TRUE)
03 data <- read.csv(file="stdin",
04 col.names=c("date","value"))
05
06 data$date <- as.Date(data$date, "%Y-%m-%d")
07 data <- data[order(data$date,
08 decreasing=FALSE),]
09
10 png(file="timeseries.png")
11 plot(value ~ date,data,col="blue",
12 xaxt="n",type="l",ylab=args[1])
13 axis(1, data$date,
14 format(data$date, "%Y"), cex.axis=2)
Eigentlich erzeugt R die Achsenbeschriftung automatisch, aber da der Graph in Abbildung 5 eine Zeitachse wünscht, die nur die Jahreszahl des jeweiligen Datums enthält, stellt der Parameter »xaxt=”n”« die Automatik ab. Für die Beschriftung sorgt die Funktion »axis()« (Zeile 13), die mit »1« die untere Achse anspricht und das Datum »$date« mit »%Y« auf das Jahr formatiert sowie mit »cex.axis« die Fontgröße verdoppelt.
Das Skript nimmt als Kommandozeilen-Parameter die Beschriftung der y-Achse entgegen. Der Aufruf »timeseries.r paper« am Ende der Konvertierungs-Pipeline erzeugt in der Datei »timeseries.png« den Graphen in Abbildung 5. Er zeigt einen linearen Anstieg des Papierverbrauchs über die Zeit, also ist der monatliche Verbrauch etwa konstant. Noch kein Grund für mahnende Ansprachen!
Waagen lügen nicht
Wie dem Laserdrucker fehlt auch der Badezimmerwaage (Abbildung 6) eine digitale Report-Schnittstelle. Auch hier hilft nur, das persönliche Gewicht einmal pro Woche abzulesen, die Daten in Evernote abzulegen und sie regelmäßig per Cronjob zu extrahieren und grafisch aufzubereiten. Das R-Skript in Listing 2 tut auch hier Dienst, da es sich wieder um Zeitreihen mit Zahlenwerten handelt. Mit »timeseries.r weight« am Ende der Pipeline mit konvertierten Gewichtsangaben aus Evernote aufgerufen, erzeugt sie das Diagramm in Abbildung 7.
Über die Jahre haben sich bei einmal wöchentlichem Wiegen etwa 140 Datenpunkte angesammelt. Das Körpergewicht schwankt beträchtlich, sodass es anzuraten wäre, die Funktion etwas zu glätten, um etwaige Tendenzen besser ablesen zu können. Nichts leichter als das: R bietet mit »loess()« so genanntes “Local Polynomial Regression Fitting” an. Dabei legt es eine Polynomkurve an, die den Daten einigermaßen nahe kommt, sich aber nur ganz langsam schlängelt. Listing 3 zeigt die drei Zeilen R-Code, die in Listing 2 – vor dem Aufruf der »plot« -Funktion eingepflanzt – die »value« -Spalte des Dataframe durch geglättete Werte ersetzen.
Listing 3
smooth.r
1 data$id <- seq.int(nrow( data )) 2 lo <- loess( data$value ~ data$id ) 3 data$value <- predict( lo )
Da die Regressionsfunktion nicht mit Datumsangaben umgehen kann, erzeugt die erste Zeile in Listing 3 eine neue Spalte »id« im Dataframe, die die einzelnen Datenwerte in »value« von 1 an durchnummeriert. Die zweite Zeile ruft dann die eingebaute Funktion »loess()« auf, die mit »data$value ~ data$id« eine Regressionskurve der Datenwerte über die Sequenznummern aufbaut. Die dritte Zeile ersetzt auf einen Schlag alle Werte in »value« durch jene, die aus der Funktion »predict()« purzeln, also die Werte des Regressionspolynoms.
Wie erwartet zeigt Abbildung 8 eine deutlich besser zu analysierende Kurve. Damit die sichtbare Tendenz sich nicht fortsetzt, ist der Leibesfülle des Autors offensichtlich bald Einhalt zu gebieten.
Daten vom Netz
Um die Graphen automatisch per Cronjob zu erzeugen, kann niemand die »Export« -Funktion der Evernote-Applikation bedienen, das Evernote-API muss ran. Diese Oauth-basierte Webschnittstelle bietet Entwicklern, die in ihren eigenen Evernote-Daten herumfuhrwerken wollen, eine vereinfachte Authentifizierung in Form eines Developer-Token [6] an. Mit dem CPAN-Modul Net::Evernote::Simple und dem in der Datei »~/.evernote.yml« abgelegten Token ist es recht einfach, lesend und schreibend auf die Evernote-Daten zuzugreifen. Listing 4 zeigt den Zugriff auf eine Notiz mit einem ab Zeile 19 definierten Suchfilter, der den Evernote-Server dazu veranlasst, aus unter Umständen Tausenden von Einträgen den richtigen herauszufieseln.
Listing 4
en-fetch
01 #!/usr/local/bin/perl
02 use strict;
03 use warnings;
04 use Net::Evernote::Simple 0.07;
05
06 my( $pattern ) = @ARGV;
07 die "usage: $0 pattern"
08 if !defined $pattern;
09
10 my $en = Net::Evernote::Simple->new();
11
12 if( ! $en->version_check() ) {
13 die "Evernote API version out of date!";
14 }
15
16 my $note_store = $en->note_store() or
17 die "getting notestore failed: $@";
18
19 my $filter = $en->sdk(
20 "EDAMNoteStore::NoteFilter" )->new(
21 { words => $pattern } );
22
23 my $offset = 0;
24 my $max_notes = 1;
25
26 my $result = $note_store->findNotes(
27 $en->{ dev_token },
28 $filter,
29 $offset,
30 $max_notes
31 );
32
33 for my $hit ( @{ $result->{ notes } } ) {
34 my $note = $note_store->getNote(
35 $en->{ dev_token }, $hit->{ guid }, 1 );
36 print $note->{ content };
37 }
Da die Notiz mit den Gewichtseinträgen als einzige den Titel “Scale Weight” hat, sucht der Aufruf
$ en-fetch '"Scale Weight"'
genau diese heraus, denn wegen der doppelten Anführungszeichen (noch einmal durch einfache maskiert, damit die Shell sie nicht wegputzt) wünscht die Anfrage nur exakte Treffer.
Genau ein Treffer
Das CPAN-Modul für das Evernote-API fußt auf mittels Thrift generiertem Perl-Code, der aus dem offiziellen Evernote-API stammt. Zeile 12 prüft zunächst, ob der Server die laufende Clientversion überhaupt noch unterstützt oder ein Upgrade fordert. Die Methode »note_store()« in Zeile 16 erzeugt dann ein Objekt vom Typ »EDAMNoteStore« , das später auf den Evernote-Speicher eines Users zugreift. Zeile 19 definiert wie erwähnt einen neuen Filter, den Anfragen an den Server mitbringen, damit die nur passende Notizen liefern. Eine der Filtermöglichkeiten [7] des API ist die nach Stichworten mit dem Parameter »words« , weitere wären Tags oder Notebooks genannte Verzeichnisse, in denen Notizen liegen.
Da solche Anfragen unter Umständen viele Treffer liefern, verlangt die Methode »findNotes()« in Zeile 26 noch die Angabe der Maximalzahl der Treffer sowie mit »$offset« den Start der Paginierung, falls der Client die Ergebnisse in Schüben einholt und den nächsten Schub verlangt. Da im vorliegenden Fall die Such-Query so formuliert sein sollte, dass sie genau auf eine Notiz passt, setzt Listing 4 »$offset« auf »0« und die Maximalzahl auf »1« . Als ersten Parameter verlangt »findNotes()« den Developer-Token, den das CPAN-Modul anfangs aus »~/.evernote.yml« gelesen hat und nun unter dem Hashschlüssel »dev_token« parat hält.
Die »for« -Schleife ab Zeile 33 iteriert über die Suchtreffer, extrahiert zu jeder gefundenen Notiz deren eindeutige »guid« und holt damit und mittels »getNote()« den Inhalt der Notiz vom Server. Im Feld »content« steht dann der XML-Salat, den »en-extract« (Listing 1) in eine Zeitserie im CSV-Format transformiert. Der letzte Teil der Pipeline zeichnet wie vorher den Graphen.
Blutdruck
Mit einem preiswerten Gerät der Firma Omron (Abbildung 9) messe ich alle paar Wochen den Blutdruck und schreibe die beiden Werte – den höheren systolischen und den niedrigeren diastolischen – in eine Evernote-Datei. Über die Jahre gesammelte Daten ergeben dann so nette Diagramme wie in Abbildung 10.
Neben den eingangs erwähnten Alternativen zum Zeichnen der Graphen von Zeitserien kennen bereits ergraute Leser vielleicht noch das ehrwürdige RRDTool. Es verlangt einige Klimmzüge beim Einfüttern der Daten und das Format der Beschreibungssprache ist etwas maschinennah und unleserlich.
Listing 5
ts2epoch
01 #!/usr/bin/perl -w
02 use strict;
03 use DateTime::Format::Strptime;
04
05 my $strp = DateTime::Format::Strptime->new(
06 pattern => "%Y-%m-%d",
07 );
08
09 while( <> ) {
10 chomp;
11 my( $date, $value ) = split /,/, $_;
12 my $dt = $strp->parse_datetime( $date );
13 printf "%d,%s\n", $dt->epoch, $value;
14 }
Das CPAN-Modul RRDTool::OO gestaltet den Vorgang augenfreundlicher, aber die Tatsache bleibt, dass RRDTool seine Daten in streng chronologischer Reihenfolge erwartet und schon beim Anlegen einer Datenbank den Zeitstempel des ersten Wertes wissen muss. Zudem nimmt es die Zeitangaben in Unix-Sekunden seit 1970 an, also konvertiert Listing 5 hereinkommende Daten mit Zeitstempeln im Format JJJJ-MM-TT mit Hilfe des CPAN-Moduls DateTime in Epoch-Sekunden, ein nachfolgendes »sort« in der Unix-Pipeline bringt sie in chronologische Reihenfolge für die RRD-Auswertung in »ts2rrd« aus Listing 6:
Listing 6
ts2rrd
01 #!/usr/bin/perl -w
02 use strict;
03 use RRDTool::OO;
04
05 my @points = ();
06
07 while( <> ) {
08 chomp;
09 my($time, $value) = split /,/, $_;
10 push @points, [$time, split /:/, $value];
11 }
12
13 my $rrd = RRDTool::OO->new(
14 file => "ts.rrd" );
15
16 $rrd->create(
17 step => 3600*24*45,
18 start => $points[0]->[0] - 1,
19 data_source => { name => "high",
20 type => "GAUGE" },
21 data_source => { name => "low",
22 type => "GAUGE" },
23 archive => { rows => 10_000 });
24
25 for(@points) {
26 $rrd->update(
27 time => $_->[0],
28 values => [ @{ $_ }[1,2] ] );
29 }
30
31 $rrd->graph(
32 width => 600,
33 height => 400,
34 image => "blood-pressure.png",
35 vertical_label => "Blood Pressure",
36 start => $points[0]->[0],
37 end => $points[-1]->[0],
38 draw => {
39 legend => "systolic",
40 dsname => "high",
41 type => "area",
42 color => "FF0000",
43 },
44 draw => {
45 legend => "diastolic",
46 dsname => "low",
47 type => "area",
48 color => "00FF00",
49 }
50 );
[...] | ts2epoch | sort | ts2rrd
Der Graph-Generator sammelt zunächst Datenpunkte wie »2016-01-01 120:80« in einem Array von Arrays namens »@points« , um später das Datum des ersten Datenpunkts für RRDTools »create« -Methode parat zu haben, sowie dann beim Zeichnen des Graphen die Zeitstempel des ersten und des letzten Datenpunkts. Er definiert zwei von RRDTools »data_source« -Strukturen, eine für den diastolischen und eine für den systolischen Blutdruck über die Zeit. Jeder Aufruf der »update()« -Methode in Zeile 26 füttert dann den Zeitstempel des Datenpunkts und auch die beiden Werte für den Blutdruck in die Round-Robin-Datenbank.
Ohne Datenloch
Da RRDTool Alarm schlägt, falls ein Datenpunkt ausbleibt, definiert Zeile 17 den Parameter »step« und setzt ihn auf 45 Tage. Da die Messwerte einmal im Monat eintrudeln, entsteht so kein Loch.
Beim Zeichnen des Diagramms ab Zeile 31 definieren die »draw« -Parameter zwei verschiedene Graphen für die beiden Blutdruckwerte, wobei der systolische Wert ein rotes Rechteck (»area« , »FF0000« ) zugewiesen bekommt, der diastolische Werte ein grünes (»00FF00« ). Die Ausgabe erfolgt in eine Bilddatei namens »blood-pressure.png« (Abbildung 10). Damit zeigen sich Entwicklungen, die beim regelmäßigen Messen ohne statistische Analyse vielleicht unter den Tisch fallen würden. “Pumperlgsund” – laut Zeitreihen! (uba)
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2016/11/plus
Infos
- Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2016/11/perl-snapshot
- Michael Schilli, “Unvergesslich”: Linux-Magazin 12/04, S. 100, https://www.linux-magazin.de/Ausgaben/2012/04/Perl-Snapshot
- Michael Schilli, “Bewegte Reife”: Linux-Magazin 13/10, S. 112, https://www.linux-magazin.de/Ausgaben/2013/10/Perl-Snapshot
- Michael Schilli, “Datenmaler”: Linux-Magazin 09/07, S. 114, https://www.linux-magazin.de/Ausgaben/2009/07/Datenmaler
- Michael Schilli, “Kurzweilige Repository-Statistiken mit Perl und R”: Linux-Magazin 11/02, S. 118, https://www.linux-magazin.de/Ausgaben/2011/02/Datumsarithmetik
- Evernote Developer Tokens: https://www.evernote.com/api/DeveloperToken.action
- Evernote-Suchfunktionen: https://help.evernote.com/hc/de/articles/208313828-Verwenden-der-erweiterten-Suchsyntax-von-Evernote















