Inhalt

Einführung

Je komplexer ein beliebiges Data Science Projekt in Python wird, desto schwieriger wird es in der Regel, den Überblick darüber zu behalten, wie alle Module miteinander interagieren. Wenn man in einem Team an einem größeren Projekt arbeitet, wie es hier bei STATWORX oft der Fall ist, kann die Codebasis schnell so groß werden, dass die Komplexität abschreckend wirken kann. In einem typischen Szenario arbeitet jedes Teammitglied in seiner „Ecke“ des Projekts, so dass jeder nur über ein solides lokales Wissen über den Code des Projekts verfügt, aber möglicherweise nur eine vage Vorstellung von der Gesamtarchitektur des Projekts hat. Im Idealfall sollte jedoch jeder, der an dem Projekt beteiligt ist, einen guten globalen Überblick über das Projekt haben. Damit meine ich nicht, dass man wissen muss, wie jede Funktion intern funktioniert, sondern eher, dass man die Zuständigkeit der Hauptmodule kennt und weiß, wie sie miteinander verbunden sind.

Ein visuelles Hilfsmittel, um die globale Struktur kennenzulernen, kann ein Call Graph sein. Ein Call Graph ist ein gerichteter Graph, der anzeigt, welche Funktion welche Funktion aufruft. Er wird aus den Daten eines Python-Profilers wie cProfile erstellt.

Da sich ein solcher Graph in einem Projekt, an dem ich arbeite, als hilfreich erwiesen hat, habe ich ein Paket namens project_graph erstellt, das einen solchen Call Graph für ein beliebiges Python-Skript erstellt. Das Paket erstellt ein Profil des gegebenen Skripts über cProfile, konvertiert es in einen gefilterten Punktgraphen über gprof2dot und exportiert es schließlich als .png-Datei.

Warum sind Projektgrafiken nützlich?

Als erstes kleines Beispiel soll dieses einfache Modul dienen.

# test_script.py

import time
from tests.goodnight import sleep_five_seconds

def sleep_one_seconds():
    time.sleep(1)

def sleep_two_seconds():
    time.sleep(2)

for i in range(3):
    sleep_one_seconds()

sleep_two_seconds()

sleep_five_seconds()

Nach der Installation (siehe unten) wird durch Eingabe von project_graph test_script.py in die Kommandozeile die folgende png-Datei neben dem Skript platziert:

Das zu profilierende Skript dient immer als Ausgangspunkt und ist die Wurzel des Baums. Jedes Kästchen ist mit dem Namen einer Funktion, dem Gesamtprozentsatz der in der Funktion verbrachten Zeit und der Anzahl ihrer Aufrufe beschriftet. Die Zahl in Klammern gibt an, wieviel Zeit innerhalb einer Funktion verbracht wurde, jedoch ohne die Zeit in weiteren Unterfunktion zu berücksichtigen.

In diesem Fall wird die gesamte Zeit in der Funktion sleep des externen Moduls time verbracht, weshalb die Zahl 0,00% beträgt. In selbstgeschriebenen Funktionen wird nur selten viel Zeit verbracht, da die Arbeitslast eines Skripts in der Regel schnell auf sehr einfache Funktionen der Python-Implementierung selbst rausläuft. Neben den Pfeilen ist auch die Zeit angegeben, die eine Funktion an die andere weitergibt, zusammen mit der Anzahl der Aufrufe. Die Farben (ROT-GRÜN-BLAU, absteigend) und die Dicke der Pfeile zeigen die Relevanz der verschiedenen Stellen im Programm an.

Beachten Sie, dass sich die Prozentsätze der drei obigen Funktionen nicht zu 100 % aufaddieren. Der Grund dafür ist, dass der Graph so eingestellt ist, dass er nur selbst geschriebene Funktionen enthält. In diesem Fall hat das Importieren des Moduls time den Python-Interpreter dazu veranlasst, 0,04% der Zeit für eine Funktion des Moduls importlib aufzuwenden.

Auswertung mit externen Packages

Betrachten wir ein zweites Beispiel:

# test_script_2.py

import pandas as pd
from tests.goodnight import sleep_five_seconds

# some random madness
for i in range(1000):
   a_frame = pd.DataFrame([[1,2,3]])

sleep_five_seconds()

In diesem Skript wird ein Teil der Arbeit in einem externen Paket erledigt, das auf der Top-Ebene und nicht in einer benutzerdefinierten Funktion aufgerufen wird. Um dies im Graphen zu erfassen, können wir das externe Paket (pandas) mit der Flag -x hinzufügen. Die Initialisierung eines Pandas DataFrame wird jedoch in vielen Pandas-internen Funktionen durchgeführt. Offen gesagt, bin ich persönlich nicht an den inneren Verwicklungen von pandas interessiert, weshalb ich möchte, dass der Baum nicht zu tief in die Pandas-Mechanik „hineinwächst“. Diesem Umstand kann man Rechnung tragen, indem man nur Funktionen auftauchen lässt, die einen minimalen Prozentsatz der Laufzeit in ihnen verbringen. Genau dies kann mit der -m-Flag erreicht werden.

In Kombination ergibt project_graph -m 8 -x pandas test_script_2.py das folgende Ergebnis:

Project Graph Creation Example 02

Spaß(-Beispiele) beiseite, nun wollen wir uns ernsteren Dingen zuwenden. Ein echtes Data Science Projekt könnte wie dieses aussehen:

Project Graph Creation Example 03

Dieses Mal ist der Baum viel größer. Er ist sogar noch größer als in der Abbildung zu sehen, da viel mehr selbst geschriebene Funktionen aufgerufen werden. Sie werden jedoch aus Gründen der Übersichtlichkeit aus dem Baum entfernt, da Funktionen, für die weniger als 0,5 % der Gesamtzeit aufgewendet werden, herausgefiltert werden (dies ist die Standardeinstellung für die -m Flag). Beachten Sie, dass ein solches Diagramm auch bei der Suche nach Leistungsengpässen sehr vorteilhaft ist. Man sieht sofort, welche Funktionen den größten Teil der Arbeitslast tragen, wann sie aufgerufen werden und wie oft sie aufgerufen werden. Das kann Sie davor bewahren, Ihr Programm an den falschen Stellen zu optimieren und dabei den Elefanten im Raum zu übersehen.

Wie man project graph verwendet

Installation

Gehen Sie in Ihrer Projektumgebung wie folgt vor:

brew install graphviz

pip install git+https://github.com/fior-di-latte/project_graph.git

Verwendung

Wechseln Sie in der Projektumgebung in das aktuelle Arbeitsverzeichnis des Projekts (das ist wichtig!) und geben Sie für die Standardverwendung ein:

project_graph myscript.py

Wenn Ihr Skript einen argparser enthält, verwenden Sie (vergessen Sie nicht die Anführungsstriche!):

project_graph "myscript.py <arg1> <arg2> (...)"

Wenn Sie den gesamten Graphen sehen wollen, einschließlich aller externen Pakete, verwenden Sie:

