Twitter Profilhintergrundfarben

Für ein Projekt habe ich Tweets von >8‘000‘000 Twitter-Usern eingesammelt. Dabei fallen noch eine Reihe weiterer Daten an, wie die Profilhintergrundfarbe. Es wäre eine Schande diese Daten einfach verkommen zu lassen, also habe ich nach einer Möglichkeit gesucht diese Information ansprechend darzustellen, was sich als weniger trivial herausgestellt hat, als ich ursprünglich angenommen hatte: Im Idealfall sollten ähnliche Farben nahe beieinander liegen, allerdings ist der RGB Farbraum ein dreidimensionaler Kubus, ein Bild aber nur zweidimensional, sodass es keine „richtige“ Art und Weise gibt, ähnliche Farben nebeneinander anzuordnen.

Ich habe mich hier dafür entschieden eine 2D Hilbert-Kurve durch mein Bild zu legen und die Farben in der Reihenfolge zu zeichnen, in der eine 3D Hilbert-Kurve ihnen im RGB-Kubus begegnet. Wenn man dann noch die beiden Standardhintergrundfarben #F5F8FA und #C0DEED ignoriert, sieht das Ergebnis so aus.

Twitter-Profil-Hintergrundfarbe

Und dank der Python Pakete hilbertcurve und pypng ist der Code sogar ziemlich harmlos:

from math import ceil, sqrt, log2

from hilbertcurve.hilbertcurve import HilbertCurve
import png


"""
    turn an RGB string like `#C0DEED` into a tuple of integers,
    i.e., coordinates of the RGB cube
"""
def str2rgb(s):
    s = s.strip("#")
    return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))


"""
    `color_histogram` is a dict mapping an rgb string like `#F5F8FA`
    to the number of usages of this color
"""
def plot_background_colors(color_histogram, filename="colors.png"):
    defaults = {"F5F8FA", "C0DEED"}

    data = {str2rgb(rgb): d for rgb, d in color_histogram if rgb not in defaults}

    # calculate the size of the resulting image
    # for a 2D Hilbert curve, it mus be square with a width, which is a power of 2
    num_pixels = sum(data.values())
    min_width = ceil(sqrt(num_pixels))
    exponent = ceil(log2(min_width))
    width = 2**exponent

    # output buffer for a width x width png, with 4 color values per pixel
    buf = [[0 for _ in range(4 * width)] for _ in range(width)]

    hc2 = HilbertCurve(exponent, 2)
    # there are 256 = 2^8 values in each direction of the RGB cube
    hc3 = HilbertCurve(8, 3)

    sorted_rgbs = sorted(data.keys(), key=lambda x: hc3.distance_from_point(x))

    idx = 0
    for rgb in sorted_rgbs:
        for _ in range(data[rgb]):
            # get the coordinate of the next pixel
            x, y = hc2.point_from_distance(idx)
            # assign the RGBA values to the pixel
            buf[x][4 * y] = rgb[0]
            buf[x][4 * y + 1] = rgb[1]
            buf[x][4 * y + 2] = rgb[2]
            buf[x][4 * y + 3] = 255

            idx += 1

    png.from_array(buf, 'RGBA').save(filename)

Das Histogram, das als Input benötigt wird war in meinem Fall nur eine SQL Query entfernt:

SELECT profile_background_color, COUNT(profile_background_color) FROM users
    GROUP BY profile_background_color;

Number of longest increasing subsequences

Meine liebsten Probleme sind solche, die einfach scheinen aber sehr tief sind. Natürlich gehört das Problem des Handlungsreisenden dazu: Es ist einfach zu verstehen, dass der Müllmann bei jeder Mülltonne vorbei muss und dabei möglichst wenig Strecke fahren will. Gerade deshalb ist es das Paradebeispiel für NP-schwere Probleme (technisch gesehen ist nur seine Entscheidungs-Version „Gibt es eine Tour, die kürzer ist als \(X\)NP-schwer und nicht die typische Optimierungsversion: „Welche ist die kürzeste Tour“).

Aber fast noch besser gefällt mir das Problem der längsten aufsteigenden Teilfolge, oder auf englisch, longest increasing subsequence (LIS): Gegeben eine Folge von Zahlen \(S_i\), welche Teilfolge ist am längsten unter der Bedingung, dass die Zahlen aufsteigen.

Eine längste aufsteigende Teilfolge ist in einer Folge markiert

Dieses Problem ist so einfach, dass es erstmals von Stanisław Ulam als Fingerübung beschrieben wurde und nach meinem Eindruck heutzutage als Übung für dynamische Programmierung in Universitäten verwendet wird. Wer weiß wie viele Bewerber vor einem Whiteboard ins Schwitzen geraten sind bei dem Versuch es aus dem Stegreif zu lösen.

The Surprising Mathematics of Longest Increasing Subsequences -- Dan Romik

