inhaltsanalyse-mit-r.de

Dieses erste Kapitel liefert einen Überblick über zahlreiche Funktionen des Pakets quanteda, die gleichzeitig die Grundlage der automatisierten Inhaltsanalyse mit R bilden. Über quanteda hinaus werden im Verlauf dieser neunteiligen Einführung noch eine Reihe weiterer R-Bibliotheken verwendet, etwa für das Berechnen von Themenmodellen (Kapitel 5) und das überwachte maschinelle Lernen (Kapitel 6). In praktisch jeden Einheit relevant sind dabei die Pakete des tidyverse (vor allem ggplot, dplyr, stringr), durch die zahlreiche Funkionen wie Plotten, Textverarbeitung und Datenmanagement gegenüber den R-Basisfunktionen stark verbessert werden. Pakete für einzelne Teilbereiche die erst später eine Rolle spielen werden sind u.a. topicmodels und stm (Themenmodelle), RTextTools (überwachtes maschinelles Lernen), und spacyr (POS-Tagging und Named-Entity-Erkennung).

Die Basis der Analyse in diesem ersten Kapitel sind die beliebten Geschichten von Sherlock Holmes. Das Sherlock Holmes-Korpus besteht aus zwölf Erzählungen, die in dem 1892 erschienenem Band The Adentures of Sherlock Holmes zusammengefasst sind, und die man gemeinfrei unter anderem durch das Internet Archive herunterladen kann. Die für diese Einführung verwendete Fassung wurde zunächst dem Internet Archive entnommen und dann in zwölf Einzeldateien aufgeteilt. Natürlich können die vorgestellten Methoden auf die anderen hier behandelten Korpora angewandt werden – das Beispiel dient nur dazu, sich langsam an quanteda und die Grundlagen der computergestützen Inhaltsanalyse zu gewöhnen.

Sämtliche in dieser Einführung verwendeter Codebeispiele, Korpora und Lexika können hier heruntergeladen werden.

Installation und Laden der benötigten R-Bibliotheken

Zunächst werden die notwendigen Bibliotheken installiert (sofern noch nicht vorhanden) und anschließend geladen. Zudem wird vorbereitend die Theme-Einstellung für das Paket ggplot gesetzt (dies sorgt für hübschere Plots). Diesen Schritt wiederholen wir zu Beginn jedes Kapitels, in einigen Kapiteln werden noch weiteren Pakete gelanden.

# Installation und Laden der Bibliotheken
if(!require("quanteda")) install.packages("quanteda")
Lade nötiges Paket: quanteda
Package version: 1.3.4
Parallel computing: 2 of 4 threads used.
See https://quanteda.io for tutorials and examples.

Attache Paket: ‘quanteda’

The following object is masked from ‘package:utils’:

    View
if(!require("readtext")) install.packages("readtext")
Lade nötiges Paket: readtext
if(!require("tidyverse")) install.packages("tidyverse")
Lade nötiges Paket: tidyverse
── Attaching packages ─────────────────────────────────────────────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.0.0     ✔ purrr   0.2.5
✔ tibble  1.4.2     ✔ dplyr   0.7.6
✔ tidyr   0.8.1     ✔ stringr 1.3.1
✔ readr   1.1.1     ✔ forcats 0.3.0
── Conflicts ────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
if(!require("RColorBrewer")) install.packages("RColorBrewer")
Lade nötiges Paket: RColorBrewer
theme_set(theme_bw())

Einlesen der Daten und Anlegen eines Korpus

Nachdem alle notwendigen Pakete geladen wurden, können wir nun die Daten einlesen und daraus ein quanteda-Korpus erstellen. Für das Einlesen der Plaintext-Dateien wird die Funktion readtext aus dem gleichnamigen Paket verwendet, durch die sich eine Reihe von Dateiformaten erfolgreich einlesen lassen (u.a. TXT, PDF, Word). Anschließend wird die Endung “.txt” aus dem Dokumentnamen entfernt. Schließlich wird die Variable korpus aufgerufen, was die wichtigen Eckdaten Dokumentanzahl und Docvars zurückliefert.

daten.sherlock <- readtext("daten/sherlock/romane/[0-9]*.txt") # Dateiname beginnt mit Zahl und endet mit .txt
daten.sherlock$doc_id <- str_sub(daten.sherlock$doc_id, start = 4, end = -5) # Dateiendung weglassen
korpus <- corpus(daten.sherlock, docid_field = "doc_id") # Korpus anlegen
docvars(korpus, "Textnummer") <- sprintf("%02d", 1:ndoc(korpus)) # Variable Textnummer generieren
korpus
Corpus consisting of 12 documents and 1 docvar.

In den folgenden Abschnitten werden häufig bereits verbereite Korpora geladen, d.h. der Befehl corpus() wird hier nicht mehr ausgeführt. Er ist aber im Vorfeld ausgeführt worden, um aus Textdatein auf der Festplatte oder Twitter-Daten in einem R-Data Frame ein quanteda-Korpus zu erstellen.

Die Funktionen ndoc(), ntoken(), ntype() und nsentence() geben die Anzahl der Dokumente, Tokens, Types und Sätze aus. Diese Statistiken können bequem gemeinsam mit Metadaten auf Dokumentebene durch die Funktion summary() zusammengefasst werden. Bei den meisten Korpora, die hier verwendet werden, liegt ein solcher Data Frame mit Statistiken zu jedem Text bereits bei.

korpus.stats <- summary(korpus, n = 1000000)
korpus.stats$Text <- reorder(korpus.stats$Text, 1:ndoc(korpus), order = T)
korpus.stats
Corpus consisting of 12 documents:

                                  Text Types Tokens Sentences Textnummer
                  A Scandal in Bohemia  2145  10542       669         01
                 The Red-headed League  2087  11118       573         02
                    A Case of Identity  1750   8506       396         03
           The Boscombe Valley Mystery  2096  11499       636         04
                  The Five Orange Pips  1925   8879       475         05
          The Man with the Twisted Lip  2173  11160       586         06
   The Adventure of the Blue Carbuncle  1926   9651       552         07
    The Adventure of the Speckled Band  2232  11783       614         08
 The Adventure of the Engineer's Thumb  1968   9999       508         09
   The Adventure of the Noble Bachelor  1944   9987       540         10
    The Adventure of the Beryl Coronet  1991  11669       626         11
   The Adventure of the Copper Beeches  2110  12011       622         12