project_graph -a myscript.py

Wenn Sie eine andere Sichtbarkeitsschwelle als 1% verwenden wollen, benutzen Sie:

project_graph -m <percent_value> myscript.py

Wenn Sie schließlich externe Pakete in den Graphen aufnehmen wollen, können Sie sie wie folgt angeben:

project_graph -x <package1> -x <package2> (...) myscript.py

Schluss & Hinweise

Dieses Paket hat einige Schwächen, von denen die meisten behoben werden können, z.B. durch Formatierung des Codes in einen funktionsbasierten Stil, durch Trimmen mit der -m-Flag oder durch Hinzufügen von Paketen mit der-x-Flag. Wenn etwas seltsam erscheint ist der erste Schritt wahrscheinlich die Verwendung der -a-Flag zur Fehlersuche. Wesentliche Einschränkungen sind die folgenden:

  • Es funktioniert nur auf Unix-Systemen.
  • Es zeigt keinen wahrheitsgetreuen Graphen an, wenn es mit Multiprocessing verwendet wird. Der Grund dafür ist, dass cProfile nicht mit Multiprocessing kompatibel ist. Wenn Multiprocessing verwendet wird, wird nur der Root-Prozess profiliert, was zu falschen Berechnungszeiten im Graphen führt. Wechseln Sie zu einer nicht-parallelen Version des Zielskripts.
  • Die Profilerstellung eines Skripts kann zu einem beträchtlichen Overhead bei der Berechnung führen. Es kann sinnvoll sein, die in Ihrem Skript geleistete Arbeit zu verringern (d. h. die Menge der Eingabedaten zu reduzieren). In diesem Fall kann die in den Funktionen verbrachte Zeit natürlich massiv verzerrt werden, wenn die Funktionen nicht linear skalieren.
  • Verschachtelte Funktionen werden im Diagramm nicht angezeigt. Insbesondere ein Dekorator verschachtelt implizit Ihre Funktion und versteckt sie daher. Das heißt, wenn Sie einen externen Dekorator verwenden, vergessen Sie nicht, das Paket des Dekorators über die-x Flag hinzuzufügen (zum Beispiel project_graph -x numba myscript.py).
  • Wenn Ihre selbst geschriebene Funktion ausschließlich von einer Funktion eines externen Pakets aufgerufen wird, müssen Sie das externe Paket manuell mit der -x Flag hinzufügen. Andernfalls wird Ihre Funktion nicht im Baum auftauchen, da ihr Parent eine externe Funktion ist und daher nicht berücksichtigt wird.

