Wir gehen viral (Linux-Magazin, Januar 2015)

Damit Hobby-Youtuber nicht verpassen, wenn ihre Guckerzahlen durch die Decke gehen, analysiert ein Perlskript einmal täglich Trends und schlägt Alarm, falls ein Video plötzlich zum Massenhit wird.

Steht ein Hobby-Handwerker wie ich heute vor einem scheinbar unlösbaren mechanischen Problem, zum Beispiel weil ein Gadget-Gehäuse sich gegen das garantieverachtende Öffnen sträubt, hilft meist ein Youtube-Video weiter. Auch bei nicht ganz einfach zu bedienender Software wie dem Gimp zeigt meist ein Fachmann in einem Screencast auf Youtube wie man als Hobby-Gimper auch komplizierte Aufgaben löst.

Screencast als Hit?

Als es mir deshalb vor einiger Zeit gelang, Gimps Scissor-Tool erfolgreich anzuwenden, machte ich gleich einen Screencast daraus, und warte nun vergeblich darauf, dass dieses meisterlich produzierte Video in den Charts aufsteigt. Erst setzte ich mir monatliche Erinnerungsnachrichten in Evernote, um in regelmäßigen Abständen von Hand die Anzahl der "Views" auf Youtube zu inspizieren, aber das kostet Zeit, die ich nicht habe. Dank des CPAN-Moduls WebService::GData::YouTube geht es nun auch automatisch.

Abbildung 1: Der Hobbyproduzent dieses Youtube-Screencasts möchte wissen, wie sich seine Userzahlen entwickeln (297 Views bisher).

Listing 1: youtube-watch.yml

    01 - 
    02   name: GIMP - Scissors Select Tool
    03   id: _Cxu3-UP0G8
    04 -
    05   name: How to fix Acura/Honda not starting in hot weather
    06   id: ssYpyXjGw9g
    07 -
    08   name: Flip it: Record Messages and play them Backwards
    09   id: LdSTIa2Tx4o
    10 - 
    11   name: Scored a goal at Garfield Square
    12   id: bvOvnyJTyss

Hierzu listet die YAML-Datei in Listing 1 die IDs der zu überwachenden Videos auf. Diese Hex-Nummern lassen sich einfach aus den entsprechenden Youtube-URLs extrahieren, zum Beispiel der String "_Cxu3-UP0G8" in der URL-Zeile des Browsers oben in Abbildung 1 (vielleicht in der Abbildung farblich markieren?).


=cut
###########################################################################

Abbildung 2: Die JSON-Ausgabe von Listing 2 mit den von Youtube eingeholten Guckerzahlen pro Video.

Das Skript in Listing 2 geht nun mit Hilfe des YAML-Moduls vom CPAN der Reihe nach durch die Einträge der YAML-Datei und übergibt jeweils die Video-ID an die Methode get_video_by_id() des Pakets WebService::GData::YouTube. Dieses wiederum kontaktiert den API-Server von Youtube und bekommt einen Wust an Metadaten über das Video zurück. Unter dem Schlüssel _feed findet das Skript im Eintrag yt$statistics den Wert für ViewCount, also die Anzahl der Aufrufe des Videos von begeisterten Youtube-Kunden.

Listing 2: youtube-viewcounts

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use WebService::GData::YouTube qw();
    04 use YAML qw( LoadFile );
    05 use JSON qw( to_json );
    06 
    07 my @result = ();
    08 my $videos = 
    09   LoadFile( "youtube-watch.yml" );
    10 my $yt = WebService::GData::YouTube->new();
    11 
    12 for my $video ( @$videos ) {
    13 
    14   my $meta = 
    15     $yt->get_video_by_id( $video->{ id } );
    16 
    17   $video->{ count } = $meta->{ _feed }->
    18     { 'yt$statistics' }->{ viewCount };
    19 
    20   push @result, $video;
    21 }
    22 
    23 print to_json( \@result, 
    24     { pretty => 1, canonical => 1 } );

Das Skript formt die Ausgabedaten am Ende noch schnell mittels des JSON-Moduls vom CPAN in das maschinenlesbare Format um, damit nachfolgende Skripts with Listing 3 sie leicht verarbeiten können. Dabei setzt es die Option pretty, um die Ausgabe auch für menschliche Konsumenten leicht lesbar darzustellen und canonical, damit die sonst in Perl unsortierten Hashschlüssel sortiert herauskommen. Im Ergebnisarray @result führt dann jedes Element die Felder name (den Titel des Videos), count (den in Zeile 17 hineingeschmuggelten Viewcount) und id, die eindeutige Youtube-Kennung des Videos.