Source: /Users/cp/Dropbox/Lehre/inhaltsanalyse-mit-r/* on x86_64 by cp
Created: Sat Sep  8 14:39:49 2018
Notes: 

Das Funktionsargument n = 1000000 wird hier nur deshalb verwendet, weil die Funktion summary() ansonsten nur maximal 100 Texte zusammenfasst. In diesem Fall reicht das zwar aus, aber bei größeren Datensätzen ist das eher unpraktisch. Technisch gesehen heißt diese Funktion summary.corpus() und ist eine an Korpus-Objekte angepasste Variante der Basisfunktion summary(), die auch sonst in R verwendet wird. Der Befehl reorder() wird verwendet um die Texte nach ihrer Nummerierung zu sortieren, statt alphabetisch nach Titel.

Basisstatistiken zu einem Korpus berechnen

Der Inhalt der Variable korpus.stats kann natürlich auch geplottet werden, um einen anschaulichen Eindruck von der Korpusbeschaffenheit zu geben. Die folgenden Zeilen liefern die Anzahl der Tokens (laufende Wörter), die Anzahl der Types (einmalige Wörter), und Sätze pro Roman zurück (vgl. dazu diese Einführung). Schließlich wird noch das Verhältnis von Typen zu Tokens (oder die sog. Typ-Token-Relation) geplottet.

Grundlage solcher Plots sind praktisch immer Data Frame-Objekte (also Tabellen), die Informationen über Korpora, Texte, Wörter, Themen usw. enthalten, welche sich visuell darstellen lassen. Im Rest dieser Einführung gehe ich nicht im Detail darauf ein, wie die jeweiligen Plots genau konstruiert werden, allerdings lassen sich die meisten Daten auch (etwas weniger ansprechend) mit der R-internen Funktion plot() darstellen. Eine hilfreiche deutschsprachige Einführung in das Plotten mit ggplot2 findet sich hier. Viele der hier vorgestellten Plots stammen zudem direkt aus quanteda (beginnend mit textplot_).

ggplot(korpus.stats, aes(Text, Tokens, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Tokens pro Roman")

ggplot(korpus.stats, aes(Text, Types, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Types pro Roman")

ggplot(korpus.stats, aes(Text, Sentences, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Sätze pro Roman")

ggplot(korpus.stats, aes(Tokens, Types, group = 1, label = Textnummer)) + geom_smooth(method = "lm", se = FALSE) + geom_text(check_overlap = T) + ggtitle("Typ-Token-Relation pro Roman")

Diese Grafiken sind zunächst einmal nicht umwerfend informativ. Sie belegen lediglich, dass die Erzählungen ‘A Case of Identity’ und (in geringerem Maße) ‘The Five Orangen Pips’ deutlich kürzer sind als die anderen Texte, was sich auf allen drei Ebenen (Tokens, Types, Sätze) niederschlägt. Etwas interessanter wird es allerdings bei der Typ-Token-Relation: während drei Romane (mit den Nummern 3, 11 und 12) jeweils einen eher unterdurchschnittlichen TTR aufweisen, liegen weitere vier oberhalb der linearen Relation (1, 5, 6 und 8), während die verbleibenden sechs ziemlich genau dem Durchschnitt entsprechen. Über den TTR lassen sich Rückschlüssen über die Informationsdichte ziehen – dazu später noch mehr.

Mit Korpora arbeiten

Korpora lassen sich in quanteda sehr leicht samplen, umformen und mit zusätlichen Metadaten versehen. Metadaten können wiederum genutzt werden, um das Korpus nach bestimmten Kriterien zu filtern.

str_sub(korpus[1], start = 1, end = 1000) # Anfang des ersten Romans wiedergeben
[1] "A Scandal in Bohemia\n\n  To Sherlock Holmes she is always the woman. I have seldom\nheard him mention her under any other name. In his eyes she\neclipses and predominates the whole of her sex. It was not that\nhe felt any emotion akin to love for Irene Adler. All emotions,\nand that one particularly, were abhorrent to his cold, precise but\nadmirably balanced mind. He was, I take it, the most perfect\nreasoning and observing machine that the world has seen, but as\na lover he would have placed himself in a false position. He\nnever spoke of the softer passions, save with a gibe and a sneer.\nThey were admirable things for the observer -- excellent for draw-\ning the veil from men's motives and actions. But for the trained\nteasoner to admit such intrusions into his own delicate and finely\nadjusted temperament was to introduce a distracting factor which\nmight throw a doubt upon all his mental results. Grit in a\nsensitive instrument, or a crack in one of his own high-power\nlenses, would not be more "

Jeder Text lässt sich also anhand seiner Indizierung (etwa korpus[1] für den ersten Text) aufrufen.

Mittels corpus_reshape() lässt sich ein Korpus so umformen, dass jeder Satz ein eigenes Dokument ergibt. Alternative Argumente sind ‘paragraphs’ und ‘documents’ (so lässt sich ein Satz-Korpus wieder in seinen Anfangszustand zurückversetzen). Die Erstellung von Satz-Korpora ist für die Sentimentanalyse und das überwachte maschinelle Lernen von Interesse.

Die Beschriftung des Beispiels besteht hier aus der Variable docname und einer angehängten Zahl (eine 1 für den ersten Satz).

korpus.saetze <- corpus_reshape(korpus, to = "sentences")
korpus.saetze[1]
                                               A Scandal in Bohemia.1 
"A Scandal in Bohemia    To Sherlock Holmes she is always the woman." 

Mit corpus_sample() kann weiterhin ein zufälliges Sample aus dem Satz-Korpus gezogen werden.

zufallssatz <- corpus_sample(korpus.saetze, size = 1)
zufallssatz[1]
                                The Adventure of the Speckled Band.567 
"With a grave face he lit the lamp and led the way down the corridor." 

Anhand von corpus_subset kann ein Korpus nach Metadaten gefiltert werden. Hier geschieht dies mittels der neu erstellten binären Variable LangerSatz, die dann TRUE ist, wenn ein Satz >= 25 Tokens enthält). So lässt sich ein Teilkorpus zu bilden, in dem nur längere Sätze enthalten sind. Korpora lassen sich mit corpus_segment() auch nach bestimmten Kriterien aufspalten

docvars(korpus.saetze, "Zeichenanzahl") <- ntoken(korpus.saetze)
docvars(korpus.saetze, "LangerSatz") <- ntoken(korpus.saetze)>=25
korpus.saetze_lang <- corpus_subset(korpus.saetze, LangerSatz == TRUE)
korpus.saetze_lang[1:3]
                                                                                                                                                                                    A Scandal in Bohemia.6 
                                           "He was, I take it, the most perfect reasoning and observing machine that the world has seen, but as a lover he would have placed himself in a false position." 
                                                                                                                                                                                    A Scandal in Bohemia.9 
"But for the trained teasoner to admit such intrusions into his own delicate and finely adjusted temperament was to introduce a distracting factor which might throw a doubt upon all his mental results." 
                                                                                                                                                                                   A Scandal in Bohemia.10 
                                             "Grit in a sensitive instrument, or a crack in one of his own high-power lenses, would not be more disturbing than a strong emotion in a nature such as his." 

Tokenisierung

Unter Tokensiierung versteht man die Aufspaltung eines Textes in laufende Wörter oder sog. N-Gramme, also Sequenzen mehrerer Wörter in Folge. Die Funktion tokens() realisiert die Tokenisierung eines Korpus in quanteda.

meine.tokens <- tokens(korpus)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A"        "Scandal"  "in"       "Bohemia"  "To"       "Sherlock"

Mittels der Funktion tokens lässt sich der Text über das Argument ngrams auch gleich in N-Gramme (Mehrwortsequenzen) aufspalten. Im folgenden Beispiel werden erst Bigramme von Anfang des ersten Textes angezeigt, und dann alle Sequenzen von einem, zwei oder drei Begriffen extrahiert (durch die Anwendung von head() sehen wir nur Trigramme, es sind aber auch kürzere Sequenzen vorhanden).

meine.tokens <- tokens(korpus, ngrams = 2)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A_Scandal"       "Scandal_in"      "in_Bohemia"      "Bohemia_To"      "To_Sherlock"     "Sherlock_Holmes"
meine.tokens <- tokens(korpus, ngrams = 1:3)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A"        "Scandal"  "in"       "Bohemia"  "To"       "Sherlock"

Hilfreich ist auch die Möglichkeit, bei der Tokenisierung bestimmte Begriffe zu entfernen oder zurückzubehalten.

meine.tokens <- tokens(korpus)
begriffe.behalten <- tokens_select(meine.tokens, c("holmes", "watson")) # Platzhalter mit padding = TRUE
head(begriffe.behalten$`A Scandal in Bohemia`)
[1] "Holmes" "Holmes" "Holmes" "Holmes" "Watson" "Watson"
begriffe.entfernen <- tokens_remove(meine.tokens, c("Sherlock", "in", "is", "the"))
head(begriffe.entfernen$`A Scandal in Bohemia`)
[1] "A"       "Scandal" "Bohemia" "To"      "Holmes"  "she"    

Die Funktion tokens() akzeptiert eine Reihe von Argumenten, mit denen ganze Klassen von Zeichenketten (Zahlen, Interpunktion, Symbole usw.) ausgeschlossen oder zurückbehalten werden können. Folgend werden zunächst Zahlen, Interpunktion und Symbole entfernt, dann mittels tokens_tolower() alle Wörter in Kleinschreibung umgewandelt und dann dann noch die Wörter ‘sherlock’ und ‘holmes’, sowei eine Reihe englischer Stoppwörter entfernt.

meine.tokens <- tokens(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE)
meine.tokens <- tokens_tolower(meine.tokens)
meine.tokens <- tokens_remove(meine.tokens, c(stopwords("english"), "sherlock", "holmes"))
head(meine.tokens$`A Scandal in Bohemia`)
[1] "scandal" "bohemia" "always"  "woman"   "seldom"  "heard"  

Das Resultat ist der Art von Daten mit denen man bei Verfahren wie der Anwendung von Lexika (Kapitel 2), der Berechnung von Themenmodellen (Kapitel 3) und dem überwachten maschinelle Lernen (Kapitel 4) arbeitet sehr ähnlich. Durch die Stoppwortentfernung und andere Schritte gehen syntaktische Informationen verloren, d.h. man kann nicht mehr nachvollziehen, wer was mit wem tut, oder wie der Text insgesamt argumentativ oder erzählerisch aufgebaut ist. Diese Informationen sind allerdings im ‘Bag-of-Words-Ansatz’, der in der automatisierten Inhaltsanalyse nahezu immer verwendet wird, nicht unbedingt relevant.

Dokument-Feature-Matrizen (DFMs) erstellen

Wir kommen nun zu einer zentralen Datenstruktur von quanteda, die im Gegensatz zu den zuvor vorgestellten Einheiten praktisch in jedem Projekt vorkommt: die Document Feature-Matrize (DFM). Üblicherweise wird direkt nachdem ein Korpus angelegt wurde berechnet Eine DFM ist eine Tabelle, deren Zeilen diee Texte und deren Spalten die Wortfrequenzen enhalten. Dabei gehen Informationen darüber, wo in einem Text ein Wort vorkommt verloren (man spricht auch vom ‘Bag-of-Words-Ansatz’). Immer dann, wenn wir uns für die Beziehung von Wörtern zu Texten (und umgekehrt) interessieren, berechnen wir eine DFM.

meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = stopwords("english"))
meine.dfm
Document-feature matrix of: 12 documents, 8,489 features (79.1% sparse).

Wichtig: Hier wird implizit der uns schon vertraute Befehl tokens() angewandt, um bestimmte Features zu entfernen. Vieles funktioniert bei DFMs analog zur Erstellung eines Korpus. So zählen die Funktionen ndoc() und nfeat() Dokumente und Features (Wörter).

ndoc(meine.dfm)
[1] 12
nfeat(meine.dfm)
[1] 8489

Mittels der Funktionen docnames() und featnames() lassen sich die Namen der Dokumente und Features ausgeben.

head(docnames(meine.dfm)) # In der DFM enthaltene Dokumente 
[1] "A Scandal in Bohemia"         "The Red-headed League"        "A Case of Identity"          
[4] "The Boscombe Valley Mystery"  "The Five Orange Pips"         "The Man with the Twisted Lip"
head(featnames(meine.dfm), 50) # Features in chronologischer Reihenfolge
 [1] "scandal"      "bohemia"      "sherlock"     "holmes"       "always"       "woman"        "seldom"      
 [8] "heard"        "mention"      "name"         "eyes"         "eclipses"     "predominates" "whole"       
[15] "sex"          "felt"         "emotion"      "akin"         "love"         "irene"        "adler"       
[22] "emotions"     "one"          "particularly" "abhorrent"    "cold"         "precise"      "admirably"   
[29] "balanced"     "mind"         "take"         "perfect"      "reasoning"    "observing"    "machine"     
[36] "world"        "seen"         "lover"        "placed"       "false"        "position"     "never"       
[43] "spoke"        "softer"       "passions"     "save"         "gibe"         "sneer"        "admirable"   
[50] "things"      

Die tabellarische Ansicht illustriert den Inhalt der DFM als Text-Wort-Matrix am besten. Die sparsity (“Spärlichkeit”) einer DFM beschreibt dabei den Anteil der leeren Zellen, also Wörter, die nur in sehr wenigen Texten vorkommen. Wie sich leicht ableiten lässt, werden DFMs sehr schnell sehr groß. Zum Glück macht sich quanteda eine Reihe von für den Nutzer unsichtbaren Funktionen aus anderen Paketen zunutze, um diesem Problem zu begegnen.

head(meine.dfm, n = 12, nf = 10) # Features und Texte als Matrix in chronologischer Reihenfolge
Document-feature matrix of: 12 documents, 10 features (30.8% sparse).
12 x 10 sparse Matrix of class "dfm"
                                       features
docs                                    scandal bohemia sherlock holmes always woman seldom heard mention name
  A Scandal in Bohemia                        4       8       11     47      5    12      3     8       1    6
  The Red-headed League                       0       0       10     51      5     0      0    15       0    6
  A Case of Identity                          0       2        7     46      7    10      0     5       0    1
  The Boscombe Valley Mystery                 1       0       10     43      5     1      0    10       0    3
  The Five Orange Pips                        1       0       10     25      5     1      0     5       1    5
  The Man with the Twisted Lip                0       0       10     28      4     5      0     8       0    4
  The Adventure of the Blue Carbuncle         0       0       10     34      5     0      1     3       0   10
  The Adventure of the Speckled Band          0       0        9     55      8     5      1    20       0    6
  The Adventure of the Engineer's Thumb       0       0        5     12      0     6      1    11       0    3
  The Adventure of the Noble Bachelor         1       0        7     34      3     8      0    11       0    6
  The Adventure of the Beryl Coronet          4       0        3     26      3     5      0    12       0    8
  The Adventure of the Copper Beeches         0       1        2     42      7     8      0     5       0    4

Gleich an den ersten Blick fällt auf, das die Wörter ‘sherlock’ und ‘holmes’ in allen Romanen vorkommen, also sehr wenig distinktiv sind, weshalb wir sie unter Umständen zu den Stoppwörtern für dieses Korpus hinzufügen sollten.

Die Funktion topfeatures() zählt Features in der gesamten DFM aus. Die Funktion textstat_frequency() liefert zusätzlich noch den Rang (rank), die Anzahl der Dokumente, in denen das Feature vorkommt (docfreq) sowie Metadaten, nach denen bei der Zählung gefiltert wurde.

topfeatures(meine.dfm) # Features nach Frequenz
  said   upon holmes    one    man     mr little    now    see    may 
   485    465    443    372    290    275    269    234    229    197 
worthaeufigkeiten <- textstat_frequency(meine.dfm) # Worthäufigkeiten
head(worthaeufigkeiten)

Mit DFMs arbeiten

DFMs lassen sich mit dfm_sort leicht nach Dokument- und Feature-Frequenzen sortieren.

head(dfm_sort(meine.dfm, decreasing = TRUE, margin = "both"), n = 12, nf = 10) 
Document-feature matrix of: 12 documents, 10 features (0% sparse).
12 x 10 sparse Matrix of class "dfm"
                                       features
docs                                    said upon holmes one man mr little now see may
  The Adventure of the Speckled Band      44   41     55  33  11  5     17  21  22  19
  The Adventure of the Copper Beeches     47   33     42  36  34 44     37  18  17  21
  The Boscombe Valley Mystery             37   42     43  31  41 24     25  16  24  19
  The Man with the Twisted Lip            28   54     28  36  30 20     21  27  18  15
  The Adventure of the Beryl Coronet      45   33     26  32  27 20     22  29  20  25
  The Red-headed League                   51   50     51  29  25 55     25  14  23   8
  A Scandal in Bohemia                    33   25     47  27  23  9     14  17  15  21
  The Adventure of the Engineer's Thumb   47   38     12  33  17 11     25  16  16   9
  The Adventure of the Noble Bachelor     33   29     34  31  10 17     26  16  16  18
  The Adventure of the Blue Carbuncle     43   38     34  38  37 17     24  33  27   7
  The Five Orange Pips                    32   47     25  29  19  3      5  12  16  24
  A Case of Identity                      45   35     46  17  16 50     28  15  15  11

Weiterhin lassen sich bestimmte Features einer DFM gezielt mittels dfm_select auswählen.

dfm_select(meine.dfm, pattern = "lov*")
Document-feature matrix of: 12 documents, 7 features (67.9% sparse).
12 x 7 sparse Matrix of class "dfm"
                                       features
docs                                    love lover lovely loves loved lovers loving
  A Scandal in Bohemia                     5     1      1     1     1      0      0
  The Red-headed League                    1     0      0     0     0      0      0
  A Case of Identity                       2     0      0     0     0      1      0
  The Boscombe Valley Mystery              1     0      1     0     1      0      0
  The Five Orange Pips                     1     0      0     0     0      0      0
  The Man with the Twisted Lip             0     0      0     0     0      0      0
  The Adventure of the Blue Carbuncle      2     0      0     0     0      0      0
  The Adventure of the Speckled Band       1     0      1     0     0      0      0
  The Adventure of the Engineer's Thumb    1     0      0     0     0      0      0
  The Adventure of the Noble Bachelor      1     2      1     0     0      0      1
  The Adventure of the Beryl Coronet       3     4      0     2     3      0      2
  The Adventure of the Copper Beeches      0     0      1     1     0      0      0

Die Funktion dfm_wordstem() reduziert Wörter auf ihre Stammform. Diese Funktion existiert in quanteda derzeit nur für Englisch und ist auch dort nur begrenzt zuverlässig, was die folgende Ausgabe gut illustriert (‘holm’ ist kein Wortstamm).

meine.dfm.stemmed <- dfm_wordstem(meine.dfm)
topfeatures(meine.dfm.stemmed)
 said  upon  holm   one   man    mr littl   see   now  come 
  485   465   460   383   304   275   269   253   234   207 

Ebenso wie bei Wortfrequenzen in Korpora ist die Gewichtung einer DFM nach relativen Wortfrequenzen und Verfahren wie TF-IDF oftmals sinnvoll. Die Gewichtung einer DFM funktioniert immer aufgrund der Wort-Text-Relation, weshalb topfeatures() in Kombination mit dfm_weight() merkwürdige Resultate produziert. Relative Frequenzen und TF-IDF sind nur kontrastiv innerhalb der Text in einem Korpus sinnvoll (hier für ‘A Scandal in Bohemia’), da für das gesamte Korpus relative Frequenz == absolute Frequenz

meine.dfm.proportional <- dfm_weight(meine.dfm, scheme = "prop")
topfeatures(meine.dfm) # absolute Frequenzen für das gesamte Korpus
  said   upon holmes    one    man     mr little    now    see    may 
   485    465    443    372    290    275    269    234    229    197 
topfeatures(meine.dfm.proportional) # ...ergibt wenig Sinn
      said       upon     holmes        one        man         mr     little        now        see        may 
0.12564554 0.12042666 0.11388128 0.09559304 0.07426168 0.07127181 0.06916820 0.06024412 0.05900373 0.05064617 
topfeatures(meine.dfm.proportional[1,]) # ...ergibt mehr Sinn
     holmes        said         one        upon         man         may  photograph      street        know 
0.012339197 0.008663691 0.007088475 0.006563402 0.006038330 0.005513258 0.004988186 0.004725650 0.004725650 
        now 
0.004463114 

Im zweiten Beispiel sehen wir etwa, dass ‘A Scandal in Bohemia’ einen leicht höheren Anteil von Nennungen der Wortes ‘holmes’ hat, als dies im Gesamtkorpus der Fall ist. Dazu später noch etwas mehr.

Die Gewichtungsansätze Propmax und TF-IDF liefern relevante Wortmetriken, zum Beispiel für die Bestimmung von Stoppwörtern. Propmax skaliert die Worthäufigkeit relativ zum frequentesten Wort (hier ‘holmes’). Funktional ähneln sich TF-IDF und der später vorgestellte Keyness-Ansatz – beide finden besonders distinktive Terme.

meine.dfm.propmax <- dfm_weight(meine.dfm, scheme = "propmax")
topfeatures(meine.dfm.propmax[1,])
    holmes       said        one       upon        man        may photograph     street       know        now 
 1.0000000  0.7021277  0.5744681  0.5319149  0.4893617  0.4468085  0.4042553  0.3829787  0.3829787  0.3617021 
meine.dfm.tfidf <- dfm_tfidf(meine.dfm)
topfeatures(meine.dfm.tfidf)
   simon rucastle mccarthy  coronet lestrade   hosmer    clair        k   hunter   wilson 
42.08807 36.69216 34.53380 29.13789 28.01345 24.82117 24.82117 22.66281 22.66281 21.58362 

Schließlich lässt sich mit dfm_trim() noch eine reduzierten Dokument-Feature-Matrix erstellen. Das ist dann sinnvoll, wenn man davon ausgeht, dass beispielsweise nur solche Begriffe eine Rolle spielen, die mindestes X mal im Gesamtkorpus vorkommen. Auch eine Mindestzahl oder ein Maximum an Dokumenten, in denen ein Begriff vorkommen muss oder darf, kann bestimmt werden. Schließlich lassen sich beide Filteroptionen auch proportional anwenden (vgl. Beispiel).

meine.dfm.trim <- dfm_trim(meine.dfm, min_docfreq = 11) # Features, die mindestens in 11 Romanen vorkommen
head(meine.dfm.trim, n = 12, nf = 10) 
Document-feature matrix of: 12 documents, 10 features (2.5% sparse).
12 x 10 sparse Matrix of class "dfm"
                                       features
docs                                    sherlock holmes always heard name eyes whole felt one cold
  A Scandal in Bohemia                        11     47      5     8    6    9     4    2  27    2
  The Red-headed League                       10     51      5    15    6   10     9    4  29    1
  A Case of Identity                           7     46      7     5    1    6     3    3  17    1
  The Boscombe Valley Mystery                 10     43      5    10    3    6     2    0  31    1
  The Five Orange Pips                        10     25      5     5    5    5     2    2  29    1
  The Man with the Twisted Lip                10     28      4     8    4   11     3    2  36    2
  The Adventure of the Blue Carbuncle         10     34      5     3   10    2     0    4  38    5
  The Adventure of the Speckled Band           9     55      8    20    6   11     1    2  33    3
  The Adventure of the Engineer's Thumb        5     12      0    11    3    4     4    4  33    1
  The Adventure of the Noble Bachelor          7     34      3    11    6    4     4    3  31    2
  The Adventure of the Beryl Coronet           3     26      3    12    8   10     6    4  32    1
  The Adventure of the Copper Beeches          2     42      7     5    4    9     7    2  36    1
meine.dfm.trim <- dfm_trim(meine.dfm, min_termfreq = 0.95, termfreq_type = "quantile") # Features im 95. Häufigkeitsperzentil (=Top 5% aller Features)
head(meine.dfm.trim, n = 12, nf = 10) 
Document-feature matrix of: 12 documents, 10 features (4.17% sparse).
12 x 10 sparse Matrix of class "dfm"
                                       features
docs                                    sherlock holmes always woman heard name eyes whole felt one
  A Scandal in Bohemia                        11     47      5    12     8    6    9     4    2  27
  The Red-headed League                       10     51      5     0    15    6   10     9    4  29
  A Case of Identity                           7     46      7    10     5    1    6     3    3  17
  The Boscombe Valley Mystery                 10     43      5     1    10    3    6     2    0  31
  The Five Orange Pips                        10     25      5     1     5    5    5     2    2  29
  The Man with the Twisted Lip                10     28      4     5     8    4   11     3    2  36
  The Adventure of the Blue Carbuncle         10     34      5     0     3   10    2     0    4  38
  The Adventure of the Speckled Band           9     55      8     5    20    6   11     1    2  33
  The Adventure of the Engineer's Thumb        5     12      0     6    11    3    4     4    4  33
  The Adventure of the Noble Bachelor          7     34      3     8    11    6    4     4    3  31
  The Adventure of the Beryl Coronet           3     26      3     5    12    8   10     6    4  32
  The Adventure of the Copper Beeches          2     42      7     8     5    4    9     7    2  36

DFMs visualisieren

DFMs lassen sich u.a. auch als Wortwolke der häufigsten Begriffe darstellen.

textplot_wordcloud(meine.dfm, max_words = 100, scale = c(5,1))
scale is deprecated; use min_size and max_size instead

Interessanter als die Darstellung des Gesamtkorpus ist hier der Vergleich. Das folgende Plot zeigt die distinktivsten Begriffe nach TF-IDF für vier Romane, wobei die Farbe den jeweiligen Roman kennzeichnet. Dass im Plot die Wortgröße nicht die absolute Frequenz anzeigt, sondern den TF-IDF-Wert, macht ein solches Plot für den unmittelbaren Vergleich nützlich.

textplot_wordcloud(meine.dfm.tfidf[1:4,], color = brewer.pal(4, "Set1"), comparison = T)
dfm has been previously weighted

LS0tCnRpdGxlOiAiQXV0b21hdGlzaWVydGUgSW5oYWx0c2FuYWx5c2UgbWl0IFIiCmF1dGhvcjogIkNvcm5lbGl1cyBQdXNjaG1hbm4iCnN1YnRpdGxlOiAiR3J1bmRsYWdlbiB2b24gcXVhbnRlZGEiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCioqW2luaGFsdHNhbmFseXNlLW1pdC1yLmRlXShodHRwOi8vaW5oYWx0c2FuYWx5c2UtbWl0LXIuZGUvKSoqCgpEaWVzZXMgZXJzdGUgS2FwaXRlbCBsaWVmZXJ0IGVpbmVuIMOcYmVyYmxpY2sgw7xiZXIgemFobHJlaWNoZSBGdW5rdGlvbmVuIGRlcyBQYWtldHMgW3F1YW50ZWRhXShodHRwczovL3F1YW50ZWRhLmlvLyksIGRpZSBnbGVpY2h6ZWl0aWcgZGllIEdydW5kbGFnZSBkZXIgYXV0b21hdGlzaWVydGVuIEluaGFsdHNhbmFseXNlIG1pdCBbUl0oaHR0cHM6Ly93d3cuci1wcm9qZWN0Lm9yZy8pIGJpbGRlbi4gw5xiZXIgcXVhbnRlZGEgaGluYXVzIHdlcmRlbiBpbSBWZXJsYXVmIGRpZXNlciBuZXVudGVpbGlnZW4gRWluZsO8aHJ1bmcgbm9jaCBlaW5lIFJlaWhlIHdlaXRlcmVyIFItQmlibGlvdGhla2VuIHZlcndlbmRldCwgZXR3YSBmw7xyIGRhcyBCZXJlY2huZW4gdm9uIFtUaGVtZW5tb2RlbGxlbl0oNV90aGVtZW5tb2RlbGxlLmh0bWwpIChLYXBpdGVsIDUpIHVuZCBkYXMgW8O8YmVyd2FjaHRlIG1hc2NoaW5lbGxlIExlcm5lbl0oNl9tYXNjaGluZWxsZXNfbGVybmVuLmh0bWwpIChLYXBpdGVsIDYpLiBJbiBwcmFrdGlzY2ggamVkZW4gRWluaGVpdCByZWxldmFudCBzaW5kIGRhYmVpIGRpZSBQYWtldGUgZGVzIFt0aWR5dmVyc2VdKGh0dHBzOi8vd3d3LnRpZHl2ZXJzZS5vcmcvKSAodm9yIGFsbGVtIGdncGxvdCwgZHBseXIsIHN0cmluZ3IpLCBkdXJjaCBkaWUgemFobHJlaWNoZSBGdW5raW9uZW4gd2llIFBsb3R0ZW4sIFRleHR2ZXJhcmJlaXR1bmcgdW5kIERhdGVubWFuYWdlbWVudCBnZWdlbsO8YmVyIGRlbiBSLUJhc2lzZnVua3Rpb25lbiBzdGFyayB2ZXJiZXNzZXJ0IHdlcmRlbi4gUGFrZXRlIGbDvHIgZWluemVsbmUgVGVpbGJlcmVpY2hlIGRpZSBlcnN0IHNww6R0ZXIgZWluZSBSb2xsZSBzcGllbGVuIHdlcmRlbiBzaW5kIHUuYS4gW3RvcGljbW9kZWxzXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPXRvcGljbW9kZWxzKSB1bmQgW3N0bV0oaHR0cHM6Ly93d3cuc3RydWN0dXJhbHRvcGljbW9kZWwuY29tLykgKFRoZW1lbm1vZGVsbGUpLCBbUlRleHRUb29sc10oaHR0cDovL3d3dy5ydGV4dHRvb2xzLmNvbS8pICjDvGJlcndhY2h0ZXMgbWFzY2hpbmVsbGVzIExlcm5lbiksIHVuZCBbc3BhY3lyXShodHRwczovL2dpdGh1Yi5jb20vcXVhbnRlZGEvc3BhY3lyKSAoUE9TLVRhZ2dpbmcgdW5kIE5hbWVkLUVudGl0eS1Fcmtlbm51bmcpLgoKRGllIEJhc2lzIGRlciBBbmFseXNlIGluIGRpZXNlbSBlcnN0ZW4gS2FwaXRlbCBzaW5kIGRpZSBiZWxpZWJ0ZW4gR2VzY2hpY2h0ZW4gdm9uIFNoZXJsb2NrIEhvbG1lcy4gRGFzIFNoZXJsb2NrIEhvbG1lcy1Lb3JwdXMgYmVzdGVodCBhdXMgenfDtmxmIEVyesOkaGx1bmdlbiwgZGllIGluIGRlbSAxODkyIGVyc2NoaWVuZW5lbSBCYW5kICpUaGUgQWRlbnR1cmVzIG9mIFNoZXJsb2NrIEhvbG1lcyogenVzYW1tZW5nZWZhc3N0IHNpbmQsIHVuZCBkaWUgbWFuIGdlbWVpbmZyZWkgdW50ZXIgYW5kZXJlbSBkdXJjaCBkYXMgW0ludGVybmV0IEFyY2hpdmVdKGh0dHBzOi8vYXJjaGl2ZS5vcmcvKSBoZXJ1bnRlcmxhZGVuIGthbm4uIERpZSBmw7xyIGRpZXNlIEVpbmbDvGhydW5nIHZlcndlbmRldGUgRmFzc3VuZyB3dXJkZSB6dW7DpGNoc3QgZGVtIEludGVybmV0IEFyY2hpdmUgZW50bm9tbWVuIHVuZCBkYW5uIGluIHp3w7ZsZiBFaW56ZWxkYXRlaWVuIGF1ZmdldGVpbHQuIE5hdMO8cmxpY2gga8O2bm5lbiBkaWUgdm9yZ2VzdGVsbHRlbiBNZXRob2RlbiBhdWYgZGllIGFuZGVyZW4gaGllciBiZWhhbmRlbHRlbiBLb3Jwb3JhIGFuZ2V3YW5kdCB3ZXJkZW4gLS0gZGFzIEJlaXNwaWVsIGRpZW50IG51ciBkYXp1LCBzaWNoIGxhbmdzYW0gYW4gcXVhbnRlZGEgdW5kIGRpZSBHcnVuZGxhZ2VuIGRlciBjb21wdXRlcmdlc3TDvHR6ZW4gSW5oYWx0c2FuYWx5c2UgenUgZ2V3w7ZobmVuLgoKU8OkbXRsaWNoZSBpbiBkaWVzZXIgRWluZsO8aHJ1bmcgdmVyd2VuZGV0ZXIgQ29kZWJlaXNwaWVsZSwgS29ycG9yYSB1bmQgTGV4aWthIGvDtm5uZW4gW2hpZXJdKGluaGFsdHNhbmFseXNlX21pdF9yLnppcCkgaGVydW50ZXJnZWxhZGVuIHdlcmRlbi4KCgojIyMjIEluc3RhbGxhdGlvbiB1bmQgTGFkZW4gZGVyIGJlbsO2dGlndGVuIFItQmlibGlvdGhla2VuCgpadW7DpGNoc3Qgd2VyZGVuIGRpZSBub3R3ZW5kaWdlbiBCaWJsaW90aGVrZW4gaW5zdGFsbGllcnQgKHNvZmVybiBub2NoIG5pY2h0IHZvcmhhbmRlbikgdW5kIGFuc2NobGllw59lbmQgZ2VsYWRlbi4gWnVkZW0gd2lyZCB2b3JiZXJlaXRlbmQgZGllIFRoZW1lLUVpbnN0ZWxsdW5nIGbDvHIgZGFzIFBha2V0IGdncGxvdCBnZXNldHp0IChkaWVzIHNvcmd0IGbDvHIgaMO8YnNjaGVyZSBQbG90cykuIERpZXNlbiBTY2hyaXR0IHdpZWRlcmhvbGVuIHdpciB6dSBCZWdpbm4gamVkZXMgS2FwaXRlbHMsIGluIGVpbmlnZW4gS2FwaXRlbG4gd2VyZGVuIG5vY2ggd2VpdGVyZW4gUGFrZXRlIGdlbGFuZGVuLiAKCmBgYHtyfQojIEluc3RhbGxhdGlvbiB1bmQgTGFkZW4gZGVyIEJpYmxpb3RoZWtlbgppZighcmVxdWlyZSgicXVhbnRlZGEiKSkgaW5zdGFsbC5wYWNrYWdlcygicXVhbnRlZGEiKQppZighcmVxdWlyZSgicmVhZHRleHQiKSkgaW5zdGFsbC5wYWNrYWdlcygicmVhZHRleHQiKQppZighcmVxdWlyZSgidGlkeXZlcnNlIikpIGluc3RhbGwucGFja2FnZXMoInRpZHl2ZXJzZSIpCmlmKCFyZXF1aXJlKCJSQ29sb3JCcmV3ZXIiKSkgaW5zdGFsbC5wYWNrYWdlcygiUkNvbG9yQnJld2VyIikKdGhlbWVfc2V0KHRoZW1lX2J3KCkpCmBgYAoKCiMjIyMgRWlubGVzZW4gZGVyIERhdGVuIHVuZCBBbmxlZ2VuIGVpbmVzIEtvcnB1cwoKTmFjaGRlbSBhbGxlIG5vdHdlbmRpZ2VuIFBha2V0ZSBnZWxhZGVuIHd1cmRlbiwga8O2bm5lbiB3aXIgbnVuIGRpZSBEYXRlbiBlaW5sZXNlbiB1bmQgZGFyYXVzIGVpbiBxdWFudGVkYS1Lb3JwdXMgZXJzdGVsbGVuLiBGw7xyIGRhcyBFaW5sZXNlbiBkZXIgUGxhaW50ZXh0LURhdGVpZW4gd2lyZCBkaWUgRnVua3Rpb24gW3JlYWR0ZXh0XShodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvcmVhZHRleHQpIGF1cyBkZW0gZ2xlaWNobmFtaWdlbiBQYWtldCB2ZXJ3ZW5kZXQsIGR1cmNoIGRpZSBzaWNoIGVpbmUgUmVpaGUgdm9uIERhdGVpZm9ybWF0ZW4gZXJmb2xncmVpY2ggZWlubGVzZW4gbGFzc2VuICh1LmEuIFRYVCwgUERGLCBXb3JkKS4gQW5zY2hsaWXDn2VuZCB3aXJkIGRpZSBFbmR1bmcgIi50eHQiIGF1cyBkZW0gRG9rdW1lbnRuYW1lbiBlbnRmZXJudC4gU2NobGllw59saWNoIHdpcmQgZGllIFZhcmlhYmxlIGtvcnB1cyBhdWZnZXJ1ZmVuLCB3YXMgZGllIHdpY2h0aWdlbiBFY2tkYXRlbiBEb2t1bWVudGFuemFobCB1bmQgRG9jdmFycyB6dXLDvGNrbGllZmVydC4gCgpgYGB7cn0KZGF0ZW4uc2hlcmxvY2sgPC0gcmVhZHRleHQoImRhdGVuL3NoZXJsb2NrL3JvbWFuZS9bMC05XSoudHh0IikgIyBEYXRlaW5hbWUgYmVnaW5udCBtaXQgWmFobCB1bmQgZW5kZXQgbWl0IC50eHQKZGF0ZW4uc2hlcmxvY2skZG9jX2lkIDwtIHN0cl9zdWIoZGF0ZW4uc2hlcmxvY2skZG9jX2lkLCBzdGFydCA9IDQsIGVuZCA9IC01KSAjIERhdGVpZW5kdW5nIHdlZ2xhc3NlbgoKa29ycHVzIDwtIGNvcnB1cyhkYXRlbi5zaGVybG9jaywgZG9jaWRfZmllbGQgPSAiZG9jX2lkIikgIyBLb3JwdXMgYW5sZWdlbgpkb2N2YXJzKGtvcnB1cywgIlRleHRudW1tZXIiKSA8LSBzcHJpbnRmKCIlMDJkIiwgMTpuZG9jKGtvcnB1cykpICMgVmFyaWFibGUgVGV4dG51bW1lciBnZW5lcmllcmVuCmtvcnB1cwpgYGAKCkluIGRlbiBmb2xnZW5kZW4gQWJzY2huaXR0ZW4gd2VyZGVuIGjDpHVmaWcgYmVyZWl0cyB2ZXJiZXJlaXRlIEtvcnBvcmEgZ2VsYWRlbiwgZC5oLiBkZXIgQmVmZWhsIGNvcnB1cygpIHdpcmQgaGllciBuaWNodCBtZWhyIGF1c2dlZsO8aHJ0LiBFciBpc3QgYWJlciBpbSBWb3JmZWxkIGF1c2dlZsO8aHJ0IHdvcmRlbiwgdW0gYXVzIFRleHRkYXRlaW4gYXVmIGRlciBGZXN0cGxhdHRlIG9kZXIgVHdpdHRlci1EYXRlbiBpbiBlaW5lbSBSLURhdGEgRnJhbWUgZWluIHF1YW50ZWRhLUtvcnB1cyB6dSBlcnN0ZWxsZW4uIAoKRGllIEZ1bmt0aW9uZW4gW25kb2MoKV0oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL25kb2MuaHRtbCksIFtudG9rZW4oKV0oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL250b2tlbi5odG1sKSwgW250eXBlKCldKGh0dHA6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9udG9rZW4uaHRtbCkgdW5kIFtuc2VudGVuY2UoKV0oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL25zZW50ZW5jZS5odG1sKSBnZWJlbiBkaWUgQW56YWhsIGRlciBEb2t1bWVudGUsIFRva2VucywgVHlwZXMgdW5kIFPDpHR6ZSBhdXMuIERpZXNlIFN0YXRpc3Rpa2VuIGvDtm5uZW4gYmVxdWVtIGdlbWVpbnNhbSBtaXQgTWV0YWRhdGVuIGF1ZiBEb2t1bWVudGViZW5lIGR1cmNoIGRpZSBGdW5rdGlvbiBbc3VtbWFyeSgpXShodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvcXVhbnRlZGEvdmVyc2lvbnMvMS4zLjAvdG9waWNzL3N1bW1hcnkuY29ycHVzKSB6dXNhbW1lbmdlZmFzc3Qgd2VyZGVuLiBCZWkgZGVuIG1laXN0ZW4gS29ycG9yYSwgZGllIGhpZXIgdmVyd2VuZGV0IHdlcmRlbiwgbGllZ3QgZWluIHNvbGNoZXIgRGF0YSBGcmFtZSBtaXQgU3RhdGlzdGlrZW4genUgamVkZW0gVGV4dCBiZXJlaXRzIGJlaS4gCgpgYGB7cn0Ka29ycHVzLnN0YXRzIDwtIHN1bW1hcnkoa29ycHVzLCBuID0gMTAwMDAwMCkKa29ycHVzLnN0YXRzJFRleHQgPC0gcmVvcmRlcihrb3JwdXMuc3RhdHMkVGV4dCwgMTpuZG9jKGtvcnB1cyksIG9yZGVyID0gVCkKa29ycHVzLnN0YXRzCmBgYAoKRGFzIEZ1bmt0aW9uc2FyZ3VtZW50IG4gPSAxMDAwMDAwIHdpcmQgaGllciBudXIgZGVzaGFsYiB2ZXJ3ZW5kZXQsIHdlaWwgZGllIEZ1bmt0aW9uIHN1bW1hcnkoKSBhbnNvbnN0ZW4gbnVyIG1heGltYWwgMTAwIFRleHRlIHp1c2FtbWVuZmFzc3QuIEluIGRpZXNlbSBGYWxsIHJlaWNodCBkYXMgendhciBhdXMsIGFiZXIgYmVpIGdyw7bDn2VyZW4gRGF0ZW5zw6R0emVuIGlzdCBkYXMgZWhlciB1bnByYWt0aXNjaC4gVGVjaG5pc2NoIGdlc2VoZW4gaGVpw590IGRpZXNlIEZ1bmt0aW9uIHN1bW1hcnkuY29ycHVzKCkgdW5kIGlzdCBlaW5lIGFuIEtvcnB1cy1PYmpla3RlIGFuZ2VwYXNzdGUgVmFyaWFudGUgZGVyIEJhc2lzZnVua3Rpb24gc3VtbWFyeSgpLCBkaWUgYXVjaCBzb25zdCBpbiBSIHZlcndlbmRldCB3aXJkLiBEZXIgQmVmZWhsIHJlb3JkZXIoKSB3aXJkIHZlcndlbmRldCB1bSBkaWUgVGV4dGUgbmFjaCBpaHJlciBOdW1tZXJpZXJ1bmcgenUgc29ydGllcmVuLCBzdGF0dCBhbHBoYWJldGlzY2ggbmFjaCBUaXRlbC4KCgojIyMjIEJhc2lzc3RhdGlzdGlrZW4genUgZWluZW0gS29ycHVzIGJlcmVjaG5lbgoKRGVyIEluaGFsdCBkZXIgVmFyaWFibGUga29ycHVzLnN0YXRzIGthbm4gbmF0w7xybGljaCBhdWNoIGdlcGxvdHRldCB3ZXJkZW4sIHVtIGVpbmVuIGFuc2NoYXVsaWNoZW4gRWluZHJ1Y2sgdm9uIGRlciBLb3JwdXNiZXNjaGFmZmVuaGVpdCB6dSBnZWJlbi4gRGllIGZvbGdlbmRlbiBaZWlsZW4gbGllZmVybiBkaWUgQW56YWhsIGRlciBUb2tlbnMgKGxhdWZlbmRlIFfDtnJ0ZXIpLCBkaWUgQW56YWhsIGRlciBUeXBlcyAoZWlubWFsaWdlIFfDtnJ0ZXIpLCB1bmQgU8OkdHplIHBybyBSb21hbiB6dXLDvGNrICh2Z2wuIGRhenUgW2RpZXNlIEVpbmbDvGhydW5nXShodHRwczovL3d3dy5idWJlbmhvZmVyLmNvbS9rb3JwdXNsaW5ndWlzdGlrL2t1cnMvaW5kZXgucGhwP2lkPWVyc3RlbGx1bmdfa29ycG9yYS5odG1sKSkuIFNjaGxpZcOfbGljaCB3aXJkIG5vY2ggZGFzIFZlcmjDpGx0bmlzIHZvbiBUeXBlbiB6dSBUb2tlbnMgKG9kZXIgZGllIHNvZy4gW1R5cC1Ub2tlbi1SZWxhdGlvbl0oaHR0cHM6Ly9kZS53aWtpcGVkaWEub3JnL3dpa2kvVHlwZS1Ub2tlbi1SZWxhdGlvbikpIGdlcGxvdHRldC4gCgpHcnVuZGxhZ2Ugc29sY2hlciBQbG90cyBzaW5kIHByYWt0aXNjaCBpbW1lciBEYXRhIEZyYW1lLU9iamVrdGUgKGFsc28gVGFiZWxsZW4pLCBkaWUgSW5mb3JtYXRpb25lbiDDvGJlciBLb3Jwb3JhLCBUZXh0ZSwgV8O2cnRlciwgVGhlbWVuIHVzdy4gZW50aGFsdGVuLCB3ZWxjaGUgc2ljaCB2aXN1ZWxsIGRhcnN0ZWxsZW4gbGFzc2VuLiBJbSBSZXN0IGRpZXNlciBFaW5mw7xocnVuZyBnZWhlIGljaCBuaWNodCBpbSBEZXRhaWwgZGFyYXVmIGVpbiwgd2llIGRpZSBqZXdlaWxpZ2VuIFBsb3RzIGdlbmF1IGtvbnN0cnVpZXJ0IHdlcmRlbiwgYWxsZXJkaW5ncyBsYXNzZW4gc2ljaCBkaWUgbWVpc3RlbiBEYXRlbiBhdWNoIChldHdhcyB3ZW5pZ2VyIGFuc3ByZWNoZW5kKSBtaXQgZGVyIFItaW50ZXJuZW4gRnVua3Rpb24gW3Bsb3QoKV0oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL2dyYXBoaWNzL3ZlcnNpb25zLzMuNS4wL3RvcGljcy9wbG90KSBkYXJzdGVsbGVuLiBFaW5lIGhpbGZyZWljaGUgZGV1dHNjaHNwcmFjaGlnZSBFaW5mw7xocnVuZyBpbiBkYXMgUGxvdHRlbiBtaXQgZ2dwbG90MiBmaW5kZXQgc2ljaCBbaGllcl0oaHR0cDovL21kLnBzeWNoLmJpby51bmktZ29ldHRpbmdlbi5kZS9tdi91bml0L2dncGxvdDIvZ2dwbG90Mi5odG1sKS4gVmllbGUgZGVyIGhpZXIgdm9yZ2VzdGVsbHRlbiBQbG90cyBzdGFtbWVuIHp1ZGVtIGRpcmVrdCBhdXMgcXVhbnRlZGEgKGJlZ2lubmVuZCBtaXQgdGV4dHBsb3RfKS4KCmBgYHtyfQpnZ3Bsb3Qoa29ycHVzLnN0YXRzLCBhZXMoVGV4dCwgVG9rZW5zLCBncm91cCA9IDEpKSArIGdlb21fbGluZSgpICsgZ2VvbV9wb2ludCgpICsgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgdmp1c3QgPSAxLCBoanVzdCA9IDEpKSArIGdndGl0bGUoIlRva2VucyBwcm8gUm9tYW4iKQpnZ3Bsb3Qoa29ycHVzLnN0YXRzLCBhZXMoVGV4dCwgVHlwZXMsIGdyb3VwID0gMSkpICsgZ2VvbV9saW5lKCkgKyBnZW9tX3BvaW50KCkgKyB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCB2anVzdCA9IDEsIGhqdXN0ID0gMSkpICsgZ2d0aXRsZSgiVHlwZXMgcHJvIFJvbWFuIikKZ2dwbG90KGtvcnB1cy5zdGF0cywgYWVzKFRleHQsIFNlbnRlbmNlcywgZ3JvdXAgPSAxKSkgKyBnZW9tX2xpbmUoKSArIGdlb21fcG9pbnQoKSArIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIHZqdXN0ID0gMSwgaGp1c3QgPSAxKSkgKyBnZ3RpdGxlKCJTw6R0emUgcHJvIFJvbWFuIikKZ2dwbG90KGtvcnB1cy5zdGF0cywgYWVzKFRva2VucywgVHlwZXMsIGdyb3VwID0gMSwgbGFiZWwgPSBUZXh0bnVtbWVyKSkgKyBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBzZSA9IEZBTFNFKSArIGdlb21fdGV4dChjaGVja19vdmVybGFwID0gVCkgKyBnZ3RpdGxlKCJUeXAtVG9rZW4tUmVsYXRpb24gcHJvIFJvbWFuIikKYGBgCgpEaWVzZSBHcmFmaWtlbiBzaW5kIHp1bsOkY2hzdCBlaW5tYWwgbmljaHQgdW13ZXJmZW5kIGluZm9ybWF0aXYuIFNpZSBiZWxlZ2VuIGxlZGlnbGljaCwgZGFzcyBkaWUgRXJ6w6RobHVuZ2VuIOKAmEEgQ2FzZSBvZiBJZGVudGl0eeKAmSB1bmQgKGluIGdlcmluZ2VyZW0gTWHDn2UpIOKAmFRoZSBGaXZlIE9yYW5nZW4gUGlwc+KAmSBkZXV0bGljaCBrw7xyemVyIHNpbmQgYWxzIGRpZSBhbmRlcmVuIFRleHRlLCB3YXMgc2ljaCBhdWYgYWxsZW4gZHJlaSBFYmVuZW4gKFRva2VucywgVHlwZXMsIFPDpHR6ZSkgbmllZGVyc2NobMOkZ3QuIEV0d2FzIGludGVyZXNzYW50ZXIgd2lyZCBlcyBhbGxlcmRpbmdzIGJlaSBkZXIgVHlwLVRva2VuLVJlbGF0aW9uOiB3w6RocmVuZCBkcmVpIFJvbWFuZSAobWl0IGRlbiBOdW1tZXJuIDMsIDExIHVuZCAxMikgamV3ZWlscyBlaW5lbiBlaGVyIHVudGVyZHVyY2hzY2huaXR0bGljaGVuIFRUUiBhdWZ3ZWlzZW4sIGxpZWdlbiB3ZWl0ZXJlIHZpZXIgb2JlcmhhbGIgZGVyIGxpbmVhcmVuIFJlbGF0aW9uICgxLCA1LCA2ICB1bmQgOCksIHfDpGhyZW5kIGRpZSB2ZXJibGVpYmVuZGVuIHNlY2hzIHppZW1saWNoIGdlbmF1IGRlbSBEdXJjaHNjaG5pdHQgZW50c3ByZWNoZW4uIMOcYmVyIGRlbiBUVFIgbGFzc2VuIHNpY2ggUsO8Y2tzY2hsw7xzc2VuIMO8YmVyIGRpZSBJbmZvcm1hdGlvbnNkaWNodGUgemllaGVuIC0tIGRhenUgc3DDpHRlciBub2NoIG1laHIuIAoKCiMjIyMgTWl0IEtvcnBvcmEgYXJiZWl0ZW4KCktvcnBvcmEgbGFzc2VuIHNpY2ggaW4gcXVhbnRlZGEgc2VociBsZWljaHQgc2FtcGxlbiwgdW1mb3JtZW4gdW5kIG1pdCB6dXPDpHRsaWNoZW4gTWV0YWRhdGVuIHZlcnNlaGVuLiBNZXRhZGF0ZW4ga8O2bm5lbiB3aWVkZXJ1bSBnZW51dHp0IHdlcmRlbiwgdW0gZGFzIEtvcnB1cyBuYWNoIGJlc3RpbW10ZW4gS3JpdGVyaWVuIHp1IGZpbHRlcm4uIAoKYGBge3J9CnN0cl9zdWIoa29ycHVzWzFdLCBzdGFydCA9IDEsIGVuZCA9IDEwMDApICMgQW5mYW5nIGRlcyBlcnN0ZW4gUm9tYW5zIHdpZWRlcmdlYmVuCmBgYAoKSmVkZXIgVGV4dCBsw6Rzc3Qgc2ljaCBhbHNvIGFuaGFuZCBzZWluZXIgSW5kaXppZXJ1bmcgKGV0d2Ega29ycHVzWzFdIGbDvHIgZGVuIGVyc3RlbiBUZXh0KSBhdWZydWZlbi4gCgpNaXR0ZWxzIFtjb3JwdXNfcmVzaGFwZSgpXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3Jlc2hhcGUuaHRtbCkgbMOkc3N0IHNpY2ggZWluIEtvcnB1cyBzbyB1bWZvcm1lbiwgZGFzcyBqZWRlciBTYXR6IGVpbiBlaWdlbmVzIERva3VtZW50IGVyZ2lidC4gQWx0ZXJuYXRpdmUgQXJndW1lbnRlIHNpbmQg4oCYcGFyYWdyYXBoc+KAmSB1bmQg4oCYZG9jdW1lbnRz4oCZIChzbyBsw6Rzc3Qgc2ljaCBlaW4gU2F0ei1Lb3JwdXMgd2llZGVyIGluIHNlaW5lbiBBbmZhbmdzenVzdGFuZCB6dXLDvGNrdmVyc2V0emVuKS4gRGllIEVyc3RlbGx1bmcgdm9uIFNhdHotS29ycG9yYSBpc3QgZsO8ciBkaWUgU2VudGltZW50YW5hbHlzZSB1bmQgZGFzIMO8YmVyd2FjaHRlIG1hc2NoaW5lbGxlIExlcm5lbiB2b24gSW50ZXJlc3NlLiAKCkRpZSBCZXNjaHJpZnR1bmcgZGVzIEJlaXNwaWVscyBiZXN0ZWh0IGhpZXIgYXVzIGRlciBWYXJpYWJsZSBkb2NuYW1lIHVuZCBlaW5lciBhbmdlaMOkbmd0ZW4gWmFobCAoZWluZSAxIGbDvHIgZGVuIGVyc3RlbiBTYXR6KS4gCgpgYGB7cn0Ka29ycHVzLnNhZXR6ZSA8LSBjb3JwdXNfcmVzaGFwZShrb3JwdXMsIHRvID0gInNlbnRlbmNlcyIpCmtvcnB1cy5zYWV0emVbMV0KYGBgCgpNaXQgW2NvcnB1c19zYW1wbGUoKV0oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2NvcnB1c19zYW1wbGUuaHRtbCkga2FubiB3ZWl0ZXJoaW4gZWluIHp1ZsOkbGxpZ2VzIFNhbXBsZSBhdXMgZGVtIFNhdHotS29ycHVzIGdlem9nZW4gd2VyZGVuLiAKCmBgYHtyfQp6dWZhbGxzc2F0eiA8LSBjb3JwdXNfc2FtcGxlKGtvcnB1cy5zYWV0emUsIHNpemUgPSAxKQp6dWZhbGxzc2F0elsxXQpgYGAKCkFuaGFuZCB2b24gW2NvcnB1c19zdWJzZXRdKGh0dHA6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9jb3JwdXNfc3Vic2V0Lmh0bWwpIGthbm4gZWluIEtvcnB1cyBuYWNoIE1ldGFkYXRlbiBnZWZpbHRlcnQgd2VyZGVuLiBIaWVyIGdlc2NoaWVodCBkaWVzIG1pdHRlbHMgZGVyIG5ldSBlcnN0ZWxsdGVuIGJpbsOkcmVuIFZhcmlhYmxlIExhbmdlclNhdHosIGRpZSBkYW5uIFRSVUUgaXN0LCB3ZW5uIGVpbiBTYXR6ID49IDI1IFRva2VucyBlbnRow6RsdCkuIFNvIGzDpHNzdCBzaWNoIGVpbiBUZWlsa29ycHVzIHp1IGJpbGRlbiwgaW4gZGVtIG51ciBsw6RuZ2VyZSBTw6R0emUgZW50aGFsdGVuIHNpbmQuIEtvcnBvcmEgbGFzc2VuIHNpY2ggbWl0IFtjb3JwdXNfc2VnbWVudCgpXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3NlZ21lbnQuaHRtbCkgYXVjaCBuYWNoIGJlc3RpbW10ZW4gS3JpdGVyaWVuIGF1ZnNwYWx0ZW4KCmBgYHtyfQpkb2N2YXJzKGtvcnB1cy5zYWV0emUsICJaZWljaGVuYW56YWhsIikgPC0gbnRva2VuKGtvcnB1cy5zYWV0emUpCmRvY3ZhcnMoa29ycHVzLnNhZXR6ZSwgIkxhbmdlclNhdHoiKSA8LSBudG9rZW4oa29ycHVzLnNhZXR6ZSk+PTI1CmtvcnB1cy5zYWV0emVfbGFuZyA8LSBjb3JwdXNfc3Vic2V0KGtvcnB1cy5zYWV0emUsIExhbmdlclNhdHogPT0gVFJVRSkKa29ycHVzLnNhZXR6ZV9sYW5nWzE6M10KYGBgCgoKIyMjIyBUb2tlbmlzaWVydW5nIAoKVW50ZXIgVG9rZW5zaWllcnVuZyB2ZXJzdGVodCBtYW4gZGllIEF1ZnNwYWx0dW5nIGVpbmVzIFRleHRlcyBpbiBsYXVmZW5kZSBXw7ZydGVyIG9kZXIgc29nLiBOLUdyYW1tZSwgYWxzbyBTZXF1ZW56ZW4gbWVocmVyZXIgV8O2cnRlciBpbiBGb2xnZS4gRGllIEZ1bmt0aW9uIFt0b2tlbnMoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS90b2tlbnMuaHRtbCkgcmVhbGlzaWVydCBkaWUgVG9rZW5pc2llcnVuZyBlaW5lcyBLb3JwdXMgaW4gcXVhbnRlZGEuCgpgYGB7cn0KbWVpbmUudG9rZW5zIDwtIHRva2Vucyhrb3JwdXMpCmhlYWQobWVpbmUudG9rZW5zJGBBIFNjYW5kYWwgaW4gQm9oZW1pYWApCmBgYAoKTWl0dGVscyBkZXIgRnVua3Rpb24gdG9rZW5zIGzDpHNzdCBzaWNoIGRlciBUZXh0IMO8YmVyIGRhcyBBcmd1bWVudCBuZ3JhbXMgYXVjaCBnbGVpY2ggaW4gTi1HcmFtbWUgKE1laHJ3b3J0c2VxdWVuemVuKSBhdWZzcGFsdGVuLiBJbSBmb2xnZW5kZW4gQmVpc3BpZWwgd2VyZGVuIGVyc3QgQmlncmFtbWUgIHZvbiBBbmZhbmcgZGVzIGVyc3RlbiBUZXh0ZXMgYW5nZXplaWd0LCB1bmQgZGFubiBhbGxlIFNlcXVlbnplbiB2b24gZWluZW0sIHp3ZWkgb2RlciBkcmVpIEJlZ3JpZmZlbiBleHRyYWhpZXJ0IChkdXJjaCBkaWUgQW53ZW5kdW5nIHZvbiBbaGVhZCgpXShodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvdXRpbHMvdmVyc2lvbnMvMy41LjEvdG9waWNzL2hlYWQpIHNlaGVuIHdpciBudXIgVHJpZ3JhbW1lLCBlcyBzaW5kIGFiZXIgYXVjaCBrw7xyemVyZSBTZXF1ZW56ZW4gdm9yaGFuZGVuKS4gCgpgYGB7cn0KbWVpbmUudG9rZW5zIDwtIHRva2Vucyhrb3JwdXMsIG5ncmFtcyA9IDIpCmhlYWQobWVpbmUudG9rZW5zJGBBIFNjYW5kYWwgaW4gQm9oZW1pYWApCgptZWluZS50b2tlbnMgPC0gdG9rZW5zKGtvcnB1cywgbmdyYW1zID0gMTozKQpoZWFkKG1laW5lLnRva2VucyRgQSBTY2FuZGFsIGluIEJvaGVtaWFgKQpgYGAKCkhpbGZyZWljaCBpc3QgYXVjaCBkaWUgTcO2Z2xpY2hrZWl0LCBiZWkgZGVyIFRva2VuaXNpZXJ1bmcgYmVzdGltbXRlIEJlZ3JpZmZlIHp1IGVudGZlcm5lbiBvZGVyIHp1csO8Y2t6dWJlaGFsdGVuLiAKCmBgYHtyfQptZWluZS50b2tlbnMgPC0gdG9rZW5zKGtvcnB1cykKYmVncmlmZmUuYmVoYWx0ZW4gPC0gdG9rZW5zX3NlbGVjdChtZWluZS50b2tlbnMsIGMoImhvbG1lcyIsICJ3YXRzb24iKSkgIyBQbGF0emhhbHRlciBtaXQgcGFkZGluZyA9IFRSVUUKaGVhZChiZWdyaWZmZS5iZWhhbHRlbiRgQSBTY2FuZGFsIGluIEJvaGVtaWFgKQpiZWdyaWZmZS5lbnRmZXJuZW4gPC0gdG9rZW5zX3JlbW92ZShtZWluZS50b2tlbnMsIGMoIlNoZXJsb2NrIiwgImluIiwgImlzIiwgInRoZSIpKQpoZWFkKGJlZ3JpZmZlLmVudGZlcm5lbiRgQSBTY2FuZGFsIGluIEJvaGVtaWFgKQpgYGAKCkRpZSBGdW5rdGlvbiB0b2tlbnMoKSBha3plcHRpZXJ0IGVpbmUgUmVpaGUgdm9uIEFyZ3VtZW50ZW4sIG1pdCBkZW5lbiBnYW56ZSBLbGFzc2VuIHZvbiBaZWljaGVua2V0dGVuIChaYWhsZW4sIEludGVycHVua3Rpb24sIFN5bWJvbGUgdXN3LikgYXVzZ2VzY2hsb3NzZW4gb2RlciB6dXLDvGNrYmVoYWx0ZW4gd2VyZGVuIGvDtm5uZW4uIEZvbGdlbmQgd2VyZGVuIHp1bsOkY2hzdCBaYWhsZW4sIEludGVycHVua3Rpb24gdW5kIFN5bWJvbGUgZW50ZmVybnQsIGRhbm4gbWl0dGVscyBbdG9rZW5zX3RvbG93ZXIoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS90b2tlbnNfdG9sb3dlci5odG1sKSBhbGxlIFfDtnJ0ZXIgaW4gS2xlaW5zY2hyZWlidW5nIHVtZ2V3YW5kZWx0IHVuZCBkYW5uIGRhbm4gbm9jaCBkaWUgV8O2cnRlciAnc2hlcmxvY2snIHVuZCAnaG9sbWVzJywgc293ZWkgZWluZSBSZWloZSBlbmdsaXNjaGVyIFtTdG9wcHfDtnJ0ZXJdKGh0dHBzOi8vZGUud2lraXBlZGlhLm9yZy93aWtpL1N0b3Bwd29ydCkgZW50ZmVybnQuIAoKYGBge3J9Cm1laW5lLnRva2VucyA8LSB0b2tlbnMoa29ycHVzLCByZW1vdmVfbnVtYmVycyA9IFRSVUUsIHJlbW92ZV9wdW5jdCA9IFRSVUUsIHJlbW92ZV9zeW1ib2xzID0gVFJVRSkKbWVpbmUudG9rZW5zIDwtIHRva2Vuc190b2xvd2VyKG1laW5lLnRva2VucykKbWVpbmUudG9rZW5zIDwtIHRva2Vuc19yZW1vdmUobWVpbmUudG9rZW5zLCBjKHN0b3B3b3JkcygiZW5nbGlzaCIpLCAic2hlcmxvY2siLCAiaG9sbWVzIikpCmhlYWQobWVpbmUudG9rZW5zJGBBIFNjYW5kYWwgaW4gQm9oZW1pYWApCmBgYAoKRGFzIFJlc3VsdGF0IGlzdCBkZXIgQXJ0IHZvbiBEYXRlbiBtaXQgZGVuZW4gbWFuIGJlaSBWZXJmYWhyZW4gd2llIGRlciBBbndlbmR1bmcgdm9uIExleGlrYSAoS2FwaXRlbCAyKSwgZGVyIEJlcmVjaG51bmcgdm9uIFRoZW1lbm1vZGVsbGVuIChLYXBpdGVsIDMpIHVuZCBkZW0gw7xiZXJ3YWNodGVuIG1hc2NoaW5lbGxlIExlcm5lbiAoS2FwaXRlbCA0KSBhcmJlaXRldCBzZWhyIMOkaG5saWNoLiBEdXJjaCBkaWUgU3RvcHB3b3J0ZW50ZmVybnVuZyB1bmQgYW5kZXJlIFNjaHJpdHRlIGdlaGVuIHN5bnRha3Rpc2NoZSBJbmZvcm1hdGlvbmVuIHZlcmxvcmVuLCBkLmguIG1hbiBrYW5uIG5pY2h0IG1laHIgbmFjaHZvbGx6aWVoZW4sIHdlciB3YXMgbWl0IHdlbSB0dXQsIG9kZXIgd2llIGRlciBUZXh0IGluc2dlc2FtdCBhcmd1bWVudGF0aXYgb2RlciBlcnrDpGhsZXJpc2NoIGF1ZmdlYmF1dCBpc3QuIERpZXNlIEluZm9ybWF0aW9uZW4gc2luZCBhbGxlcmRpbmdzIGltICdbQmFnLW9mLVdvcmRzLUFuc2F0el0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvQmFnLW9mLXdvcmRzX21vZGVsKScsIGRlciBpbiBkZXIgYXV0b21hdGlzaWVydGVuIEluaGFsdHNhbmFseXNlIG5haGV6dSBpbW1lciB2ZXJ3ZW5kZXQgd2lyZCwgbmljaHQgdW5iZWRpbmd0IHJlbGV2YW50LgoKCiMjIyMgRG9rdW1lbnQtRmVhdHVyZS1NYXRyaXplbiAoREZNcykgZXJzdGVsbGVuCgpXaXIga29tbWVuIG51biB6dSBlaW5lciB6ZW50cmFsZW4gRGF0ZW5zdHJ1a3R1ciB2b24gcXVhbnRlZGEsIGRpZSBpbSBHZWdlbnNhdHogenUgZGVuIHp1dm9yIHZvcmdlc3RlbGx0ZW4gRWluaGVpdGVuIHByYWt0aXNjaCBpbiBqZWRlbSBQcm9qZWt0IHZvcmtvbW10OiBkaWUgRG9jdW1lbnQgRmVhdHVyZS1NYXRyaXplIChERk0pLiDDnGJsaWNoZXJ3ZWlzZSB3aXJkIGRpcmVrdCBuYWNoZGVtIGVpbiBLb3JwdXMgYW5nZWxlZ3Qgd3VyZGUgYmVyZWNobmV0IEVpbmUgREZNIGlzdCBlaW5lIFRhYmVsbGUsIGRlcmVuIFplaWxlbiBkaWVlIFRleHRlIHVuZCBkZXJlbiBTcGFsdGVuIGRpZSBXb3J0ZnJlcXVlbnplbiBlbmhhbHRlbi4gRGFiZWkgZ2VoZW4gSW5mb3JtYXRpb25lbiBkYXLDvGJlciwgd28gaW4gZWluZW0gVGV4dCBlaW4gV29ydCB2b3Jrb21tdCB2ZXJsb3JlbiAobWFuIHNwcmljaHQgYXVjaCB2b20gJ1tCYWctb2YtV29yZHMtQW5zYXR6XShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9CYWctb2Ytd29yZHNfbW9kZWwpJykuIEltbWVyIGRhbm4sIHdlbm4gd2lyIHVucyBmw7xyIGRpZSBCZXppZWh1bmcgdm9uIFfDtnJ0ZXJuIHp1IFRleHRlbiAodW5kIHVtZ2VrZWhydCkgaW50ZXJlc3NpZXJlbiwgYmVyZWNobmVuIHdpciBlaW5lIERGTS4KCmBgYHtyfQptZWluZS5kZm0gPC0gZGZtKGtvcnB1cywgcmVtb3ZlX251bWJlcnMgPSBUUlVFLCByZW1vdmVfcHVuY3QgPSBUUlVFLCByZW1vdmVfc3ltYm9scyA9IFRSVUUsIHJlbW92ZSA9IHN0b3B3b3JkcygiZW5nbGlzaCIpKQptZWluZS5kZm0KYGBgCgpXaWNodGlnOiBIaWVyIHdpcmQgaW1wbGl6aXQgZGVyIHVucyBzY2hvbiB2ZXJ0cmF1dGUgQmVmZWhsIFt0b2tlbnMoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS90b2tlbnMuaHRtbCkgYW5nZXdhbmR0LCB1bSBiZXN0aW1tdGUgRmVhdHVyZXMgenUgZW50ZmVybmVuLiBWaWVsZXMgZnVua3Rpb25pZXJ0IGJlaSBERk1zIGFuYWxvZyB6dXIgRXJzdGVsbHVuZyBlaW5lcyBLb3JwdXMuIFNvIHrDpGhsZW4gZGllIEZ1bmt0aW9uZW4gW25kb2MoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9uZG9jLmh0bWwpIHVuZCBbbmZlYXQoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9uZG9jLmh0bWwpIERva3VtZW50ZSB1bmQgRmVhdHVyZXMgKFfDtnJ0ZXIpLgoKYGBge3J9Cm5kb2MobWVpbmUuZGZtKQpuZmVhdChtZWluZS5kZm0pCmBgYAoKTWl0dGVscyBkZXIgRnVua3Rpb25lbiBbZG9jbmFtZXMoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9kb2NuYW1lcy5odG1sKSB1bmQgCltmZWF0bmFtZXMoKV0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9mZWF0bmFtZXMuaHRtbCkgbGFzc2VuIHNpY2ggZGllIE5hbWVuIGRlciBEb2t1bWVudGUgdW5kIEZlYXR1cmVzIGF1c2dlYmVuLgoKYGBge3J9CmhlYWQoZG9jbmFtZXMobWVpbmUuZGZtKSkgIyBJbiBkZXIgREZNIGVudGhhbHRlbmUgRG9rdW1lbnRlIApoZWFkKGZlYXRuYW1lcyhtZWluZS5kZm0pLCA1MCkgIyBGZWF0dXJlcyBpbiBjaHJvbm9sb2dpc2NoZXIgUmVpaGVuZm9sZ2UKYGBgCgpEaWUgdGFiZWxsYXJpc2NoZSBBbnNpY2h0IGlsbHVzdHJpZXJ0IGRlbiBJbmhhbHQgZGVyIERGTSBhbHMgVGV4dC1Xb3J0LU1hdHJpeCBhbSBiZXN0ZW4uIERpZSBzcGFyc2l0eSAoIlNww6RybGljaGtlaXQiKSBlaW5lciBERk0gYmVzY2hyZWlidCBkYWJlaSBkZW4gQW50ZWlsIGRlciBsZWVyZW4gWmVsbGVuLCBhbHNvIFfDtnJ0ZXIsIGRpZSBudXIgaW4gc2VociB3ZW5pZ2VuIFRleHRlbiB2b3Jrb21tZW4uIFdpZSBzaWNoIGxlaWNodCBhYmxlaXRlbiBsw6Rzc3QsIHdlcmRlbiBERk1zIHNlaHIgc2NobmVsbCBzZWhyIGdyb8OfLiBadW0gR2zDvGNrIG1hY2h0IHNpY2ggcXVhbnRlZGEgZWluZSBSZWloZSB2b24gZsO8ciBkZW4gTnV0emVyIHVuc2ljaHRiYXJlbiBGdW5rdGlvbmVuIGF1cyBhbmRlcmVuIFBha2V0ZW4genVudXR6ZSwgdW0gZGllc2VtIFByb2JsZW0genUgYmVnZWduZW4uIAoKYGBge3J9CmhlYWQobWVpbmUuZGZtLCBuID0gMTIsIG5mID0gMTApICMgRmVhdHVyZXMgdW5kIFRleHRlIGFscyBNYXRyaXggaW4gY2hyb25vbG9naXNjaGVyIFJlaWhlbmZvbGdlCmBgYAoKR2xlaWNoIGFuIGRlbiBlcnN0ZW4gQmxpY2sgZsOkbGx0IGF1ZiwgZGFzIGRpZSBXw7ZydGVyICdzaGVybG9jaycgdW5kICdob2xtZXMnIGluIGFsbGVuIFJvbWFuZW4gdm9ya29tbWVuLCBhbHNvIHNlaHIgd2VuaWcgZGlzdGlua3RpdiBzaW5kLCB3ZXNoYWxiIHdpciBzaWUgdW50ZXIgVW1zdMOkbmRlbiB6dSBkZW4gU3RvcHB3w7ZydGVybiBmw7xyIGRpZXNlcyBLb3JwdXMgaGluenVmw7xnZW4gc29sbHRlbi4gCgpEaWUgRnVua3Rpb24gW3RvcGZlYXR1cmVzKCldKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvdG9wZmVhdHVyZXMuaHRtbCkgesOkaGx0IEZlYXR1cmVzIGluIGRlciBnZXNhbXRlbiBERk0gYXVzLiBEaWUgRnVua3Rpb24gW3RleHRzdGF0X2ZyZXF1ZW5jeSgpXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL3RleHRzdGF0X2ZyZXF1ZW5jeS5odG1sKSBsaWVmZXJ0IHp1c8OkdHpsaWNoIG5vY2ggZGVuIFJhbmcgKHJhbmspLCBkaWUgQW56YWhsIGRlciBEb2t1bWVudGUsIGluIGRlbmVuIGRhcyBGZWF0dXJlIHZvcmtvbW10IChkb2NmcmVxKSBzb3dpZSBNZXRhZGF0ZW4sIG5hY2ggZGVuZW4gYmVpIGRlciBaw6RobHVuZyBnZWZpbHRlcnQgd3VyZGUuCgpgYGB7cn0KdG9wZmVhdHVyZXMobWVpbmUuZGZtKSAjIEZlYXR1cmVzIG5hY2ggRnJlcXVlbnoKd29ydGhhZXVmaWdrZWl0ZW4gPC0gdGV4dHN0YXRfZnJlcXVlbmN5KG1laW5lLmRmbSkgIyBXb3J0aMOkdWZpZ2tlaXRlbgpoZWFkKHdvcnRoYWV1Zmlna2VpdGVuKQpgYGAKCgojIyMjIE1pdCBERk1zIGFyYmVpdGVuCgpERk1zIGxhc3NlbiBzaWNoIG1pdCBbZGZtX3NvcnRdKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvZGZtX3NvcnQuaHRtbCkgbGVpY2h0IG5hY2ggRG9rdW1lbnQtIHVuZCBGZWF0dXJlLUZyZXF1ZW56ZW4gc29ydGllcmVuLgoKYGBge3J9CmhlYWQoZGZtX3NvcnQobWVpbmUuZGZtLCBkZWNyZWFzaW5nID0gVFJVRSwgbWFyZ2luID0gImJvdGgiKSwgbiA9IDEyLCBuZiA9IDEwKSAKYGBgCgpXZWl0ZXJoaW4gbGFzc2VuIHNpY2ggYmVzdGltbXRlIEZlYXR1cmVzIGVpbmVyIERGTSBnZXppZWx0IG1pdHRlbHMgW2RmbV9zZWxlY3RdKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvZGZtX3NlbGVjdC5odG1sKSBhdXN3w6RobGVuLgoKYGBge3J9CmRmbV9zZWxlY3QobWVpbmUuZGZtLCBwYXR0ZXJuID0gImxvdioiKQpgYGAKCkRpZSBGdW5rdGlvbiBbZGZtX3dvcmRzdGVtKCldKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvZGZtX3dvcmRzdGVtLmh0bWwpIHJlZHV6aWVydCBXw7ZydGVyIGF1ZiBpaHJlIFN0YW1tZm9ybS4gRGllc2UgRnVua3Rpb24gZXhpc3RpZXJ0IGluIHF1YW50ZWRhIGRlcnplaXQgbnVyIGbDvHIgRW5nbGlzY2ggdW5kIGlzdCBhdWNoIGRvcnQgbnVyIGJlZ3Jlbnp0IHp1dmVybMOkc3NpZywgd2FzIGRpZSBmb2xnZW5kZSBBdXNnYWJlIGd1dCBpbGx1c3RyaWVydCAoJ2hvbG0nIGlzdCBrZWluIFdvcnRzdGFtbSkuIAoKYGBge3J9Cm1laW5lLmRmbS5zdGVtbWVkIDwtIGRmbV93b3Jkc3RlbShtZWluZS5kZm0pCnRvcGZlYXR1cmVzKG1laW5lLmRmbS5zdGVtbWVkKQpgYGAKCkViZW5zbyB3aWUgYmVpIFdvcnRmcmVxdWVuemVuIGluIEtvcnBvcmEgaXN0IGRpZSBHZXdpY2h0dW5nIGVpbmVyIERGTSBuYWNoIHJlbGF0aXZlbiBXb3J0ZnJlcXVlbnplbiB1bmQgVmVyZmFocmVuIHdpZSBbVEYtSURGXShodHRwczovL2RlLndpa2lwZWRpYS5vcmcvd2lraS9UZi1pZGYtTWElQzMlOUYpIG9mdG1hbHMgc2lubnZvbGwuIERpZSBHZXdpY2h0dW5nIGVpbmVyIERGTSBmdW5rdGlvbmllcnQgaW1tZXIgYXVmZ3J1bmQgZGVyIFdvcnQtVGV4dC1SZWxhdGlvbiwgd2VzaGFsYiB0b3BmZWF0dXJlcygpIGluIEtvbWJpbmF0aW9uIG1pdCBbZGZtX3dlaWdodCgpXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RmbV93ZWlnaHQuaHRtbCkgbWVya3fDvHJkaWdlIFJlc3VsdGF0ZSBwcm9kdXppZXJ0LiBSZWxhdGl2ZSBGcmVxdWVuemVuIHVuZCBURi1JREYgc2luZCBudXIga29udHJhc3RpdiBpbm5lcmhhbGIgZGVyIFRleHQgaW4gZWluZW0gS29ycHVzIHNpbm52b2xsIChoaWVyIGbDvHIgJ0EgU2NhbmRhbCBpbiBCb2hlbWlhJyksIGRhIGbDvHIgZGFzIGdlc2FtdGUgS29ycHVzIHJlbGF0aXZlIEZyZXF1ZW56ID09IGFic29sdXRlIEZyZXF1ZW56CgpgYGB7cn0KbWVpbmUuZGZtLnByb3BvcnRpb25hbCA8LSBkZm1fd2VpZ2h0KG1laW5lLmRmbSwgc2NoZW1lID0gInByb3AiKQp0b3BmZWF0dXJlcyhtZWluZS5kZm0pICMgYWJzb2x1dGUgRnJlcXVlbnplbiBmw7xyIGRhcyBnZXNhbXRlIEtvcnB1cwp0b3BmZWF0dXJlcyhtZWluZS5kZm0ucHJvcG9ydGlvbmFsKSAjIC4uLmVyZ2lidCB3ZW5pZyBTaW5uCnRvcGZlYXR1cmVzKG1laW5lLmRmbS5wcm9wb3J0aW9uYWxbMSxdKSAjIC4uLmVyZ2lidCBtZWhyIFNpbm4KYGBgCgpJbSB6d2VpdGVuIEJlaXNwaWVsIHNlaGVuIHdpciBldHdhLCBkYXNzICdBIFNjYW5kYWwgaW4gQm9oZW1pYScgZWluZW4gbGVpY2h0IGjDtmhlcmVuIEFudGVpbCB2b24gTmVubnVuZ2VuIGRlciBXb3J0ZXMgJ2hvbG1lcycgaGF0LCBhbHMgZGllcyBpbSBHZXNhbXRrb3JwdXMgZGVyIEZhbGwgaXN0LiBEYXp1IHNww6R0ZXIgbm9jaCBldHdhcyBtZWhyLiAgCgpEaWUgR2V3aWNodHVuZ3NhbnPDpHR6ZSBQcm9wbWF4IHVuZCBURi1JREYgbGllZmVybiByZWxldmFudGUgV29ydG1ldHJpa2VuLCB6dW0gQmVpc3BpZWwgZsO8ciBkaWUgQmVzdGltbXVuZyB2b24gU3RvcHB3w7ZydGVybi4gUHJvcG1heCBza2FsaWVydCBkaWUgV29ydGjDpHVmaWdrZWl0IHJlbGF0aXYgenVtIGZyZXF1ZW50ZXN0ZW4gV29ydCAoaGllciAnaG9sbWVzJykuIEZ1bmt0aW9uYWwgw6RobmVsbiBzaWNoIFRGLUlERiB1bmQgZGVyIHNww6R0ZXIgdm9yZ2VzdGVsbHRlIEtleW5lc3MtQW5zYXR6IC0tIGJlaWRlIGZpbmRlbiBiZXNvbmRlcnMgZGlzdGlua3RpdmUgVGVybWUuCgpgYGB7cn0KbWVpbmUuZGZtLnByb3BtYXggPC0gZGZtX3dlaWdodChtZWluZS5kZm0sIHNjaGVtZSA9ICJwcm9wbWF4IikKdG9wZmVhdHVyZXMobWVpbmUuZGZtLnByb3BtYXhbMSxdKQoKbWVpbmUuZGZtLnRmaWRmIDwtIGRmbV90ZmlkZihtZWluZS5kZm0pCnRvcGZlYXR1cmVzKG1laW5lLmRmbS50ZmlkZikKYGBgCgpTY2hsaWXDn2xpY2ggbMOkc3N0IHNpY2ggbWl0IFtkZm1fdHJpbSgpXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RmbV90cmltLmh0bWwpIG5vY2ggZWluZSByZWR1emllcnRlbiBEb2t1bWVudC1GZWF0dXJlLU1hdHJpeCBlcnN0ZWxsZW4uIERhcyBpc3QgZGFubiBzaW5udm9sbCwgd2VubiBtYW4gZGF2b24gYXVzZ2VodCwgZGFzcyBiZWlzcGllbHN3ZWlzZSBudXIgc29sY2hlIEJlZ3JpZmZlIGVpbmUgUm9sbGUgc3BpZWxlbiwgZGllIG1pbmRlc3RlcyBYIG1hbCBpbSBHZXNhbXRrb3JwdXMgdm9ya29tbWVuLiBBdWNoIGVpbmUgTWluZGVzdHphaGwgb2RlciBlaW4gTWF4aW11bSBhbiBEb2t1bWVudGVuLCBpbiBkZW5lbiBlaW4gQmVncmlmZiB2b3Jrb21tZW4gbXVzcyBvZGVyIGRhcmYsIGthbm4gYmVzdGltbXQgd2VyZGVuLiBTY2hsaWXDn2xpY2ggbGFzc2VuIHNpY2ggYmVpZGUgRmlsdGVyb3B0aW9uZW4gYXVjaCBwcm9wb3J0aW9uYWwgYW53ZW5kZW4gKHZnbC4gQmVpc3BpZWwpLgoKYGBge3J9Cm1laW5lLmRmbS50cmltIDwtIGRmbV90cmltKG1laW5lLmRmbSwgbWluX2RvY2ZyZXEgPSAxMSkgIyBGZWF0dXJlcywgZGllIG1pbmRlc3RlbnMgaW4gMTEgUm9tYW5lbiB2b3Jrb21tZW4KaGVhZChtZWluZS5kZm0udHJpbSwgbiA9IDEyLCBuZiA9IDEwKSAKCm1laW5lLmRmbS50cmltIDwtIGRmbV90cmltKG1laW5lLmRmbSwgbWluX3Rlcm1mcmVxID0gMC45NSwgdGVybWZyZXFfdHlwZSA9ICJxdWFudGlsZSIpICMgRmVhdHVyZXMgaW0gOTUuIEjDpHVmaWdrZWl0c3BlcnplbnRpbCAoPVRvcCA1JSBhbGxlciBGZWF0dXJlcykKaGVhZChtZWluZS5kZm0udHJpbSwgbiA9IDEyLCBuZiA9IDEwKSAKYGBgCgoKIyMjIyBERk1zIHZpc3VhbGlzaWVyZW4KCkRGTXMgbGFzc2VuIHNpY2ggdS5hLiBhdWNoIGFscyBXb3J0d29sa2UgZGVyIGjDpHVmaWdzdGVuIEJlZ3JpZmZlIGRhcnN0ZWxsZW4uIAoKYGBge3J9CnRleHRwbG90X3dvcmRjbG91ZChtZWluZS5kZm0sIG1heF93b3JkcyA9IDEwMCwgc2NhbGUgPSBjKDUsMSkpCmBgYAoKSW50ZXJlc3NhbnRlciBhbHMgZGllIERhcnN0ZWxsdW5nIGRlcyBHZXNhbXRrb3JwdXMgaXN0IGhpZXIgZGVyIFZlcmdsZWljaC4gRGFzIGZvbGdlbmRlIFBsb3QgemVpZ3QgZGllIGRpc3Rpbmt0aXZzdGVuIEJlZ3JpZmZlIG5hY2ggVEYtSURGIGbDvHIgdmllciBSb21hbmUsIHdvYmVpIGRpZSBGYXJiZSBkZW4gamV3ZWlsaWdlbiBSb21hbiBrZW5uemVpY2huZXQuIERhc3MgaW0gUGxvdCBkaWUgV29ydGdyw7bDn2UgbmljaHQgZGllIGFic29sdXRlIEZyZXF1ZW56IGFuemVpZ3QsIHNvbmRlcm4gZGVuIFRGLUlERi1XZXJ0LCBtYWNodCBlaW4gc29sY2hlcyBQbG90IGbDvHIgZGVuIHVubWl0dGVsYmFyZW4gVmVyZ2xlaWNoIG7DvHR6bGljaC4KCmBgYHtyfQp0ZXh0cGxvdF93b3JkY2xvdWQobWVpbmUuZGZtLnRmaWRmWzE6NCxdLCBjb2xvciA9IGJyZXdlci5wYWwoNCwgIlNldDEiKSwgY29tcGFyaXNvbiA9IFQpCmBgYAoKCg==