Sie können das kleine Paket gerne für Ihr eigenes Projekt verwenden, sei es für Leistungsanalysen, Code-Einführungen für neue Teammitglieder oder aus reiner Neugier. Was mich betrifft, so finde ich es sehr befriedigend, eine solche Visualisierung meiner Projekte zu sehen. Wenn Sie Probleme bei der Verwendung haben, zögern Sie nicht, mich auf Github zu kontaktieren (https://github.com/fior-di-latte/project_graph/).

PS: Wenn Sie nach einem ähnlichen Paket in R suchen, sehen Sie sich Jakobs Beitrag über Flussdiagramme von Funktionen an.

Management Summary

In modernen Unternehmen fallen im Tagesgeschäft an vielen Stellen Informationen in Textform an: Je nach Businesskontext können dies Rechnungen sein, oder auch Emails, Kundeneingaben (wie Rezensionen oder Anfragen), Produktbeschreibungen, Erklärungen, FAQs sowie Bewerbungen. Diese Informationsquellen blieben bis vor kurzem weitestgehend dem Menschen vorbehalten, da das maschinelle, inhaltliche Verstehen von Text ein technologisch herausforderndes Problem darstellt.
Aufgrund jüngster Errungenschaften im Bereich Deep Learning können nun eine Reihe unterschiedlicher NLP („Natural Language Processing“) Tasks mit erstaunlicher Güte gelöst werden.
Erfahren Sie in diesem Beitrag anhand von fünf praxisnahen Beispielen, wie NLP Use Cases diverse Businessprobleme lösen und so für Effizienz und Innovation sorgen.

Einführung

Natural Language Processing (NLP) ist zweifelslos ein Gebiet, dem in jüngster Vergangenheit besondere Aufmerksamkeit im Big Data-Umfeld zugekommen ist. So hat sich das von Google gemessene Interesse an dem Thema in den letzten drei Jahren mehr als verdoppelt. Daran ist erkennbar, dass innovative NLP-Technologien längst nicht mehr nur ein Thema für die Big Player, wie Apple, Google oder Amazon, ist. Vielmehr ist eine generelle Demokratisierung der Technologie zu beobachten. Einer der Gründe dafür ist sicherlich, dass nach einer Schätzung von IBM etwa 80% der „weltweiten Informationen“ nicht in strukturierten Datenbanken vorliegen, sondern in unstrukturierter, natürlicher Sprache. NLP wird zukünftig eine Schlüsselrolle einnehmen, wenn es darum geht, diese Informationen nutzbar zu machen. Damit wird der erfolgreiche Einsatz von NLP-Technologien zu einem der Erfolgsfaktoren für die Digitalisierung in Unternehmen werden.

Damit Sie sich ein Bild davon machen können, welche Möglichkeiten NLP heutzutage im Businesskontext öffnet, werden Ihnen im Folgenden fünf praxisnahe Anwendungsfälle vorgestellt und die dahinterstehende Lösungen erklärt.

Was ist NLP? – Ein kurzer Überblick

Als ein Forschungsthema, das bereits in den 50er Jahren Linguisten und Informatiker beschäftigte, fristete NLP im 20sten Jahrhundert auf der Anwendungsseite ein kaum sichtbares Dasein.

Der zentrale Grund dafür lag in der Verfügbarkeit der notwendigen Trainingsdaten. Zwar ist generell die Verfügbarkeit von unstrukturierten Daten, in Form von Texten, insbesondere mit dem Aufstieg des Internets exponentiell gestiegen, jedoch fehlte es weiterhin an geeigneten Daten für das Modelltraining. Dies lässt sich damit begründen, dass die frühen NLP Modelle zumeist überwacht trainiert werden mussten (sogenanntes Supervised Learning). Das Supervised Learning setzt jedoch voraus, dass Trainingsdaten mit einer dedizierten Zielvariable versehen werden müssen. Dies bedeutet, dass z.B. bei einer Textklassifikation der Textkorpus vor dem Modelltraining manuell durch Menschen annotiert werden muss.

Dies änderte sich Ende der 2010er Jahre, als eine neue Modellgeneration künstlicher neuronaler Netzwerke zu einem Paradigmenwechsel führte. Diese sogenannten „Language Models“ werden auf Grundlage riesiger Textkorpora von Facebook, Google und Co. (vor-)trainiert, indem einzelne Wörter in den Texten zufällig maskiert und im Verlauf des Trainings vorhergesagt werden. Es handelt sich dabei um das sogenannte selbstüberwachte Lernen (Self-Supervised Learning), das nicht länger eine separate Zielvariable voraussetzt. Im Zuge des Trainings erlernen diese Modelle ein kontextuelles Verständnis von Texten.

Vorteil dieses Vorgehens ist, dass ein- und dasselbe Modell mit Hilfe des erlernten kontextuellen Verständnisses für eine Vielzahl unterschiedlicher Downstream-Tasks (z.B. Textklassifizierung, Sentiment Analysis, Named Entity Recognition) nachjustiert werden kann. Dieser Vorgang wird als Transfer Learning bezeichnet. In der Praxis lassen sich diese vortrainierten Modelle herunterladen, sodass nur die Feinjustierung für die spezifische Anwendung durch zusätzliche Daten selbst gemacht werden muss. Folglich lassen sich mittlerweile mit wenig Entwicklungsaufwand performante NLP-Anwendungen entwickeln.

Um mehr über Language Models (insbesondere die sogenannten Transformer Modelle wie „BERT“, bzw. „roBERTa“, u.ä.) sowie Trends und Hemmnisse im Bereich NLP zu erfahren, lesen Sie hier den Beitrag zum Thema NLP-Trends von unserem Kollegen Dominique Lade.

Die 5 Use Cases

Textklassifizierung im Rekrutierungsprozess

Ein medizinisches Forschungsinstitut möchte seinen Rekrutierungsprozess von Studienteilnehmer*innen effizienter gestalten.

Für das Testen eines neuen Medikaments werden unterschiedliche, untereinander abhängige Anforderungen an die infrage kommende Personen gestellt (z.B. Alter, allg. Gesundheitszustand, Vorhandensein/Abwesenheit von Vorerkrankungen, Medikationen, genetische Dispositionen etc.). Das Prüfen all dieser Anforderungen ist mit einem großen Zeitaufwand verbunden. Üblicherweise dauert das Sichten und Beurteilen relevanter Informationen etwa eine Stunde pro potenziellen Studienteilnehmenden. Hauptgrund dafür ist, dass die klinischen Notizen Informationen über Patienten enthalten, die über strukturierte Daten wie Laborwerte und Medikamente hinausgehen: Auch unstrukturierter Informationen sind in den medizinischen Berichten, Arztbriefen, und Entlassungsberichten o.ä. in Textform zu finden. Insbesondere das Auswerten letzterer Daten bedarf viel Lesezeit und ist daher mit großem Aufwand verbunden. Um den Prozess zu beschleunigen, entwickelt das Forschungsinstitut ein Machine Learning Modell, das eine Vorauswahl von vielversprechenden Kandidaten trifft, sodass die Experten*innen lediglich die vorgeschlagene Personengruppe validieren müssen.

Die NLP Lösung

Aus methodischer Sicht handelt es sich bei diesem Problem um eine sogenannte Textklassifikation. Dabei wird basierend auf einem Text, eine Prognose für eine zuvor definierte Zielvariable erstellt. Um das Modell zu trainieren, ist es – wie im Supervised Learning üblich – notwendig, die Daten, in diesem Fall also die Arztdokumente, mit der Zielvariable zu annotieren. Da es hier ein Klassifikationsproblem zu lösen gilt (geeignete oder ungeeignete Studienteilnehmer*in), beurteilen die Experten*innen für einige Personen im Pool die Eignung für die Studie manuell. Ist eine Person geeignet, wird sie mit einer Eins gekennzeichnet (=positiver Fall), ansonsten mit einer Null (=negativer Fall). Anhand dieser Trainingsbeispiele kann das Modell nun Zusammenhänge zwischen den medizinischen Dokumenten der Personen und ihrer Eignung lernen.

Um der Komplexität des Problems Herr zu werden, wird ein entsprechend komplexes Modell namens ClinicalBERT verwendet. Dabei handelt es sich um ein Language Modell, das auf BERT (Bidirectional Encoder Representations from Transformers) basiert, und zusätzlich auf einem Datensatz von klinischen Texten trainiert wurde. Somit ist ClinicalBERT in der Lage, sogenannte Repräsentationen von sämtlichen medizinischen Dokumentationen für jede Person zu generieren. In einem letzten Schritt wird das neuronale Netzwerk von ClinicalBERT durch eine taskspezifische Komponente ergänzt. In diesem Fall handelt es sich um eine binäre Klassifikation: Zu jeder Person soll eine Eignungswahrscheinlichkeit ausgegeben werden. Durch einen entsprechenden linearen Layer wird die hochdimensionale Textdokumentation schlussendlich in eine einzige Zahl, die Eignungswahrscheinlichkeit, überführt. In einem Gradientenverfahren lernt das Modell nun anhand der Trainingsbeispiele die Eignungswahrscheinlichkeiten.

Weitere Anwendungsszenarien von Textklassifikation

Textklassifikation findet häufig in der Form von Sentiment Analysis statt. Dabei geht es darum, Texte in vordefinierte Gefühlskategorien (z.B. negativ/positiv) einzuordnen. Diese Informationen sind insbesondere in der Finanzwelt oder beim Social Media Monitoring wichtig. Darüber hinaus kann Textklassifikation in verschiedenen Kontexten verwendet werden, in denen es darum geht, Dokumente nach ihrem Typ zu sortieren (z.B. Rechnungen, Briefe, Mahnungen…).

Named Entity Recognition zur Verbesserung der Usability einer Nachrichtenseite

Ein Verlagshaus bietet seinen Leser*innen auf einer Nachrichtenseite eine Vielzahl von Artikeln über diverse Themen an. Im Zuge von Optimierungsmaßnahmen möchte man ein besseres Recommender-System implementieren, sodass zu jedem Artikel weitere passende (ergänzende oder ähnliche) Artikel vorgeschlagen werden. Außerdem möchte man die Suchfunktion auf der Landingpage verbessern, damit der Kunde oder die Kundin schnell den Artikel findet, der gesucht ist.
Um für diese Zwecke eine gute Datengrundlage zu schaffen, entscheidet sich der Verlag dazu, mit Named Entity Recognition den Texten automatisierte Tags zuzuordnen, anhand derer sowohl das Recommender-System als auch die Suchfunktion verbessert werden können. Nach erfolgreicher Implementierung wird auf deutlich mehr vorgeschlagene Artikel geklickt und die Suchfunktion ist wesentlich komfortabler geworden. Im Resultat verbringen die Leser*innen signifikant mehr Zeit auf der Seite.

Die NLP Lösung

Um das Problem zu lösen, ist es wichtig, die Funktionsweise von NER zu verstehen:

Bei NER geht es darum, Worte oder ganze Satzglieder inhaltlichen Kategorien zuzuordnen. So kann man „Peter“ beispielsweise als Person identifizieren, „Frankfurt am Main“ ist ein Ort und „24.12.2020“ ist eine Zeitangabe. Offensichtlich gibt es aber auch deutlich kompliziertere Fälle. Dazu vergleichen Sie die folgenden Satzpaare:

  1. „Früher spazierte Emma im Park immer an der schönen Bank aus Holz vorbei.“   (Bank = Sitzbank)
  2. „Gestern eilte sie noch zur Bank, um das nötige Bargeld abzuheben.“ (Bank = Geldinstitut)

Für den Menschen ist vollkommen offensichtlich, dass das Wort „Bank“ in den beiden Sätzen eine jeweils andere Bedeutungen hat. Diese scheinbar einfache Unterscheidung ist für den Computer allerdings alles andere als trivial. Ein Entity Recognition Modell könnte die beiden Sätze wie folgt kennzeichnen:

  1. „[Früher] (Zeitangabe) spazierte [Emma] (Person) im Park immer an der schönen [Bank] (Sitzgelegenheit) aus Holz vorbei.“
  2. „[Gestern] (Zeitangabe) eilte [sie] (Person/Pronomen) noch zur [Bank] (Geldinstitut), um das nötige Bargeld abzuheben.“  

In der Vergangenheit hätte man zur Lösung des obigen NER-Problems zu regelbasierten Algorithmen gegriffen, doch auch hier setzt sich der Machine Learning Ansatz durch:

Das vorliegende Multiclass-Klassifizierungsproblem der Entitätsbestimmung wird erneut mithilfe des BERT-Modells angegangen. Zusätzlich wird das Modell auf einem annotierten Datensatz trainiert, in dem die Entitäten manuell identifiziert sind. Die umfangreichste öffentlich zugängliche Datenbank in englischer Sprache ist die Groningen Meaning Bank (GMB). Nach erfolgreichem Training ist das Modell in der Lage, aus dem Kontext, der sich aus dem Satz ergibt, auch bisher unbekannte Wörter korrekt zu bestimmen. So erkennt das Modell, dass nach Präpositionen wie „in, bei, nach…“ ein Ort folgt, aber auch komplexere Kontexte werden in Bezug auf die Entitätsbestimmung herangezogen.

Weitere Anwendungsszenarien von NER:

NER ist als klassische Information Retrieval-Task für viele andere NER-Tasks, wie zum Beispiel Chatbots und Frage-Antwort Systeme, zentral. Darüber hinaus wird NER häufig zur Textkatalogisierung verwendet, bei der der Typ des Textes anhand von stichhaltigen, erkannten Entitäten bestimmt wird.

Ein Chatbot für ein Fernbusunternehmen

Ein Fernbusunternehmen möchte seine Erreichbarkeit erhöhen und darum die Kommunikationswege mit dem Kunden ausbauen. Neben seiner Homepage und seiner App möchte das Unternehmen einen dritten Weg zum Kunden, nämlich einen Whatsapp-Chatbot, anbieten. Die Zielvorstellung ist, dass man in der Konversation mit dem Chatbot gewisse Aktionen wie das Suchen, Buchen und Stornieren von Fahrten ausführen kann. Außerdem soll mit dem Chatbot ein zuverlässiger Weg geschaffen werden, die Fahrgäste über Verspätungen zu informieren.

Mit der Einführung des Chatbots können nicht nur bestehende Fahrgäste leichter erreicht werden, sondern auch Kontakt zu neuen Kunden*innen aufgebaut werden, die noch keine App installiert haben.

Die NLP Lösung

Abhängig von den Anforderungen, die an den Chatbot gestellten werden, wählt man zwischen verschiedenen Chatbot Architekturen aus.

Über die Jahre sind im Wesentlichen vier Chatbot-Paradigmen erprobt worden: In einer ersten Generation wurde die Anfrage auf bekannte Muster geprüft und entsprechend angepasste vorgefertigte Antworten ausgegeben („pattern matching“). Etwas ausgefeilter ist das sogenannte „grounding“, bei der durch Named Entity Recognition (s.o.) aus Wissensbibliotheken (z.B. Wikipedia) extrahierte Informationen in einem Netzwerk organisiert werden. Ein solches Netzwerk hat den Vorteil, dass nicht nur eingetragenes Wissen abgerufen werden kann, sondern, dass auch nicht registriertes Wissen durch die Netzwerkstruktur inferiert werden kann. Beim „searching“ werden direkt Fragen-Antwortpaare aus dem Konversationsverlauf (oder aus davor registrierten Logs) zum Suchen einer passenden Antwort herangezogen. Die Anwendung von Machine Learning Modellen ist der bewährteste Ansatz, um dynamisch passende Antworten zu generieren („generative models“).

Um einen modernen Chatbot mit klar eingrenzbaren Kompetenzen für das Fernbusunternehmen zu implementieren, empfiehlt es sich, auf bestehende Frameworks wie Google Dialogflow zurückzugreifen. Hierbei handelt es sich um eine Plattform, mit der sich Chatbots konfigurieren lassen, die die Elemente aller zuvor gennannten Chatbot-Paradigmen besitzen. Dazu übergibt man Parameter wie Intends, Entitäten und Actions.

Ein Intend („Benutzerabsicht“) ist beispielsweise die Fahrplanauskunft. Indem man verschiedene Beispielphrasen („Wie komme ich am … von … nach … “, „Wann fährt der nächste Bus von … nach …“) an ein Language Model übergibt, gelingt es dem Chatbot auch ungesehene Inputsätze dem richtigen Intend zuzuordnen (vgl. Textklassifikation).

Weiterhin werden die verschiedenen Reiseorte und Zeitangaben als Entitäten definiert. Wird nun vom Chatbot ein Intend mit passenden Entitäten erfasst (vgl. NER), dann kann eine Action, in diesem Fall eine Datenbankabfrage, ausgelöst werden. Schlussendlich wird eine Intend-Answer mit den relevanten Informationen ausgegeben, die an sämtliche vom Benutzer angegebene Informationen im Chatverlauf angepasst ist („stateful“).

Weitere Anwendungsszenarien von Chatbots:

Es gibt vielfältige Einsatzmöglichkeiten im Kundenservice – je nach Komplexität des Szenarios von der automatischen Vorbereitung (z.B. Sortierung) eines Kundenauftrags hin zur kompletten Abwicklung einer Kundenerfahrung.

Ein Question-Answering-System als Voice Assistant für technische Fragen zum Automobil

 Ein Automobilhersteller stellt fest, dass viele seiner Kunden*innen nicht gut mit den Handbüchern, die den Autos beiliegt, zurechtkommt. Häufig wird zu lange nach der relevanten Information gesucht oder sie wird gar nicht gefunden. Daher wird beschlossen, ergänzend zum statischen Handbuch auch einen Voice Assistant anzubieten, der auf technische Fragen präzise Antworten gibt. Zukünftig können die Fahrer*innen bequem mit ihrer Mittelkonsole sprechen, wenn sie ihr Fahrzeug warten wollen oder technische Auskunft wünschen.

Die NLP Lösung

Mit Frage-Antwort-Systemen wird sich schon seit Jahrzehnten auseinandergesetzt wird, stehen sie doch in gewisser Hinsicht an der Vorfront der künstlichen Intelligenz. Ein Frage-Antwort-System, das unter Berücksichtigung aller vorliegenden Daten immer eine korrekte Antwort fände, könnte man auch als „General AI“ bezeichnen. Eine Hauptschwierigkeit auf dem Weg zur General AI ist, dass das Gebiet, über das das System informiert sein muss, unbegrenzt ist. Demgegenüber liefern Frage-Antwort-Systeme gute Ergebnisse, wenn das Gebiet klar eingegrenzt ist, wie es beim Automobilassistenten der Fall ist. Grundsätzlich gilt: Je spezifischer das Gebiet, desto bessere Ergebnisse können erwartet werden.

Für die Implementierung des Frage-Antwort-Systems werden strukturierte Daten, wie technische Spezifikationen der Komponenten und Kennzahlen des Modells, aber auch unstrukturierte Daten, wie Handlungsanweisungen, aus dem Handbuch herangezogen. Sämtliche Daten werden in einem Vorbereitungsschritt mithilfe anderer NLP-Techniken (Klassifikation, NER) in Frage-Antwort-Form gebracht. Diese Daten werden einer Version von BERT übergeben, die bereits auf einem großen Frage-Antwort-Datensatz („SQuAD“) vortrainiert wurde. Das Modell ist damit in der Lage, souverän bereits eingespeiste Fragen zu beantworten, aber auch „educated guesses“ für ungesehene Fragen abzugeben.

Weitere Anwendungsszenarien von Frage-Antwort-Systemen:

Mithilfe von Frage-Antwort-Systemen können unternehmensinterne Suchmaschinen um Funktionalitäten erweitert werden. Im E-Commerce können auf Basis von Artikelbeschreibungen und Rezensionen automatisiert Antworten auf Sachfragen gegeben werden.

Automatische Textzusammenfassungen (Textgenerierung) von Schadensbeschreibungen für eine Sachversicherung

Eine Versicherung möchte die Effizienz ihrer Schadensregulierungsabteilung erhöhen. Es wurde festgestellt, dass es bei einigen Schadensreklamationen vom Kunden zu internen Zuständigkeitskonflikten kommt. Grund dafür ist, dass diese Schäden von Kund*innen zumeist über mehrere Seiten beschrieben werden und so eine erhöhte Einarbeitungszeit benötigt wird, um beurteilen zu können, ob man den Fall bearbeiten soll. So passiert es häufig, dass eine Schadensbeschreibung komplett gelesen werden muss, um zu verstehen, dass man den Schaden selbst nicht zu bearbeiten hat. Nun soll ein System Abhilfe schaffen, das automatisierte Zusammenfassungen generiert. Die Sachbearbeiter*innen können in Folge der Implementierung nun deutlich schneller über die Zuständigkeit entscheiden.

Die NLP Lösung

Grundsätzlich kann man beim Probelm der Textzusammenfassung zwischen zwei verschiedenen Ansätzen differenzieren: Bei der Extraction werden aus dem Inputtext die wichtigsten Sätze identifiziert, die dann im einfachsten Fall als Zusammenfassung verwendet werden. Dem gegenüber steht die Abstraction, bei der ein Text durch ein Modell in einen neu generierten Zusammenfassungstext überführt wird. Der zweite Ansatz ist deutlich komplexer, da hier Paraphrasierung, Generalisierung oder das Einbeziehen von weiterführendem Wissen möglich ist. Daher birgt dieser Ansatz auch ein größeres Potenzial, sinnvolle Zusammenfassungen generieren zu können, ist allerdings auch fehleranfälliger. Moderne Algorithmen zur Textzusammenfassung verfolgen den zweiten Ansatz, oder aber eine Kombination aus beiden Ansätzen.

Zur Lösung des Versicherungs-Use-Cases wird ein sogenanntes Sequence-to-Sequence-Modell verwendet, welches einer Wortsequenz (der Schadensbeschreibung) einer anderen Wortsequenz (der Zusammenfassung) zuordnet. Hierbei handelt es sich üblicherweise um ein rekurrentes neuronales Netzwerk (RNN), das auf Grundlage von Textzusammenfassungs-Paaren trainiert wird. Der Trainingsprozess ist so gestaltet, dass die Wahrscheinlichkeit für das nächste Wort abhängig von den letzten Worten (und zusätzlich einem „inner state“ des Modells), modelliert wird. Gleichsam schreibt das Modell effektiv die Zusammenfassung „von links nach rechts“, indem sukzessiv das nächste Wort vorhergesagt wird. Ein alternativer Ansatz sieht vor, den Input vom Language Model BERT numerisch encodieren zu lassen und auf Basis dieser Zahlenrepräsentation einen GPT-Decoder den Text autoregressiv zusammenfassen zu lassen. Mithilfe von Modellparametern kann in beiden Fällen angepasst werden, wie lang die Zusammenfassung etwa sein soll.

Weitere Anwendungsszenarien von Sprachgenerierung:

Ein solches Szenario ist an vielen Stellen denkbar: Das automatisierte Schreiben von Berichten, die Generierung von Texten auf der Grundlage der Analyse von Einzelhandelsverkaufsdaten, die Zusammenfassung von elektronischen Krankenakten oder die Erstellung von textlichen Wettervorhersagen aus Wetterdaten sind denkbare Anwendungen. Darüber hinaus kommt es auch bei anderen NLP Anwendungsfällen wie Chatbots und Q&A-Systemen zur Sprachgenerierung.

Ausblick

Vielleicht haben Sie beim Durchlesen dieser Anwendungsbeispiele von Textklassifikation, Chatbots, Frage-Antwort-Systemen, NER und Textzusammenfassungen den Eindruck gewonnen, dass es auch in Ihrem Unternehmen viele Prozesse gibt, die sich mit NLP-Lösungen beschleunigen ließen.

Tatsächlich ist NLP nicht nur ein spannendes Forschungsfeld, sondern auch eine Technologie, deren Anwendbarkeit im Businessumfeld stetig wächst.

NLP wird in Zukunft nicht nur ein Fundament einer datengetriebenen Unternehmenskultur werden, sondern birgt schon jetzt durch direkte Anwendung ein riesiges Innovationspotenzial, in das es sich zu investieren lohnt.

Bei STATWORX haben wir bereits jahrelange Erfahrung in der Entwicklung von maßgeschneiderten NLP-Lösungen. Hier finden die zwei unserer Case Studies zum Thema NLP: Social Media Recruiting mit NLP & Supplier Recommendation Tool. Wir stehen Ihnen gerne für eine individuelle Beratung zu diesem und vielen weiteren Themen zur Verfügung.

 

„There is no way you know Thomas! What a coincidence! He’s my best friend’s saxophone teacher! This cannot be true. Here we are, at the other end of the world and we meet? What are the odds?“ Surely, not only us here at STATWORX have experienced similar situations, be it in a hotel’s lobby, on the far away hiking trail or in the pub in that city you are completely new to. However, the very fact that this story is so suspiciously relatable might indicate that the chances of being socially connected to a stranger by a short chain of friends of friends isn’t too low after all.

Lots of research has been done in this field, one particular popular result being the 6-Handshake-Rule. It states that most people living on this planet are connected by a chain of six handshakes or less. In the general setting of graphs, in which edges connect nodes, this is often referred to as the so-called small-world-effect. That is to say, the typical number of edges needed to get from node A to node B grows logarithmically in population size (i.e., # nodes). Note that, up until now, no geographic distance has been included in our consideration, which seems inadequate as it plays a significant role in social networks.

When analyzing data from social networks such as Facebook or Instagram, three observations are especially striking:

  • Individuals who are geographically farther away from each other are less likely to connect, i.e., people from the same city are more likely to connect.
  • Few individuals have extremely many connections. Their number of connections follows a heavy-tailed Pareto distribution. Such individuals interact as hubs in the network. That could be a celebrity or just a really popular kid from school.
  • Connected individuals tend to share a set of other individuals they are both connected to (e.g., „friend cliques“). This is called the clustering property.

A model that explains these observations

Clearly, due to the characteristics of social networks mentioned above, only a model that includes geographic distances of the individuals makes sense. Also, to account for the occurrence of hubs, research has shown that reasonable models attach a random weight to each node (which can be regarded as the social attractiveness of the respective individual). A model that accounts for all three properties is the following: First, randomly place nodes in space with a certain intensity nu, which can be done with a Poisson process. Then, with an independent uniformly distributed weight U_x attached to each node x, every two nodes get connected by an edge with a probability

    \[p_{xy} =mathbb{P}(xtext{ is connected to } y):=varphi(frac{1}{beta}U_x^gamma U_y^gamma vert x-yvert^d)\]

where d is the dimension of the model (here: d=2 as we’ll simulate the model on the plane), model parameter gammain [0,1] controls the impact of the weights, model parameter beta>0 squishes the overall input to the profile function varphi, which is a monotonously decreasing, normalized function that returns a value between 0 and 1.

That is, of course, what we want because its output shall be a probability. Take a moment to go through the effects of different beta and gamma on p_{xy}. A higher beta yields a smaller input value for varphi and thereby a higher connection probability. Similarly, a high gamma entails a lower U^gamma (as Uin [0,1]) and thus a higher connection probability. All this comprises a scale-free random connection model, which can be seen as a generalization of the model by Deprez and Würthrich. So much about the theory. Now that we have a model, we can use this to generate synthetic data that should look similar to real-world data. So let’s simulate!

Obtain data through simulation

From here on, the simulation is pretty straight forward. Don’t worry about specific numbers at this point.

library(tidyverse)
library(fields)
library(ggraph)
library(tidygraph)
library(igraph)
library(Matrix)

# Create a vector with plane dimensions. The random nodes will be placed on the plane.
plane <- c(1000, 1000)

poisson_para <- .5 * 10^(-3) # Poisson intensity parameter
beta <- .5 * 10^3
gamma <- .4

# Number of nodes is Poisson(gamma)*AREA - distributed
n_nodes <- rpois(1, poisson_para * plane[1] * plane[2])
weights <- runif(n_nodes) # Uniformly distributed weights

# The Poisson process locally yields node positions that are completely random.
x = plane[1] * runif(n_nodes)
y = plane[2] * runif(n_nodes)

phi <- function(z) { # Connection function
  pmin(z^(-1.8), 1)
} 

What we need next is some information on which nodes are connected. That means, we need to first get the connection probability by evaluating varphi for each pair of nodes and then flipping a biased coin, accordingly. This yields a 0-1 encoding, where 1 means that the two respective nodes are connected and 0 that they’re not. We can gather all the information for all pairs in a matrix that is commonly known as the adjacency matrix.

# Distance matrix needed as input
dist_matrix <-rdist(tibble(x,y))

weight_matrix <- outer(weights, weights, FUN="*") # Weight matrix

con_matrix_prob <- phi(1/beta * weight_matrix^gamma*dist_matrix^2)# Evaluation

con_matrix <- Matrix(rbernoulli(1,con_matrix_prob), sparse=TRUE) # Sampling
con_matrix <- con_matrix * upper.tri(con_matrix) # Transform to symmetric matrix
adjacency_matrix <- con_matrix + t(con_matrix)

Visualization with ggraph

In an earlier post we praised visNetwork as our go-to package for beautiful interactive graph visualization in R. While this remains true, we also have lots of love for tidyverse, and ggraph (spoken „g-giraffe“) as an extension of ggplot2 proves to be a comfortable alternative for non-interactive graph plots, especially when you’re already familiar with the grammar of graphics. In combination with tidygraph, which lets us describe a graph as two tidy data frames (one for the nodes and one for the edges), we obtain a full-fledged tidyverse experience. Note that tidygraph is based on a graph manipulation library called igraph from which it inherits all functionality and „exposes it in a tidy manner“. So before we get cracking with the visualization in ggraph, let’s first tidy up our data with tidygraph!

Make graph data tidy again!

Let’s attach some new columns to the node dataframe which will be useful for visualization. After we created the tidygraph object, this can be done in the usual dplyr fashion after using activate(nodes)and activate(edges)for accessing the respective dataframes.

# Create Igraph object
graph <- graph_from_adjacency_matrix(adjacency_matrix, mode="undirected")

# Make a tidygraph object from it. Igraph methods can still be called on it.
tbl_graph <- as_tbl_graph(graph)

hub_id <- which.max(degree(graph))

# Add spacial positions, hub distance and degree information to the nodes.
tbl_graph <- tbl_graph %>%
  activate(nodes) %>%
  mutate(
    x = x,
    y = y,
    hub_dist = replace_na(bfs_dist(root = hub_id), Inf),
    degree = degree(graph),
    friends_of_friends = replace_na(local_ave_degree(), 0),
    cluster = as.factor(group_infomap())
  )

Tidygraph supports most of igraphs methods, either directly or in the form of wrappers. This also applies to most of the functions used above. For example breadth-first search is implemented as the bfs_* family, wrapping igraph::bfs(), the group_graphfamily wraps igraphs clustering functions and local_ave_degree() wraps igraph::knn().

Let’s visualize!

GGraph is essentially built around three components: Nodes, Edges and Layouts. Nodes that are connected by edges compose a graph which can be created as an igraph object. Visualizing the igraph object can be done in numerous ways: Remember that nodes usually are not endowed with any coordinates. Therefore, arranging them in space can be done pretty much arbitrarily. In fact, there’s a specific research branch called graph drawing that deals with finding a good layout for a graph for a given purpose.

Usually, the main criteria of a good layout are aesthetics (which is often interchangeable with clearness) and capturing specific graph properties. For example, a layout may force the nodes to form a circle, a star, two parallel lines, or a tree (if the graph’s data allows for it). Other times you might want to have a layout with a minimal number of intersecting edges. Fortunately, in ggraph all the layouts from igraph can be used.

We start with a basic plot by passing the data and the layout to ggraph(), similar to what you would do with ggplot() in ggplot2. We can then add layers to the plot. Nodes can be created by using geom_node_point()and edges by using geom_edge_link(). From then on, it’s full-on ggplot2-style.

# Add coord_fixed() for fixed axis ratio!
basic <- tbl_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1) +
  geom_node_point(aes(size = degree, color = degree)) +
  scale_color_gradient(low = "dodgerblue2", high = "firebrick4") +
  coord_fixed() +
  guides(size = FALSE)

To see more clearly what nodes are essential to the network, the degree, which is the number of edges a node is connected with, was highlighted for each node. Another way of getting a good overview of the graph is to show a visual decomposition of the components. Nothing easier than that!

cluster <- tbl_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1) +
  geom_node_point(aes(size = degree, color = cluster)) +
  coord_fixed() +
  theme(legend.position = "none")