Abbildung 2 zeigt die Ausgabe des Skripts, und offenbart, dass meine bislang wenig öffentlich bekannte Tätigkeit als Automechaniker an zwanzig Jahre alten Honda-Motoren mit ca. 56.000 Aufrufen zu den Highlights meines Oevres gehört, während der Gimp-Screencast mit nur 297 Aufrufen noch (!) vor sich hindümpelt.

Tägliche Tests

Läuft Listing 1 mittels eines Cronjobs jeden Tag um dieselbe Uhrzeit einmal ab, steht als nächstes die Archivierung der Daten zur späteren historischen Auswertung an. Hierzu dient Listing 4 die Minidatenbank SQLite, die ihre Daten in einer einzigen Datei ablegt, aber mittels eines Query-Engines SQL-Abfragen erlaubt.

Listing 3: viewcounts.sql

    1 CREATE TABLE views (
    2   video_id TEXT,
    3   views    INTEGER,
    4   queried  DATE
    5 );

Das in Listing 3 gezeigte Datenbankschema enthält Felder für die Youtube-ID des Videos (video_id), die Anzahl der zum Zeitpunkt der Abfrage festgestellte Anzahl der Views (views) und den Datumsstempel der Abfrage (queried). Mit dem Kommando

    $ sqlite3 viewcounts.db <viewcounts.sql

erzeugt das mit sudo apt-get sqlite3 installierte SQLite dann die Datenbank in der frischen Datei viewcounts.db. Damit kann dann Listing 4 als zweite Brennstufe nach Listing 2 mittels

    youtube-viewcounts | viewcounts-todb

die von Listing 2 erzeugten JSON-Daten entgegennehmen und für jeden Array-Eintrag einen Datenbankrecord anlegen. Das Datumsfeld jedes neuen Eintrags füllt es in Zeile 16 mit CURRENT_TIMESTAMP, was SQLite in der Datenbank durch das aktuelle Datum mit Uhrzeit ersetzt. So entsteht für jedes überwachte Video pro Tag ein Datenbankeintrag mit Zeitstempel, die sich hinterher leicht zeitlich sortiert wieder herausholen lassen.

