Text Mining on DerStandard Comments

Text Mining on DerStandard Comments

I did some text mining on DerStandard.at before, back then primarily interested in the comment count per article. What has been a simple HTTP GET and passing the response to BeautifulSoup requires a more sophisticated approach today. Things change, webcontent is created dynamically and we have to resort to other tools these days.

Headless browsers provide a JavaScript API, useful for retrieving the desired data after loading the page. My choice fell on phantomjs, available on pacman:

pacman -Qi phantomjs | head -n3
Name                     : phantomjs
Version                  : 2.1.1-3
Beschreibung             : Headless WebKit with JavaScript API

Since my JavaScript skills were close to non-existent, writing the script was the hard part. After some copypasta and trial-and-error coding, inevitably running into scoping and async issues, this clobbed together piece of code actually works! It reads the URL as its first and the pagenumber in the comment section as its second argument. The innerHTML from a .postinglist element is written to a content.html in the directory the parse-postinglists.js has been invoked from. Actually the data is appended to the file, that’s why I can loop over the available comment pages in R:

for (i in 1:43) {
  system(paste("phantomjs parse-postinglists.js",
               "http://derstandard.at/2000043417670/Das-Vokabular-der-Asylkritiker",
               i, sep=" "))
}

The article of interest is itself a quantitative analysis of word frequencies in recent forum debates on asylum. Presentation and insights are somewhat underwhelming though. After all, there is a lot of information collected and stored. Some of which, found in the attributes, is perfectly appropriate for the metadata in my Corpus object (created with the help of the tm package1), as can be seen from the first document in it:

meta(corp[[1]])
author       : Darth Invadeher
datetimestamp: 16-09-06 14:33
pos          : 104
neg          : 13
badges       : 0
pid          : 1014788751
parentpid    : NA
id           : 1
language     : de

pid is the unique posting ID, a parentpid value is applied when this particular posting refers to another posting, i.e. is a followup posting. This opens up the possibility to relate authors to each other and probably a lot more. badges doesn’t fit too well as an attribute name, it actually denotes the follower count in that forum. pos and neg show the positive resp. negative rating count on that particular posting. At the time of this analysis there were 1064 documents (i.e. postings) in the corpus.

The average lifespan of an online article is rather short. Interestingly, the likelihood to get a lot of votes diminishes even faster. That’s probably because a few posters take their debate further long-since the average voter is done with this article. So don’t be late to the party!

Sorry, your browser does not support SVG.

The bigger part of the data stored relates to the posting content. For now, I’m interested in extracting keywords that define the discussions. Their importance for the particular posting and the whole corpus is defined by a two-fold normalization by use of a TF-IDF weighting function. Obviously, what has been a rather one-sided reflection on terms used by asylum critics was followed by a nomenclatura debate. You can tell that from the dominance of “Begriff”, “Wort”, “Bezeichnung”, “Ausdruck” etc:

Sorry, your browser does not support SVG.

Prettify the Pipe

Prettify the Pipe

Eine der bemerkenswerteren Neuerungen der jüngsten Vergangenheit Rs dürfte wohl die Einführung des Pipe Operators sein, der mittlerweile doch auch schon eine Geschichte aufzuweisen hat. Weite Verbreitung erlangt er v.a. durch die dplyr Library aus dem populären „Hadleyverse“. Mithilfe der Pipe lässt sich der Code von oben nach unten statt von innen nach außen lesen. Ursprünglich hat man, um einen ähnlichen Lesefluss zu gewährleisten, etliche temporäre Objekte erstellt oder aber mehrfach verschachtelte Funktionen hingenommen.

Emacs in der Version 24.4 führt mit dem prettify-symbols-mode die Möglichkeit ein, bestimmte Symbols durch Unicode Charaktere zu ersetzen. Damit lassen sich entsprechend bedeutsame Operatoren hervorheben. Der nachstehende Code erweitert den hier gewählten Ansatz um die Verwendung des font-lock-constant-face für unterschiedliche Pipes:

(dolist (hook '(ess-mode-hook inferior-ess-mode-hook))
  (add-hook hook
            (lambda ()
              (push '("%>%"  . ?▶) prettify-symbols-alist)
              (push '("%<>%" . ?⧎) prettify-symbols-alist))))
(setq ess-R-assign-ops
      (remove-duplicates (nconc ess-R-assign-ops '("%>%" "%<>%"))
                         :test 'string-match-p)
      ess-R-fl-keyword:assign-ops
      (cons (regexp-opt ess-R-assign-ops) 'font-lock-constant-face))
ess-pipe.png
Abbildung 1: Unicode Pipe Character

Weekly Reviews mit orgclockr

Weekly Reviews mit orgclockr

Ich mache von den Möglichkeiten der Zeiterfassung in Org mode Gebrauch. Um ehrlich zu sein, ich schöpfe sie ziemlich aus. Die Inspiration dazu hatte ich vor allem diesem ausschweifenden Setup von Bernt Hansen zu verdanken. Wärend das tatsächliche Clocking u.a. dank Speedcommands ausgesprochen anwenderfreundlich ist, hatte ich doch gewisse Schwierigkeiten damit, den von Getting Things Done häufig hervorgehobenen Grundsatz der wöchentlichen Durchsicht in einer Art und Weise zu befolgen, die auch einen Ertrag verspricht. Für den Weekly Review verwende ich daher seit Kurzem das zu diesem Zweck geschriebene orgclockr, das v.a. durch die Visualisierungen die wesentlichen Informationen schnell zu Tage fördert. Außerdem finde ich die Möglichkeit des Filterns in R ausgesprochen vorteilhaft. Die Labels der folgenden Plots wurden, da nicht weiter von Belang, zur Unkenntlichkeit gekürzt. Ansonsten sprechen die Plots für sich:

library(orgclockr)
library(scales)

my_todofile <-
    file("~/orgmode/TODO.org") %>%
      readLines()
a_df <- org_clock_df(my_todofile)
b_df <- org_elements_df(my_todofile)

a_df %>%
  select(Date, TimeSpent, Headline) %>%
  filter(between(Date, Sys.Date() - 7, Sys.Date())) %>%
  qplot(Date, TimeSpent, data = ., geom = "bar",
        stat = "identity",
        position = "stack") +
  aes(fill = Headline) +
  scale_fill_hue(l=80, h=c(0, 270)) +
  guides(fill = FALSE) +
  scale_x_date(breaks = "days", labels = date_format("%A")) +
  theme_classic() +
  ylab("Time Spent (min)") +
  xlab("Date")
clocking1.png
library(tidyr)

left_join(a_df, b_df) %>%
  select(Date, Headline, TimeSpent, Effort) %>%
  filter(!is.na(Effort), between(Date, Sys.Date() - 7, Sys.Date())) %>%
  group_by(Headline) %>%
  summarise(TimeSpent = sum(TimeSpent),
            Effort = unique(Effort)) %>%
  arrange(desc(TimeSpent)) %>%
  head(8) %>%
  tidyr::gather(Variable, Value, TimeSpent:Effort) %>%
  as.data.frame() %>%
  ggplot() +
  aes(Headline, Value,
      fill = Variable) +
  scale_fill_brewer(type = "qual",
                    palette = 7) +
  geom_bar(stat = "identity",
           position = "dodge") +
  theme_classic() +
  theme(legend.title = element_blank(),
        legend.position = "bottom") +
  labs(x = "Task", y = "Time (min)") +
  scale_x_discrete(labels = abbreviate)
clocking3.png

Mich interessiert v.a. der Vergleich des veranschlagten Zeitaufwandes zur tatsächlich investierten Zeit. Mit einer Effizienzrate von knapp 90 Prozent ist die vergangene Woche unter diesem Aspekt betrachtet erfolgreich verlaufen:

f <-
    left_join(a_df, b_df) %>%
      select(Date, Headline, TimeSpent, Effort) %>%
      filter(!is.na(Effort), between(Date, Sys.Date() - 7, Sys.Date())) %>%
      mutate(Headline = abbreviate(Headline)) %>%
      arrange(desc(TimeSpent)) %>%
      group_by(Headline) %>%
      summarise(TimeSpent = sum(TimeSpent),
                Effort = unique(Effort)) %>%
      mutate(EfficiencyRate = round(Effort/TimeSpent, 2)) %>%
      print(n = 8)

round(sum(f$Effort) / sum(f$TimeSpent), 3)
     Joining by: "Headline"
    Source: local data frame [42 x 4]

       Headline TimeSpent Effort EfficiencyRate
    1     Asatd        35     60           1.71
    2      CSir        18     30           1.67
    3      Frrs         7     15           2.14
    4      Lmts        20     30           1.50
    5      Or()       141     15           0.11
    6    Or-nie        81     60           0.74
    7      Scmd        20      3           0.15
    8      WRQS        55     12           0.22
    ..      ...       ...    ...            ...
    [1] 0.892

Weitere Anwendungsmöglichkeiten von orgclockr sind der Vignette zu entnehmen.

Österreichs meistgelesener Facebook-Post in Zahlen

Österreichs meistgelesener Facebook-Post in Zahlen

Zum Text Mining im Web greife ich zwar meistens auf bewährte Python Libraries zurück, das Rfacebook Package lädt allerdings dazu ein, davon ausnahmsweise abzusehen. Anfang Oktober hat Armin Wolf, einer der bekanntesten Nutzer Sozialer Netzwerke Österreichs, mit einem Post laut DerStandard.at mehr als 4,5 Millionen Leser erreicht. Ausschlaggebend für die überwältigende Resonanz war zweifellos die Bezugnahme auf ein Thema, das in Österreich nach wie vor hohe Wellen zu schlagen vermag. Hier interessiert allerdings ausschließlich die Quantifizierung besagten Postings. Zum Vergleich ziehe ich die letzten 100 Posts auf Armin Wolfs Seite heran. Erwartungsgemäß erzielt der mich interessierende Eintrag mit über 70000 Likes zum Zeitpunkt der Analyse (31.10.2014) die meisten Likes des Untersuchungszeitraumes.

Sorry, your browser does not support SVG.

Dasselbe gilt allerdings nicht für die Shares, wie der folgenden Tabelle zu entnehmen ist:

Tabelle 1 Anzahl der Likes, Comments und Shares
Date Post Likes Comments Shares
2014-10-07 Versenkt. (Screenshot: Markus Albrecht)… 70090 1462 5886
2014-05-28 Das nenn ich sparsam! Zwei Männer teilen… 49403 720 6847
2014-10-08 Ich habe so etwas auf dieser FB-Seite wi… 28840 1040 708
2014-09-02 Prägnanter kann man die Differenz zwisch… 12942 208 1262
2014-04-13 Manchmal gibt es in der Innenpolitik Din… 11526 645 1377
2014-04-24 Mehr recht als Michael Hufnagl kann man … 10276 563 1242
2014-08-04 Barbara Prammer ist nur 60 Jahre alt gew… 9541 329 448
2014-10-17 Scheint, als hätte die Reinigungskraft i… 9098 201 260

Auch wenn mein Interesse für die nachfolgende Debatte in den 1462 Comments nicht einmal dazu langte, diese kurz zu überfliegen, kann ich mir die stattgefundenen Anfeindungen nur zu lebhaft vorstellen. Einen Überblick gewährt mir die Wortwolke der Wortstämme, die in diesem Kontext zumindest 15 Mal Verwendung fanden. Dobermann und Korun heißen die beiden Protagonisten, um die es in diesem Post geht:

wc.png

Das Resultat ist weder überraschend noch sonderlich aufschlussreich. Interessanter ist es, die Termdokumentmatrix auf Wortassoziationen zu untersuchen. Ich gehe davon aus, dass die FPÖ auf dieser FB-Seite auf wenig Gegenliebe stößt:

Tabelle 2 Teil der Wortassoziationen mit FPÖ
Wortassoziation r
versag 0.43
wahl 0.4
alexandrehaha 0.39
amtlich 0.39
anhangsel 0.39
ankreuz 0.39
baaaaaaaammmmmmmmmmmm 0.39
bevolkerungsschicht 0.39
beweis 0.39
bierzelt 0.39
bursch 0.39
deppad 0.39

Der verwendete Code ist hier zu finden.

MBLeague Teamstatistiken

MBLeague Teamstatistiken

Wenn auch nicht als aktiver Spieler, sondern nur “auf Abruf” rekrutiert, so verfolge ich die Mixed Basketball League Wiens doch mit zunehmendem Interesse. Die Website bietet eine umfangreiche Tabelle der wöchentlich erbrachten Leistungen der Spieler. Spärlich sind allerdings Informationen zur Performanz der Teams gesät: Einzig Rang, Siege und Niederlagen lassen sich aus einer weiteren Tabelle beziehen. Der Reiz, die Informationen zu verknüpfen und aussagekräftige Kennzahlen für die Stärken und Schwächen der Teams zu bilden, besteht v.a. darin, dass durch die obligatorische Frau auf dem Feld neben Dreipunktern, Zweipunktern und Freiwürfen auch Vierpunkter und Dreipunkter aus dem Feld die Scoring Verteilung bereichern. Besonders interessant wird sein, welches Verhältnis der genannten Möglichkeiten, den Punktestand hochzuschrauben, in der Mixed League zum Erfolg führt.

Das Regelwerk sieht einen zusätzlichen Punktgewinn für Frauen bei einem Treffer aus dem Spiel heraus vor. Die Punkte der weiblichen Mitspieler auszumachen ist daher insofern möglich, als deren Gesamtpunkte P die Summe aus 3P * 3, FG * 2 und FT übersteigen muss. Der Rest ergibt sich dann aus simpler Arithmetik. Eine Übersicht über die Punkteverteilung innerhalb der Teams (darüber hinaus auch Informationen zur durchschnittlichen Punktedifferenz und vieles mehr) in der Saison 2014/15 findet sich hier, der Code dazu auf Github.

Der Schönbrunner Schlosspark im Tageslicht

Der Schönbrunner Schlosspark im Tageslicht

Vor wenigen Wochen habe ich beschlossen, den Ballsport um ein Lauftraining mit geringer Intensität zu ergänzen. Als Abwechslung zur sonst doch einseitigen sportlichen Betätigung gedacht, entpuppt sich das Laufen v.a. als willkommene Gelegenheit, für eine halbe bis dreiviertel Stunde abzuschalten. Dem zuträglich ist natürlich das Ambiente der gewählten Laufroute im Schönbrunner Schlosspark. Die naturgemäß im Herbst sich verkürzende Dauer des Tageslichts und die nach und nach reduzierten Öffnungszeiten des Schlossparks stellen insbesondere Berufstätige vor die Herausforderung, das günstige Zeitfenster nicht zu verpassen. Hier versuche ich auszumachen, wie knapp dieser tägliche Zeitrahmen bemessen ist:

daylight.png

Das Tageslicht wird durch die Öffnungszeiten offensichtlich relativ gut eingefangen. Im Mai, August und September wird etwas Potential verschenkt, nämlich in Summe knapp 11h Tageslicht. Zu beachten gilt auch, dass Einlasszeiten einzuhalten sind. Deshalb hat man, solange der Schlosspark bereits um 17:30 schließt, auch zeitig loszustarten. Das gilt immerhin für 121 Tage im Jahr:

closing.png

Für die restliche Zeit, also immerhin ca. zwei Drittel des Jahres, wird mir der Schlosspark mit all seinen Annehmlichkeiten wohl als Laufroute dienen. Der Code zur Analyse ist hier zu finden.

Die Treffsicherheit der Oldies

Die Treffsicherheit der Oldies

In den letzten beiden NBA-Playoffs wurde die Bedeutung der Leistungen jenseits der Drei-Punkt-Linie teils deutlich unterstrichen. Besonders interessant finde ich dabei, dass ein gewisser Spielertyp immer häufiger anzutreffen ist, der früher nicht in dieser Regelmäßigkeit in Erscheinung getreten ist. Spontan denke ich dabei an Matt Bonner und Derek Fisher, aber auch an Spieler mit mehr Einsatzminuten wie Shane Battier und Vince Carter, die sich primär ihrer Stärke “from downtown” besinnen. Ansonsten sind sie relativ unscheinbar aber mit vielen Jahren NBA-Erfahrung ausgestattet. Die Daten beziehe ich von basketball-reference.com und beschränke mich auf die Playoffs der Jahre 2010 bis 2014. Dort finden sich die besten 16 Teams der jeweils abgeschlossenen Saison (Ausnahmen bestätigen die Regel). Die Zahlen der Playoffs 2014 zeigen am deutlichsten, welchen Spielertyp ich meine. Während das Alter aller Playoff-Teilnehmer dieses Jahres im Median bei 27 Jahren liegt, liegt dasjenige der Spieler, die mehr als die Hälfte ihrer Würfe von weit draußen nehmen (bei mindestens 10 Dreipunkt-Versuchen insgesamt in den Playoffs), bereits bei 31 Jahren. Wählt man die Top 20 nach Dreipunkt-Versuchsrate, erhält man einen Median von stolzen 33 Jahren:

age_distribution.png

Während die Playoff-Spieler der letzten fünf Saisonen im Median relativ konstant um die 27 Jahre alt waren, hat sich das bei der uns interessierenden Spielergruppe deutlich geändert:

Tabelle 1 Alter der Playoff-Teilnehmer im Median
  top25 total
2014 33 27
2013 31 27
2012 31 26
2011 30 27
2010 27 27

Die Herren können ihre Wurfauswahl durchaus rechtfertigen: Die Top20 dieser Gruppe kommen im Schnitt auf eine 3FG% von über 42,2 Prozent. Selbst die Top50 liegen noch bei überdurchschnittlichen 39,2 Prozent. Was das Verhältnis der Würfe von draußen zu allen genommenen Würfen angeht, hat es Shane Battier in den Playoffs 2013 auf die Spitze getrieben: Von seinen 93 Field-Goal Attempts absolvierte er ganze 88 von jenseits der Dreipunkt-Linie. Resultiert in einer unglaublichen Quote von knapp 95 Prozent. Bei den verbleibenden fünf Würfen hat er wohl aus Versehen übertreten:

Tabelle 2 Höchste Dreipunkt-Versuchsrate bei mind. 10 Versuchen
TPAR Player Pos Tm Age FGA 3PA 3P PTS year
0.9462 Shane Battier SF MIA 34 93 88 26 103 2013
0.8824 Troy Daniels SG HOU 22 17 15 8 31 2014
0.8551 Keith Bogans SG CHI 30 69 59 25 82 2011
0.8519 Rudy Fernandez SG POR 24 27 23 11 41 2010
0.8333 Steve Blake PG GSW 33 12 10 3 11 2014
0.8214 Daequan Cook SG OKC 23 56 46 16 64 2011
0.8193 DeShawn Stevenson SG DAL 29 83 68 27 94 2011
0.8182 Kyle Korver SG ATL 32 66 54 23 94 2014
0.8 Mike Miller SF MIA 32 45 36 16 58 2013
0.8 Rasual Butler SG IND 34 15 12 5 19 2014
0.8 James Jones SF MIA 33 40 32 15 53 2014
0.7857 Shane Battier SF MIA 33 140 110 42 161 2012
0.7857 Anthony Tolliver SF ATL 27 14 11 7 24 2013
0.7826 Steve Blake PG LAL 30 23 18 6 20 2011
0.775 Carlos Delfino SF HOU 30 40 31 11 45 2013
0.7692 Shane Battier SF MIA 35 26 20 9 37 2014
0.7692 Hedo Turkoglu SF LAC 34 13 10 4 16 2014
0.7667 Chris Copeland PF NYK 28 30 23 11 37 2013
0.7625 Mickael Pietrus SF ORL 27 80 61 28 118 2010

Der Code zur Analyse ist hier zu finden.

← Newer  1/2  Older →