Wouldn’t it be interesting to visualize the reach of a hub node? Let’s do it with a facet plot:

# Copy of tbl_graph with columns that indicate weather in n - reach of hub.
reach_graph <- function(n) {
  tbl_graph %>%
    activate(nodes) %>%
    mutate(
      reach = n,
      reachable = ifelse(hub_dist <= n, "reachable", "non_reachable"),
      reachable = ifelse(hub_dist == 0, "Hub", reachable)
    )
}
# Tidygraph allows to bind graphs. This means binding rows of the node and edge dataframes.
evolving_graph <- bind_graphs(reach_graph(0), reach_graph(1), reach_graph(2), reach_graph(3))

evol <- evolving_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1, alpha = .2) +
  geom_node_point(aes(size = degree, color = reachable)) +
  scale_size(range = c(.5, 2)) +
  scale_color_manual(values = c("Hub" = "firebrick4",
                                "non_reachable" = "#00BFC4",
                                "reachable" = "#F8766D","")) +
  coord_fixed() +
  facet_nodes(~reach, ncol = 4, nrow = 1, labeller = label_both) +
  theme(legend.position = "none")

A curious observation

At this point, there are many graph properties (including the three above but also cluster sizes and graph distances) that are worth taking a closer look at, but this is beyond the scope of this blogpost. However, let’s look at one last thing. Somebody just recently told me about a very curious fact about social networks that seems paradoxical at first: Your average friend on Facebook (or Instagram) has way more friends than the average user of that platform.