Listing 4: viewcounts-todb

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use DBD::SQLite;
    04 use JSON qw( from_json );
    05 use DBI;
    06 my $dbh = DBI->connect( 
    07   "dbi:SQLite:dbname=viewcounts.db", 
    08   "", "", { RaiseError => 1} );
    09 
    10 my $data = from_json( join '', <STDIN> );
    11 
    12 for my $video ( @$data ) {
    13   my $sth = $dbh->prepare( 
    14     "INSERT INTO 
    15      views(video_id,views,queried) 
    16      VALUES(?,?,CURRENT_TIMESTAMP)" );
    17   $sth->execute( $video->{ id }, 
    18                  $video->{ count } );
    19 }

Abbildung 3: In der SQLite-Datenbank speichert das Skript täglich die aktuellen Viewzahlen ab.

Anhand der in Abbildung 3 gezeigten Datenbankeinträge versucht nun Listing 5, herauszufinden, ob die Zuschauerzahlen nur stetig ansteigen or sich explosionsartig erhöhen. In diesem Fall soll das Skript später den interessierten Beobachter alarmieren.

Was macht einen Hit?

Was macht nun einen hitverdächtigen Sprung aus? Bei einem Video mit 30.000 Aufrufen, das im Schnitt 10 Views pro Tag zulegt, bedeuten 10 mehr nicht viel. Bei einem weniger erfolgreichen, das lange unentdeckt blieb und nun auf einmal 10 Views zulegt, wäre vielleicht eine Notiz angebracht.

Da ich letztens das ausgezeichnete Buch "Machine Learning with R" ([2]) studiert habe, das allerhand statistische Methoden erläutert, kam mir die Idee, außergewöhnliche Sprünge in den Viewerzahlen mittels linearer Regression ([3]) zu ermitteln.

Abbildung 4: Einen Hit erkennt Listing 5 wenn der neueste Wert (blauer Punkt ganz rechts) die per linearer Regression geschätzten Wert weit übersteigt.

Dabei versucht das Modul Statistics::LineFit vom CPAN in Listing 5, durch flache oder stetig ansteigende historische Zuschauerzahlen eine Gerade zu legen (Abbildung 4). Auf der x-Achse läuft die Zeit von links nach rechts und die y-Achse gibt die Anzahl der zum Messzeitpunkt vorliegenden Views an.

Dabei ist es nicht so wichtig, dass die Gerade jeden Messpunkt genau trifft (was auch gar nicht möglich ist), solange das Verfahren die zwangsläufig auftretenden Fehler minimal hält. Das Problem ist wissenschaftlich seit Urzeiten gelöst und das CPAN-Modul implementiert nur den Algorithmus, um die Parameter a und b aus der Geraden-Formel y = b + a*x zu berechnen. Der Parameter b gibt dabei den y-Wert am Nulldurchgang der Geraden an ("intercept"), während a die Steigung ("slope") der Geraden darstellt.

Machine Learning Lite

Listing 5 iteriert in Zeile 16 durch alle zu überwachenden Videos und setzt für jedes einen SQL-Query ab, um die historischen View-Zahlen aufsteigendend sortiert nach dem Messzeitpunkt hervorzuholen. Diese y-Werte stopft es in den Array @yvalues, während es die zugehörigen X-Werte bei 1 anfangen lässt und dann an jedem Messtag um jeweils eins erhöht. In $row[0], dem ersten Element einer jeden Zeile des SQL-Ergebnisses steht der View-Count des gerade bearbeiteten Videos zum Messzeitpunkt.

Listing 5: hit-detect

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use DBI;
    04 use DBD::SQLite;
    05 use Statistics::LineFit;
    06 use YAML qw( LoadFile );
    07 use JSON qw( to_json );
    08 
    09 my $dbh = DBI->connect( 
    10   "dbi:SQLite:dbname=viewcounts.db", 
    11   "", "", { RaiseError => 1} );
    12 
    13 my $videos = 
    14   LoadFile( "youtube-watch.yml" );
    15 
    16 for my $video ( @$videos ) {
    17   my $sth = $dbh->prepare( 
    18     "SELECT views from views 
    19      WHERE video_id = ? ORDER BY 
    20      queried ASC" );
    21   $sth->execute( $video->{ id } );
    22 
    23   my @xvalues = ();
    24   my @yvalues = ();
    25 
    26   my $x = 1;
    27   while ( my @row = 
    28           $sth->fetchrow_array ) {
    29     push @xvalues, $x;
    30     push @yvalues, $row[0];
    31     $x++;
    32   }
    33 
    34   my $last_x = pop @xvalues;
    35   my $last_y = pop @yvalues;
    36 
    37   my $fit = Statistics::LineFit->new();
    38   $fit->setData (\@xvalues, \@yvalues) or 
    39     die "Invalid data";
    40 
    41   my ($intercept, $slope) = 
    42     $fit->coefficients();
    43 
    44   my $y_predicted = $intercept + 
    45                     $last_x * $slope;
    46 
    47   if( abs( $last_y - $y_predicted ) > 
    48                              3 * $slope ) {
    49     print 
    50     "Hooray, $video->{ name } is a hit!\n";
    51   }
    52 }

Die Zeilen 34 und 35 entfernen anschließend den letzten (tagesaktuellen) Messwert, damit das Skript die Regressionsgerade nur durch die mehr als einen Tag zurückliegenden Messpunkte legt. Die Methode coefficients() in Zeile 42 liefert schließlich den errechneten Nulldurchgang in $intercept und die Steigung der Geraden in der Variablen $slope. Mit diesen interpoliert Zeile 44 den Wert für die letzte Messung $y_predicted, ermittelt also, welcher Wert zu erwarten wäre, falls die Zuschauerzahlen wie bisher weitergestiegen wären. Ist die Differenz zum wirklich gemessenen Wert dreimal so groß wie die Geradensteigung, liegt offensichtlich ein virales Video vor, und der User erfährt davon durch die ausgegebene Nachricht. Mit dem Faktor 3 sollte man etwas experimentieren, manch einer möchte schon bei kleineren Erfolgen Bescheid bekommen. Statt der print-Anweisung empfiehlt sich die Benachrichtigung per Email, zum Beispiel mit dem CPAN-Modul Mail::DWIM, das dazu nur ein paar Zeilen benötigt, und auch HTML-Emails beherrscht, sodass der beglückte Empfänger gleich auf sein frischgebackenes Hitvideo klicken kann.

Infos

[1]

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

[2]

"Machine Learning with R", Brett Lantz, Packt Publishing, 2013

[3]

"Lineare Regression", http://de.wikipedia.org/wiki/Lineare_Regression

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.