Auf der anderen Seite ist es aber offenbar tief genug, dass man ganze Bücher darüber schreiben kann. Es zeigen sich überraschende Querverbindungen zu scheinbar unabhängigen Problemen. Denn die Länge \(L\) der LIS einer Permutation fluktuiert genauso wie der Abstand von der Mitte zum Rand eines Kaffeeflecks oder die größten Eigenwerte von Zufallsmatrizen.

Nun ist die Lösung dieses Problems nicht eindeutig: Es kann viele längste aufsteigende Teilfolgen geben. Tatsächlich wächst die Anzahl sogar exponentiell mit der Länge der ursprünglichen Sequenz.

Verschiedene längste aufsteigende Teilfolgen der gleichen Folge

Allerdings wurde bisher nie untersucht wie viele genau. Oftmals hört man, es sei nicht praktikabel alle durchzuzählen, da es exponentiell viele seien. Und wenn es darum ginge alle zu enumerieren, würde das stimmen. Aber wir wollen an dieser Stelle nur die Anzahl wissen, die wir mittels dynamischer Programmierung effizient bestimmen können. Die Idee ist, dass wir für jedes Element, das an Position \(x\) in einer LIS auftauchen kann, berechnen, wie viele aufsteigende Teilfolgen der Länge \(L-x\) mit diesem Element beginnen.

Besonders einfach geht das, wenn wir zuerst eine Datenstruktur aufbauen, die kodiert welche Elemente in einer LIS aufeinander folgen können. Dazu erweitern wir Patience Sort, und da dieser Algorithmus nach einem Kartenspiel benannt ist, werden wir es auch mit Karten visualisieren: Wir schreiben jedes Element unserer Sequenz auf eine Karte und legen die Karten auf einen Stapel, sodass das erste Element der Sequenz oben liegt. Dann nehmen wir Karten von oben ab und legen sie auf verschiedene Stapel. Die erste Karte legen wir auf den ersten, noch leeren Stapel. Die folgenden Karten legen wir auf den ersten Stapel, dessen oberstes Element größer ist als die aktuelle Karte und ansonsten machen wir einen neuen Stapel rechts davon auf. Jedes mal wenn wir eine Karte ablegen, lassen wir sie auf alle Karten, die aktuell auf dem Vorgängerstapel liegen und kleiner sind, zeigen — dies sind die Karten die in einer aufsteigenden längsten Teilfolge direkt vor ihr auftauchen können.

Animation von Patience Sort

Am Ende haben wir \(L\) Stapel, wobei \(L\) die Länge der LIS ist, und wir können vom Stapel ganz rechts starten und den Pfeilen folgen, um eine LIS zusammenzubauen. Wenn wir nur an der Länge interessiert wären, müssten wir uns über den Inhalt der Stapel keine Gedanken machen und der Algorithmus ließe sich sehr kompakt darstellen:

fn lis_len<T: Ord>(seq: &[T]) -> usize {
    let mut stacks = Vec::new();
    for i in seq {
        let pos = stacks.binary_search(&i)
            .err()
            .expect("Encountered non-unique element in sequence!");
        if pos == stacks.len() {
            stacks.push(i);
        } else {
            stacks[pos] = i;
        }
    }
    stacks.len()
}

Aber wir wollen mehr, deshalb notieren wir uns im nächsten Schritt bei allen Karten des rechtesten Stapels wie viele aufsteigende Teilfolgen der Länge \(x=1\) mit ihnen starten, was trivialerweise je eine ist. Dann notieren wir bei allen Karten des Stapels links davon wie viele aufsteigenden Teilfolgen der Länge 2 mit ihnen anfangen. Das können wir berechnen, indem wir den Pfeilen rückwärts folgen und die Annotationen jeweils aufaddieren. Nachdem wir dies für alle Stapel wiederholt haben und den linkesten Stapel beschriftet haben, können wir alle Annotationen des linkesten Stapels aufaddieren, um die gesamte Anzahl LIS zu erhalten: hier \(7\).

Beispiel der Datenstruktur zum Zählen der unterschiedlichen LIS

Wie sich das ganze für längere Sequenzen aus unterschiedlichen Zufallsensembles im Detail verhält haben wir in einem Artikel veröffentlicht.

Pebble Rules

Im letzten Monat habe ich jemanden getroffen, auf dessen Armbanduhr eine MCMC Simulation von Hamilton-Pfaden auf einem quadratischen Gitter liefen. Ich war derartig begeistert, dass ich beschlossen habe auch etwas auf meiner Pebble simulieren zu lassen. Aufgrund der geringen Auflösung des Displays (\(144 \times 168\)) bieten sich „blockige“ Visualisierungen an. Glücklicherweise habe ich schon genügend Spielereien geschrieben, die sich eignen [1, 2, 3, 4].

Pebble wurde zwar inzwischen von Fitbit aufgekauft, aber das SDK ist noch verfügbar. Die neueren Exemplare lassen sich per JavaScript programmieren, meine „Kickstarter Edition“ aus der ersten Generation allerdings noch nicht.

Da ich meine Uhr also in C programmieren muss, konnte ich allerdings den den alten Code aus Wolfram’s Rules wiederbenutzen.

Der Code ist auf GitHub verfügbar.