It sounds odd, but if you think about it for a second, it is not too surprising. Sampling from the pool of your friends is very different from sampling from all users on the platform (entirely at random). It’s exactly those very prominent people who have a much higher probability of being among your friends. Hence, when calculating the two averages, we receive very different results.

As can be seen, the model also reflects that property: In the small excerpt of the graph that we simulate, the average node has a degree of around 5 (blue intercept). The degree of connected nodes is over 10 on average (red intercept).

Conclusion

In the first part, I introduced a model that describes the features of real-life data of social networks well. In the second part, we obtained artificial data from that model and used it to create an igraph object (by means of the adjacency matrix). The latter can then be transformed into a tidygraph object, allowing us to easily make manipulation on the node and edge tibble to calculate any graph statistic (e.g., the degree) we like. Further, the tidygraph object is then used for conveniently visualizing the network through Ggraph.

I hope that this post has sparked your interest in network modeling and has given you an idea of how seamlessly graph manipulation and visualization with Tidygraph and Ggraph merge into the usual tidyverse workflow. Have a wonderful day!

„There is no way you know Thomas! What a coincidence! He’s my best friend’s saxophone teacher! This cannot be true. Here we are, at the other end of the world and we meet? What are the odds?“ Surely, not only us here at STATWORX have experienced similar situations, be it in a hotel’s lobby, on the far away hiking trail or in the pub in that city you are completely new to. However, the very fact that this story is so suspiciously relatable might indicate that the chances of being socially connected to a stranger by a short chain of friends of friends isn’t too low after all.

Lots of research has been done in this field, one particular popular result being the 6-Handshake-Rule. It states that most people living on this planet are connected by a chain of six handshakes or less. In the general setting of graphs, in which edges connect nodes, this is often referred to as the so-called small-world-effect. That is to say, the typical number of edges needed to get from node A to node B grows logarithmically in population size (i.e., # nodes). Note that, up until now, no geographic distance has been included in our consideration, which seems inadequate as it plays a significant role in social networks.

When analyzing data from social networks such as Facebook or Instagram, three observations are especially striking:

A model that explains these observations

Clearly, due to the characteristics of social networks mentioned above, only a model that includes geographic distances of the individuals makes sense. Also, to account for the occurrence of hubs, research has shown that reasonable models attach a random weight to each node (which can be regarded as the social attractiveness of the respective individual). A model that accounts for all three properties is the following: First, randomly place nodes in space with a certain intensity nu, which can be done with a Poisson process. Then, with an independent uniformly distributed weight U_x attached to each node x, every two nodes get connected by an edge with a probability

    \[p_{xy} =mathbb{P}(xtext{ is connected to } y):=varphi(frac{1}{beta}U_x^gamma U_y^gamma vert x-yvert^d)\]

where d is the dimension of the model (here: d=2 as we’ll simulate the model on the plane), model parameter gammain [0,1] controls the impact of the weights, model parameter beta>0 squishes the overall input to the profile function varphi, which is a monotonously decreasing, normalized function that returns a value between 0 and 1.

That is, of course, what we want because its output shall be a probability. Take a moment to go through the effects of different beta and gamma on p_{xy}. A higher beta yields a smaller input value for varphi and thereby a higher connection probability. Similarly, a high gamma entails a lower U^gamma (as Uin [0,1]) and thus a higher connection probability. All this comprises a scale-free random connection model, which can be seen as a generalization of the model by Deprez and Würthrich. So much about the theory. Now that we have a model, we can use this to generate synthetic data that should look similar to real-world data. So let’s simulate!

Obtain data through simulation

From here on, the simulation is pretty straight forward. Don’t worry about specific numbers at this point.

library(tidyverse)
library(fields)
library(ggraph)
library(tidygraph)
library(igraph)
library(Matrix)

# Create a vector with plane dimensions. The random nodes will be placed on the plane.
plane <- c(1000, 1000)

poisson_para <- .5 * 10^(-3) # Poisson intensity parameter
beta <- .5 * 10^3
gamma <- .4

# Number of nodes is Poisson(gamma)*AREA - distributed
n_nodes <- rpois(1, poisson_para * plane[1] * plane[2])
weights <- runif(n_nodes) # Uniformly distributed weights

# The Poisson process locally yields node positions that are completely random.
x = plane[1] * runif(n_nodes)
y = plane[2] * runif(n_nodes)

phi <- function(z) { # Connection function
  pmin(z^(-1.8), 1)
} 

What we need next is some information on which nodes are connected. That means, we need to first get the connection probability by evaluating varphi for each pair of nodes and then flipping a biased coin, accordingly. This yields a 0-1 encoding, where 1 means that the two respective nodes are connected and 0 that they’re not. We can gather all the information for all pairs in a matrix that is commonly known as the adjacency matrix.

# Distance matrix needed as input
dist_matrix <-rdist(tibble(x,y))

weight_matrix <- outer(weights, weights, FUN="*") # Weight matrix

con_matrix_prob <- phi(1/beta * weight_matrix^gamma*dist_matrix^2)# Evaluation

con_matrix <- Matrix(rbernoulli(1,con_matrix_prob), sparse=TRUE) # Sampling
con_matrix <- con_matrix * upper.tri(con_matrix) # Transform to symmetric matrix
adjacency_matrix <- con_matrix + t(con_matrix)

Visualization with ggraph

In an earlier post we praised visNetwork as our go-to package for beautiful interactive graph visualization in R. While this remains true, we also have lots of love for tidyverse, and ggraph (spoken „g-giraffe“) as an extension of ggplot2 proves to be a comfortable alternative for non-interactive graph plots, especially when you’re already familiar with the grammar of graphics. In combination with tidygraph, which lets us describe a graph as two tidy data frames (one for the nodes and one for the edges), we obtain a full-fledged tidyverse experience. Note that tidygraph is based on a graph manipulation library called igraph from which it inherits all functionality and „exposes it in a tidy manner“. So before we get cracking with the visualization in ggraph, let’s first tidy up our data with tidygraph!

Make graph data tidy again!

Let’s attach some new columns to the node dataframe which will be useful for visualization. After we created the tidygraph object, this can be done in the usual dplyr fashion after using activate(nodes)and activate(edges)for accessing the respective dataframes.

# Create Igraph object
graph <- graph_from_adjacency_matrix(adjacency_matrix, mode="undirected")

# Make a tidygraph object from it. Igraph methods can still be called on it.
tbl_graph <- as_tbl_graph(graph)

hub_id <- which.max(degree(graph))

# Add spacial positions, hub distance and degree information to the nodes.
tbl_graph <- tbl_graph %>%
  activate(nodes) %>%
  mutate(
    x = x,
    y = y,
    hub_dist = replace_na(bfs_dist(root = hub_id), Inf),
    degree = degree(graph),
    friends_of_friends = replace_na(local_ave_degree(), 0),
    cluster = as.factor(group_infomap())
  )

Tidygraph supports most of igraphs methods, either directly or in the form of wrappers. This also applies to most of the functions used above. For example breadth-first search is implemented as the bfs_* family, wrapping igraph::bfs(), the group_graphfamily wraps igraphs clustering functions and local_ave_degree() wraps igraph::knn().

Let’s visualize!

GGraph is essentially built around three components: Nodes, Edges and Layouts. Nodes that are connected by edges compose a graph which can be created as an igraph object. Visualizing the igraph object can be done in numerous ways: Remember that nodes usually are not endowed with any coordinates. Therefore, arranging them in space can be done pretty much arbitrarily. In fact, there’s a specific research branch called graph drawing that deals with finding a good layout for a graph for a given purpose.

Usually, the main criteria of a good layout are aesthetics (which is often interchangeable with clearness) and capturing specific graph properties. For example, a layout may force the nodes to form a circle, a star, two parallel lines, or a tree (if the graph’s data allows for it). Other times you might want to have a layout with a minimal number of intersecting edges. Fortunately, in ggraph all the layouts from igraph can be used.

We start with a basic plot by passing the data and the layout to ggraph(), similar to what you would do with ggplot() in ggplot2. We can then add layers to the plot. Nodes can be created by using geom_node_point()and edges by using geom_edge_link(). From then on, it’s full-on ggplot2-style.

# Add coord_fixed() for fixed axis ratio!
basic <- tbl_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1) +
  geom_node_point(aes(size = degree, color = degree)) +
  scale_color_gradient(low = "dodgerblue2", high = "firebrick4") +
  coord_fixed() +
  guides(size = FALSE)

To see more clearly what nodes are essential to the network, the degree, which is the number of edges a node is connected with, was highlighted for each node. Another way of getting a good overview of the graph is to show a visual decomposition of the components. Nothing easier than that!

cluster <- tbl_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1) +
  geom_node_point(aes(size = degree, color = cluster)) +
  coord_fixed() +
  theme(legend.position = "none")

Wouldn’t it be interesting to visualize the reach of a hub node? Let’s do it with a facet plot:

# Copy of tbl_graph with columns that indicate weather in n - reach of hub.
reach_graph <- function(n) {
  tbl_graph %>%
    activate(nodes) %>%
    mutate(
      reach = n,
      reachable = ifelse(hub_dist <= n, "reachable", "non_reachable"),
      reachable = ifelse(hub_dist == 0, "Hub", reachable)
    )
}
# Tidygraph allows to bind graphs. This means binding rows of the node and edge dataframes.
evolving_graph <- bind_graphs(reach_graph(0), reach_graph(1), reach_graph(2), reach_graph(3))

evol <- evolving_graph %>%
  ggraph(layout = tibble(V(.)x, V(.)y)) +
  geom_edge_link(width = .1, alpha = .2) +
  geom_node_point(aes(size = degree, color = reachable)) +
  scale_size(range = c(.5, 2)) +
  scale_color_manual(values = c("Hub" = "firebrick4",
                                "non_reachable" = "#00BFC4",
                                "reachable" = "#F8766D","")) +
  coord_fixed() +
  facet_nodes(~reach, ncol = 4, nrow = 1, labeller = label_both) +
  theme(legend.position = "none")

A curious observation

At this point, there are many graph properties (including the three above but also cluster sizes and graph distances) that are worth taking a closer look at, but this is beyond the scope of this blogpost. However, let’s look at one last thing. Somebody just recently told me about a very curious fact about social networks that seems paradoxical at first: Your average friend on Facebook (or Instagram) has way more friends than the average user of that platform.

It sounds odd, but if you think about it for a second, it is not too surprising. Sampling from the pool of your friends is very different from sampling from all users on the platform (entirely at random). It’s exactly those very prominent people who have a much higher probability of being among your friends. Hence, when calculating the two averages, we receive very different results.

As can be seen, the model also reflects that property: In the small excerpt of the graph that we simulate, the average node has a degree of around 5 (blue intercept). The degree of connected nodes is over 10 on average (red intercept).

Conclusion

In the first part, I introduced a model that describes the features of real-life data of social networks well. In the second part, we obtained artificial data from that model and used it to create an igraph object (by means of the adjacency matrix). The latter can then be transformed into a tidygraph object, allowing us to easily make manipulation on the node and edge tibble to calculate any graph statistic (e.g., the degree) we like. Further, the tidygraph object is then used for conveniently visualizing the network through Ggraph.

I hope that this post has sparked your interest in network modeling and has given you an idea of how seamlessly graph manipulation and visualization with Tidygraph and Ggraph merge into the usual tidyverse workflow. Have a wonderful day!