<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>text as data on Methods Bites</title>
    <link>https://socialsciencedatalab.mzes.uni-mannheim.de/tags/text-as-data/</link>
    <description>Recent content in text as data on Methods Bites</description>
    <generator>Hugo -- gohugo.io</generator>
    <lastBuildDate>Tue, 30 May 2023 00:00:00 +0100</lastBuildDate>
    
        <atom:link href="https://socialsciencedatalab.mzes.uni-mannheim.de/tags/text-as-data/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Transformer-based language models</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/video/transformer-models/</link>
      <pubDate>Tue, 30 May 2023 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/video/transformer-models/</guid>
      <description><![CDATA[
        <div id="abstract" class="section level5">
<h5>Abstract</h5>
<p>Transformer-based models have recently gained much attention, especially with the release of ChatGPT. Since 2017, deep learning models based on the Transformer architecture have become an important research tool. Their development and application in various fields, including the social sciences, continue to expand. In this talk, we will examine the components that make up these language models and explore how to train state-of-the-art models with HuggingFace for your research. We will also discuss these models’ limitations and open challenges, including open-source availability, the growing need for resources, responsibility, and more.</p>
</div>
<div id="presenter" class="section level5">
<h5>Presenter</h5>
<p>Christopher Klamm <a href="mailto:klamm@uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://chkla.github.io/gitPage/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/chklamm"><i class="fa fa-twitter"></i> </a> is an interdisciplinary researcher at the University of Mannheim (Germany) at the Data and Web Science Group working at the intersection of Natural Language Processing and Computational Political Science.</p>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Collection, Management, and Analysis of Twitter Data</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/twitter-research-track/</link>
      <pubDate>Thu, 02 Jun 2022 01:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/twitter-research-track/</guid>
      <description><![CDATA[
        </p>
<p>As a highly relevant platform for political and social online interactions, researchers increasingly analyze Twitter data. As of 01/2021, Twitter renewed its API, which now includes access to the full history of tweets for academic usage. In this <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/categories/tutorials/">Methods Bites Tutorial</a>, <a href="https://twitter.com/ankuepfer">Andreas Küpfer</a> (Technical University of Darmstadt &amp; MZES) presents a walkthrough of the collection, management, and analysis of Twitter data.</p>
<p>After reading this blog post and engaging with the applied exercises, readers will be able to:</p>
<ul>
<li>complete the academic research track application process for the Twitter API.</li>
<li>crawl tweets using customized queries based on the R package <code>academictwitteR</code> <span class="citation">(Barrie and Ho <a href="#ref-BarrieHo2021" role="doc-biblioref">2021</a>)</span>.</li>
<li>apply a selection of pre-processing steps to these tweets.</li>
<li>take decisions in order to minimize reprodubcibility issues with Twitter data and to comply with the policies.</li>
</ul>
<p><em>Note:</em> This blog post provides a summary of Andreas’ workshop in the <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/page/events/">MZES Social Science Data Lab</a>. The original workshop materials, including slides and scripts, are available from our <a href="https://github.com/SocialScienceDataLab/twitter-api-bert-method">GitHub</a>.
A live recording of the workshop is available on our <a href="https://www.youtube.com/watch?v=Gzl0lpQ7S7w">YouTube channel</a>.</p>
<div id="overview" class="section level3">
<h3>Overview</h3>
<ol style="list-style-type: decimal">
<li><a href="#introduction-to-social-media-twitter-api-v2"><strong>Introduction to social media &amp; Twitter API v2</strong></a></li>
<li><a href="#academic-research-track-application-process"><strong>Academic research track application process</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#application">Application</a></li>
<li><a href="#after-the-application">After the application</a></li>
</ol></li>
<li><a href="#using-the-api"><strong>Using the API</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#postman-as-a-playground">Postman-as-a-playground</a></li>
<li><a href="#which-package-should-i-choose">Which package should I choose?</a></li>
<li><a href="#academictwitter-a-code-walkthrough-using-r">academictwitteR: a code walkthrough using R</a></li>
</ol></li>
<li><a href="#wpreparing-for-methods-working-with-textual-data"><strong>Preparing for methods: working with textual data</strong></a></li>
<li><a href="#reproducibility-of-research-basbed-on-twitter-data"><strong>Reproducibility of research based on Twitter data</strong></a></li>
<li><a href="#further-readings"><strong>Further readings</strong></a></li>
</ol>
</div>
<div id="introduction-to-social-media-and-twitter-api-v2" class="section level3">
<h3>Introduction to social media and Twitter API v2</h3>
<p>Social media posts are full of potential for data mining and analysis. Despite problems tackling fake accounts and bots on the platform, it can be a very fruitful source to tackle research questions in a bandwidth of disciplines, including social sciences <span class="citation">(e.g., Barberá <a href="#ref-barberá_2015" role="doc-biblioref">2015</a>; Nguyen et al. <a href="#ref-NGUYEN2021100922" role="doc-biblioref">2021</a>; Valle-Cruz et al. <a href="#ref-cruz2022" role="doc-biblioref">2022</a>; Sältzer <a href="#ref-doi:10.1177/1354068820957960" role="doc-biblioref">2022</a>)</span>. Recognizing this potential also for commercial usage, platform providers increasingly restrict free access to such data.</p>
<p>Especially Twitter is an important data source with its richness of social and political interactions. As well Twitter did not offer a free-of-charge option to implement a full archive search of all tweets and users. Back then, the free version of API v1.1 was very limited with a maximum of 3,200 tweets or the past seven days of tweets. In addition, the range of available meta data<a href="#fn1" class="footnote-ref" id="fnref1"><sup>1</sup></a> as well as implemented query<a href="#fn2" class="footnote-ref" id="fnref2"><sup>2</sup></a> options were rather small. These limitations were lifted by the introduction of the redeveloped and rearranged Twitter API v2 in January 2021.<a href="#fn3" class="footnote-ref" id="fnref3"><sup>3</sup></a> For academic purposes, they opened up access to all available tweets and other objects posted on Twitter without any monetary costs for the researcher.</p>
<p>While this blog post focuses on the retrieval of textual data, Twitter content certainly offers more. Looking at social network interactions (e.g., followers, likes, …) is just one of the opportunities beyond text to reveal valuable information. This can be, for example, the usage of follower networks to estimate ideological positions <span class="citation">(e.g., Barberá <a href="#ref-barberá_2015" role="doc-biblioref">2015</a>)</span> or measuring the importance of a user in a social network based on social interaction data.</p>
</div>
<div id="academic-research-track-application-process" class="section level3">
<h3>Academic research track application process</h3>
<p>As Application Programming Interfaces (APIs) are powerful tools which allow access to vast databases full of information, companies offering them are increasingly careful about who is allowed to use them. While the previous version of the Twitter API provided access without a dedicated application (for a detailed description, see this Methods Bites <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/collecting-and-analyzing-twitter-using-r/">tutorial</a>), the novel version requires you to go through an application process where you have to provide several details about you and your project with Twitter. This information includes data regarding yourself as well as the research project where you intend to work with Twitter data.</p>
<div id="prerequisites" class="section level5">
<h5>Prerequisites</h5>
<p>Before getting access, you have to fulfill several formal prerequisites to be eligible for application:</p>
<ul>
<li>You are either a master’s student, a doctoral candidate, a post-doc, a faculty member, or a research-focused employee at an academic institution or university.</li>
<li>You have a clearly defined research objective, and you have specific plans for how you intend to use, analyze, and share Twitter data from your research.</li>
<li>You will use this access for non-commercial purposes.<a href="#fn4" class="footnote-ref" id="fnref4"><sup>4</sup></a></li>
</ul>
<p>Furthermore, you need a Twitter account which is also used to log in to the Twitter Developer Platform after a successful application. This portal lets you configure your API projects, keep an eye on your monthly tweet cap<a href="#fn5" class="footnote-ref" id="fnref5"><sup>5</sup></a>, and more. A more detailed explanation of prerequisites can be found on the <a href="https://developer.twitter.com/en/products/twitter-api/academic-research/application-info">Twitter API academic research track Track</a> website.</p>
</div>
<div id="application" class="section level5">
<h5>Application</h5>
<p>The whole process can be initiated by clicking <a href="https://developer.twitter.com/en/portal/petition/academic/is-it-right-for-you">Apply</a> on the official <a href="https://developer.twitter.com/en/products/twitter-api/academic-research">Twitter API academic research track</a> Website. You’ll be asked to log in with your personal Twitter account.</p>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:unnamed-chunk-1"></span>
<img src="../../../../../article/twitter-research-track/twitter_application.png" alt="Twitter Application Steps for academic research track API access." width="100%" />
<p class="caption">
Figure 1: Twitter Application Steps for academic research track API access.
</p>
</div>
<div style="text-align: right">
<p><sub><sup>
Source: <a href="https://developer.twitter.com/en/portal/petition/academic/is-it-right-for-you">Twitter API Application Process</a>
</sub></sup></p>
</div>
<p>The figure above visualizes the steps you have to complete before your application can finally be submitted for Twitter’s internal review:</p>
<ol style="list-style-type: decimal">
<li><strong>Basic Info</strong>: such as phone number verification and country selection</li>
<li><strong>Academic Profile</strong>: such as link to an official profile (department website or similar) and academic role</li>
<li><strong>Project Details</strong>: such as information about findings, description of the project itself, and how the API should be used there (e.g. methodologies and how the outcomes will be shared)</li>
<li><strong>Review</strong>: provides an overview of the previous steps</li>
<li><strong>Terms</strong>: developer agreement and policy</li>
</ol>
<p>Before starting, it is recommended to carefully read which kind of career levels, projects and data behaviors are not allowed to use the API and thus have a high chance of receiving a refusal for their application. To give an example, if you plan to share the content of tweets publicly, you most probably won’t get access to the API as this would violate the Twitter rules. Again, more detailed information about this can be found on the <a href="https://developer.twitter.com/en/products/twitter-api/academic-research/application-info">Twitter API academic research track</a> and <a href="https://developer.twitter.com/en/developer-terms/more-on-restricted-use-cases">Developer Terms</a> information guides.</p>
<p>Step one requests generic information about your Twitter account while in step two you have to provide information about your academic profile. This includes a link to a publicly available record on an official department website and information regarding the academic institution you are working in. The third step is the most sophisticated one: your research project. It asks for short paragraphs about the project in general, what and how Twitter data is used there, and how the outcome of your work is shared with the public. The last two steps, review and terms, do not require any user-specific input but provide an overview of all filled-in information as well as the chance to read the developer agreement and policy.</p>
</div>
<div id="after-submitting-your-application" class="section level5">
<h5>After submitting your application</h5>
<p>After submitting your application, you receive a decision via the e-mail address connected with your Twitter account (usually) within a few days. However, according to Twitter, this process can take up to two weeks.</p>
<p>You application may be rejected for two common reasons: First, you do violate the policy at one point according to the information given, or second, you do not meet the <a href="#before-the-application">requirements (as described above)</a>. Further explanations what can be the next steps after a rejection can be found in the <a href="https://developer.twitter.com/en/support/twitter-api/developer-account">Developer Account Support FAQ</a>.</p>
<p>As of writing this blog post (May 2022), submitting a reapplication for access using the same account is not possible.</p>
</div>
</div>
<div id="using-the-api" class="section level3">
<h3>Using the API</h3>
<p>After your successful application, the <a href="https://developer.twitter.com/en/portal/dashboard">Twitter Developer Portal</a> is there to manage projects and environments (which belong to a project), generate API keys (“credentials” for API access), get an overview of real-time monthly tweet cap usage, check available API endpoints and their specifics and more.</p>
<p>After the creation of a project, an environment can be added and API keys generated.</p>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:unnamed-chunk-2"></span>
<img src="../../../../../article/twitter-research-track/twitter_apikeys.png" alt="API keys of an environment" width="50%" />
<p class="caption">
Figure 2: API keys of an environment
</p>
</div>
<div style="text-align: right">
<p><sub><sup>
Source: <a href="https://developer.twitter.com/en/docs/tutorials/step-by-step-guide-to-making-your-first-request-to-the-twitter-api-v2">Twitter API Application Guide</a>
</sub></sup></p>
</div>
<p>The following keys are generated automatically and used depending on the API interface (e.g. the R package) at hand:</p>
<ul>
<li>API key <span class="math inline">\(\approx\)</span> username (also called consumer key)</li>
<li>API key secret <span class="math inline">\(\approx\)</span> password (also called consumer secret)</li>
<li>Bearer token <span class="math inline">\(\approx\)</span> special access token (also called an authentication token)</li>
</ul>
<p>It is crucial to <strong>keep them private and not push them to GitHub or similar!</strong> Otherwise someone else could gain access to your API account. Instead, store them somewhere locally or directly within an environment variable. The package we’re going into detail later on this blog post is guiding you safely through this process.</p>
<p>However, in case you’re plan to use them in other applications, you can store your keys in different ways. The most common way in R is to add them to the <code>.Renviron</code> file. To do this with comfort, install the R package <code>usethis</code> and call its method <code>usethis::edit_r_environ()</code> which lets you edit the <code>.Renviron</code> in the home directory of your computer. In the following you can add tokens (or anything else you want to keep stored locally) using this format:</p>
<pre class="bash"><code>Key1=value1
Key2=value2
# ...</code></pre>
<p>After saving the file you can access values by calling <code>Sys.getenv("Key1")</code> within your R application. More best practices on managing your secrets can be found on the website <a href="https://bookdown.org/paul/apis_for_social_scientists/best-practices.html">APIs for Social Scientists</a>.</p>
<div id="postman-as-a-playground" class="section level5">
<h5>Postman-as-a-playground</h5>
<p><a href="https://www.postman.com/">Postman</a> is an easily accessible application to try out different queries, tokens, and more. Without any programming knowledge, you get the API results immediately. <a href="https://developer.twitter.com/en/docs/tutorials/postman-getting-started">Here</a> you can find an official tutorial to use Postman with the Twitter API.</p>
<p>However, there are several reasons why Postman cannot replace a package and programming code.</p>
<ul>
<li>Building flexible <a href="https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query">queries</a> (e.g., a list of users to retrieve tweets from)</li>
<li>Handling large responses which come split up during <a href="https://developer.twitter.com/en/docs/twitter-api/pagination">pagination</a></li>
<li>Handle <a href="https://developer.twitter.com/en/docs/twitter-api/rate-limits#v2-limits">rate limit restrictions</a></li>
<li>Transforming responses into manageable data structures (e.g., dataframe and comma-separated values)</li>
</ul>
<p>All of these tasks can be handled by a suitable package in your favorite programming language.</p>
</div>
<div id="which-package-should-i-choose" class="section level5">
<h5>Which package should I choose?</h5>
<p>It has to be noted that there are dozens of packages out there but only some of them already integrated the academic research track of the Twitter API. A selection of packages is listed below:</p>
<ul>
<li><strong><a href="https://github.com/cjbarrie/academictwitteR">academictwitteR</a> (R)</strong>:
The package offers customizable fucntions for all common v2 API endpoints. Additionally, it smoothly guides the developer through all critical steps (e.g. authentication or data processing) of the API interaction.</li>
<li><strong><a href="https://github.com/MaelKubli/RTwitterV2">RTwitterV2</a> (R)</strong>:
Although <code>RTwitterV2</code> as of now has less API endpoints included than <code>academictwitteR</code> it still is a valuable alternative which covers all basic functionalities.</li>
<li><strong><a href="https://github.com/ropensci/rtweet">rtweet</a> (R)</strong>:
<code>rtweet</code> does not support the academic research track yet, however it offers much basic functionality by using the previous API version. A dedicated Methods Bites blog post introducing <code>rtweet</code> in detail can be found <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/collecting-and-analyzing-twitter-using-r/">here</a>.</li>
<li><strong><a href="https://github.com/twitterdev/search-tweets-python/tree/v2">searchtweets-v2</a> (Python)</strong>:
This is the official package developed and maintained by Twitter available for Python. The library offers flexible functions which even handle very specialized requests but one has to dive deeper into the technical aspects of the API.</li>
<li><strong><a href="https://github.com/tweepy/tweepy">tweepy</a> (Python)</strong>:
<code>tweepy</code> is the most common package for Python and backed up by a large developer community. As a bonus, it includes many examples of how to use the various features offered by the package.</li>
</ul>
<p>Which package you pick should depend on your preferred programming language as well as whether the feature list of a package fits your research purpose.</p>
</div>
<div id="academictwitter-a-code-walkthrough-using-r" class="section level5">
<h5>academictwitteR: a code walkthrough using R</h5>
<p>In this blog post <a href="https://github.com/cjbarrie/academictwitteR">academictwitteR</a> <span class="citation">(Barrie and Ho <a href="#ref-BarrieHo2021" role="doc-biblioref">2021</a>)</span> (available for R) is used to demonstrate a simple scenario of retrieving tweets from German members of the parliament. The name <code>academictwitteR</code> is derived by the Twitter API academic research track for which it is developed for.</p>
<p>We will start by first loading all the needed R packages for the walkthrough:</p>
<details>
<p><summary>Code: R packages used in this tutorial</summary></p>
<pre class="r"><code>## Save package names as a vector of strings
pkgs &lt;- c(&quot;dplyr&quot;, &quot;academictwitteR&quot;, &quot;quanteda&quot;, &quot;purrr&quot;)

## Install uninstalled packages
lapply(pkgs[!(pkgs %in% installed.packages())], install.packages)

## Load all packages to library and adjust options
lapply(pkgs, library, character.only = TRUE)</code></pre>
</details>
<p><br>
After loading the packages, we need to share our API Bearer Token with <code>academictwitteR</code>. The following code will guide you through the process to store the key in an R-specific environment file (<code>.Renviron</code>) which we introduced earlier in this blog post:</p>
<pre class="r"><code>academictwitteR::set_bearer()</code></pre>
<pre class="bash"><code>## Instructions:
## ℹ 1. Add line: TWITTER_BEARER=YOURTOKENHERE to .Renviron 
##      on new line, replacing YOURTOKENHERE with  actual bearer token
## ℹ 2. Restart R</code></pre>
<p>After restarting R, everything is initialized and we can load a table of Twitter user IDs from German MPs into R:</p>
<pre class="r"><code>german_mps &lt;- read.csv(&quot;data/MP_de_twitter_uid.csv&quot;,
                       colClasses=c(&quot;user_id&quot;=&quot;character&quot;))
head(german_mps)</code></pre>
<pre class="bash"><code>##              user_id                   name party
## 1           44608858       Marc Henrichmann   CDU
## 2 819914159915667456      Stephan Pilsinger   CSU
## 3         1391875208   Markus Alexander Uhl   CDU
## 4          569832889 Sigmar Hartmut Gabriel   SPD
## ...</code></pre>
<p>To prevent replication issues with your work, it is recommended to use the Twitter user ID (e.g. <em>819914159915667456</em>) instead of the user handle (e.g. <em>@StephPilsinger</em>) as the user handle can be changed by the user over time. This would result in not being able anymore to recrawl tweets of these users. In case you only have access to the handle, there is a v2 API endpoint to receive a user object from a handle: <em>/2/users/by/username/:username</em></p>
<p>Databases and lists of Twitter users can be retrieved from the following sources:</p>
<ul>
<li>The Twitter Parliamentarian Database <span class="citation">(Vliet, Törnberg, and Uitermark <a href="#ref-vanVliet2020TheTP" role="doc-biblioref">2020</a>)</span></li>
<li>Public Twitter lists (e.g. <a href="https://twitter.com/i/lists/912241909002833921" class="uri">https://twitter.com/i/lists/912241909002833921</a>)<a href="#fn6" class="footnote-ref" id="fnref6"><sup>6</sup></a></li>
<li>legislatoR R Package <span class="citation">(Göbel and Munzert <a href="#ref-göbel_munzert_2021" role="doc-biblioref">2021</a>)</span></li>
</ul>
<p>Afterward, we are ready to crawl our first tweets using a simple wrapper function (<code>get_tweets_from_user()</code>) asking for a single <code>user_id</code>. <code>get_all_tweets()</code>, which is called inside this function is the heart of our code. It manages the generation of queries for the API, working with rate limits as well as storing the data in <code>JSON</code>-files (which can be transformed later).</p>
<p>In case you look for specific content, tweet types, or even topics, you can add another parameter to the package function: <code>query</code>. It allows you to narrow down your search by using specific strings. To give an example, one could look for English retweets containing the keywords <em>putin</em> or <em>selenskij</em> having a geo-location attached. This can be achieved by simply assigning the following string to the <code>query</code> parameter:</p>
<pre class="r"><code>(putin OR selenskyj) -is:retweet lang:en has:geo</code></pre>
<p>Beyond that, there exist many more parameters to individualize the crawling method. All of them are documented in the <a href="https://cran.r-project.org/web/packages/academictwitteR/academictwitteR.pdf">official <code>academictwitteR</code> CRAN documentation</a> of the package.
However, in this tutorial I only restrict my search to a Twitter user ID as well as a start and end date for the tweets we are interested in:</p>
<pre class="r"><code># function to retrieve tweets in a specific time period of a single user
# (list of user IDs would be possible but one should keep
# the max. query string of 1024 characters in mind)

get_tweets_from_user &lt;- function(user_id) {
  # Another option is to add &quot;query&quot; parameter
  academictwitteR::get_all_tweets(
    users = user_id,
    start_tweets = &quot;2021-01-01T00:00:00Z&quot;,
    end_tweets = &quot;2021-09-30T00:00:00Z&quot;,
    data_path = &quot;data/raw/&quot;,
    n = 100)
}</code></pre>
<p>The function is then called for each <code>user_id</code> in the dataframe by using <code>walk()</code> from the <a href="https://purrr.tidyverse.org"><code>purrr</code></a> package (the <code>purrr</code> package allows you to work with functions and vectors):</p>
<pre class="r"><code>purrr::walk(german_mps[[&quot;user_id&quot;]], get_tweets_from_user)</code></pre>
<p>To import the tweets into a workable format, call <code>bind_tweets()</code> from <code>academictwitteR</code>. It consolidates all available files in the given <code>data_path</code> and organizes them into the requested format (in our case tidy). In addition, only a relevant fraction of columns is selected in the code below by using <code>select()</code> from the <a href="https://dplyr.tidyverse.org"><code>dplyr</code></a>-package.</p>
<pre class="r"><code># concatenate all retrieved tweets into one dataframe and select which columns
# should be kept
# Another option: set parameter &quot;user&quot; to TRUE to retrieve user information
tweets_df &lt;- academictwitteR::bind_tweets(data_path = &quot;data/raw/&quot;,
                                          output_format = &quot;tidy&quot;) %&gt;%
  dplyr::select(
    tweet_id,
    text,
    author_id,
    user_username,
    created_at,
    sourcetweet_type,
    sourcetweet_text,
    lang
  )</code></pre>
<p>Finally, I store the tweets in a single .csv-file:</p>
<pre class="r"><code>write.csv(tweets_df, &quot;data/raw/tweets_german_mp.csv&quot;, row.names = FALSE)</code></pre>
<p>Congratulations! You successfully applied to the academic research track, got admitted, and crawled a selection of tweets using the R package <code>academictwitteR</code>.</p>
</div>
</div>
<div id="preparing-for-methods-working-with-textual-data" class="section level3">
<h3>Preparing for methods: working with textual data</h3>
<p>You are now ready to move on! The usual steps applied to textual data (lowercasing, stopwords removal, stemming, …) depending on the method at hand can be used for pre-processing your tweets. Additional fine-tuning of these steps could involve the removal of, e.g. party IDs, URLs, user mentions or similar. Such steps can be easily done by using regular expressions (<code>regex</code>). Regular expressions are used to extract patterns in texts which then can be used for further analysis, removal or replacement. Many tutorials available on the web (e.g. <a href="https://regexone.com/">RegexOne interactive tutorial</a>) make it straightforward to learn how to bring such expressions into action within your domain.</p>
<p>The following code provides a first starting point for applying pre-processing steps. The code relies on the package <a href="https://quanteda.io"><code>quanteda</code></a> which is an R package that is often used when working with text data in R. If you want to dive deeper into <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/">text mining</a> and <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/quantitative-analysis-of-political-text/">text analysis</a>, Methods Bites has more blog posts on these topics.</p>
<pre class="r"><code>tweet_corpus &lt;- quanteda::corpus(tweets_df[[&quot;text&quot;]],
                                 docnames = tweets_df[[&quot;tweet_id&quot;]])</code></pre>
<p>The code first transforms the dataframe of tweets into another data format, <code>corpus</code>, keeping the <code>tweet_id</code> as an identifier attached to each tweet <code>text</code>.
Having the tweets in the corpus format makes it easy to apply pre-processing steps after tokenizing. The following list shows a selection of common methods. However, it is important that the decision, on which methods are applied, heavily relies on the following text processing approach:</p>
<ul>
<li><code>remove_punct</code>: removes all punctuation</li>
<li><code>remove_numbers</code>: removes all numbers</li>
<li><code>dfm_tolower()</code>: applies lowercasing</li>
<li><code>dfm_remove(stopwords("german"))</code>: removes German stopwords which occur very frequently</li>
<li><code>dfm_wordstem(language = "german")</code>: applies German stemming (e.g., <em>wurden</em> <span class="math inline">\(\rightarrow\)</span> <em>wurd</em>)</li>
</ul>
<pre class="r"><code># &quot;2020 wurden in Berlin ca. 18.800 Miet-
# in Eigentumswohnungen umgewandelt. #Umwandlungsverbot&quot;
dfm &lt;-
  quanteda::dfm(tweet_corpus %&gt;%
                  quanteda::tokens(
                    remove_punct = TRUE,
                    remove_numbers = TRUE)) %&gt;%
  quanteda::dfm_tolower() %&gt;% # removes capitialization
  quanteda::dfm_remove(
    stopwords(&quot;german&quot;)) %&gt;% # removes German stopwords
  quanteda::dfm_wordstem(
    language = &quot;german&quot;) # transforms words to their German wordstems
# &quot;wurd berlin ca miet- eigentumswohn umgewandelt #umwandlungsverbot&quot;</code></pre>
<p>The function <code>dfm</code> (called above) returns a <a href="https://quanteda.io/reference/dfm.html">sparse document-feature matrix</a> which could be a fruitful starting point for first-word frequency analysis:</p>
<pre class="bash"><code>head(dfm)

## Document-feature matrix of: 6 documents, 87 features (79.77% sparse)
## and 0 docvars.
##                       features
## docs                  leb plotzlich mehr schablon gut bos pass 😉 #esk #miet
##   44608858            1   1         1    1        1   1   1    1  1    1
##   819914159915667456  0   0         0    0        0   0   0    0  0    0
##   1391875208          0   0         0    0        0   0   0    0  0    0
##   569832889           0   0         1    0        0   0   0    0  0    1
## ...</code></pre>
<p>You are finally at the step of applying further methods to tackle your research question and getting deeper insights into your crawled tweets. There is much more to explore: You can find further <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/tags/text-as-data/">text-as-data tutorials</a> on our blog.</p>
</div>
<div id="reproducibility-of-research-based-on-twitter-data" class="section level3">
<h3>Reproducibility of research based on Twitter data</h3>
<p>As reproducible results are one of the major requirements of research projects, it has to be discussed how this could affect your work with Twitter data. The Twitter development agreement includes a clear statement of what researchers are allowed to publish along with their work:</p>
<blockquote>
<font size="-1">
“<em>Academic researchers are permitted to distribute an unlimited number of Tweet IDs and/or User IDs if they are doing so on behalf of an academic institution and for the sole purpose of non-commercial research. For example, you are permitted to share an unlimited number of Tweet IDs for the purpose of enabling peer review or validation of your research.</em>”<a href="#fn7" class="footnote-ref" id="fnref7"><sup>7</sup></a>
</font>
</blockquote>
<p>This means that the content of tweets must not be shared publicly. As tweets can be deleted or accounts can be suspended this certainly states an issue for subsequent researchers attempting to replicate the findings as they won’t be able to recrawl such tweets via the API. However, there are also platforms like <a href="https://polititweet.org/">polititweet.org</a>, which track <em>public figures</em> and based on that justify the publication even of deleted tweets:</p>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:unnamed-chunk-3"></span>
<img src="../../../../../article/twitter-research-track/politweet.png" alt="&lt;a href=&quot;https://polititweet.org&quot;&gt;polititweet.org&lt;/a&gt; section of the landing page" width="100%" />
<p class="caption">
Figure 3: <a href="https://polititweet.org">polititweet.org</a> section of the landing page
</p>
</div>
<div style="text-align: right">
<p><sub><sup>
Source: <a href="https://polititweet.org/">polititweet.org landing page</a>
</sub></sup></p>
</div>
<p>To conclude, this makes the decision of how to share what kind of data not easier. But one still has to ensure to choose the best available option to share his or her data without violating the Twitter rules which are as of today to at least share tweet IDs amongst the community.</p>
</div>
<div id="conclusion" class="section level3">
<h3>Conclusion</h3>
<p>This blog post provides a first glimpse into the academic research track Twitter API and the information richness of Twitter data. As there certainly will be further updates and changes to the API in the future, there are plenty of easy-to-use packages that build on active user communities. The community is there to keep the packages updated accordingly to the current Twitter API version. While there exist a lot of powerful packages to tackle the data gathering step, researchers still need to think carefully about how to further process the crawled information depending on their research question and method as well as how to make their research accessible to the community in an <em>open science</em> approach.</p>
</div>
<div id="further-readings" class="section level3">
<h3>Further readings <a name="furtherreadings"></a></h3>
<ul>
<li><a href="https://developer.twitter.com/en/docs">Official Twitter API Documentation</a></li>
<li><a href="https://doi.org/10.1007/978-1-4614-9372-3">Shamanth Kumar, Fred Morstatter, and Huan Liu. 2013. Twitter Data Analytics. Springer Publishing Company, Incorporated.</a></li>
<li><a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/collecting-and-analyzing-twitter-using-r/">Denis Cohen and Simon Kühne. 2019. Collecting and Analyzing Twitter Data Using R. Methods Bites.</a></li>
</ul>
</div>
<div id="about-the-author" class="section level3">
<h3>About the author</h3>
<p>Andreas Küpfer <a href="mailto:andreas.kuepfer@tu-darmstadt.de"><svg aria-hidden="true" role="img" viewBox="0 0 512 512" style="height:1em;width:1em;vertical-align:-0.125em;margin-left:auto;margin-right:auto;font-size:inherit;fill:currentColor;overflow:visible;position:relative;"><path d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"/></svg></a> <a href="https://twitter.com/ankuepfer"><svg aria-hidden="true" role="img" viewBox="0 0 512 512" style="height:1em;width:1em;vertical-align:-0.125em;margin-left:auto;margin-right:auto;font-size:inherit;fill:currentColor;overflow:visible;position:relative;"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/></svg></a> is a graduate of the Mannheim Master in Data Science and a doctoral researcher at the Technical University of Darmstadt. His interdisciplinary research interests include text-as-data, applying machine learning technologies, and substantial inference in the fields of political communication and political competition.</p>
</div>
<div id="references" class="section level3 unnumbered">
<h3>References</h3>
<div id="refs" class="references">
<div id="ref-barberá_2015">
<p>Barberá, Pablo. 2015. “Birds of the Same Feather Tweet Together: Bayesian Ideal Point Estimation Using Twitter Data.” <em>Political Analysis</em> 23 (1): 76–91. <a href="https://doi.org/10.1093/pan/mpu011">https://doi.org/10.1093/pan/mpu011</a>.</p>
</div>
<div id="ref-BarrieHo2021">
<p>Barrie, Christopher, and Justin Chun-ting Ho. 2021. “AcademictwitteR: An R Package to Access the Twitter Academic Research Product Track V2 Api Endpoint.” <em>Journal of Open Source Software</em> 6 (62): 3272. <a href="https://doi.org/10.21105/joss.03272">https://doi.org/10.21105/joss.03272</a>.</p>
</div>
<div id="ref-göbel_munzert_2021">
<p>Göbel, Sascha, and Simon Munzert. 2021. “The Comparative Legislators Database.” <em>British Journal of Political Science</em>, 1–11. <a href="https://doi.org/10.1017/S0007123420000897">https://doi.org/10.1017/S0007123420000897</a>.</p>
</div>
<div id="ref-NGUYEN2021100922">
<p>Nguyen, Thu T., Shaniece Criss, Eli K. Michaels, Rebekah I. Cross, Jackson S. Michaels, Pallavi Dwivedi, Dina Huang, et al. 2021. “Progress and Push-Back: How the Killings of Ahmaud Arbery, Breonna Taylor, and George Floyd Impacted Public Discourse on Race and Racism on Twitter.” <em>SSM - Population Health</em> 15: 100922. <a href="https://doi.org/https://doi.org/10.1016/j.ssmph.2021.100922">https://doi.org/https://doi.org/10.1016/j.ssmph.2021.100922</a>.</p>
</div>
<div id="ref-doi:10.1177/1354068820957960">
<p>Sältzer, Marius. 2022. “Finding the Bird’s Wings: Dimensions of Factional Conflict on Twitter.” <em>Party Politics</em> 28 (1): 61–70. <a href="https://doi.org/10.1177/1354068820957960">https://doi.org/10.1177/1354068820957960</a>.</p>
</div>
<div id="ref-cruz2022">
<p>Valle-Cruz, David, Vanessa Fernandez, Asdrubal Lopez-Chau, and Rodrigo Sandoval Almazan. 2022. “Does Twitter Affect Stock Market Decisions? Financial Sentiment Analysis During Pandemics: A Comparative Study of the H1n1 and the Covid‐19 Periods.” <em>Cognitive Computation</em> 14 (January). <a href="https://doi.org/10.1007/s12559-021-09819-8">https://doi.org/10.1007/s12559-021-09819-8</a>.</p>
</div>
<div id="ref-vanVliet2020TheTP">
<p>Vliet, Livia van, Petter Törnberg, and Justus Uitermark. 2020. “The Twitter Parliamentarian Database: Analyzing Twitter Politics Across 26 Countries.” <em>PLoS ONE</em> 15.</p>
</div>
</div>
</div>
<div class="footnotes">
<hr />
<ol>
<li id="fn1"><p>Meta data serves as a explanatory information such as topical indicators or the language of the tweet which should explain and enrich the actual tweet, image or main object retrieved from the API<a href="#fnref1" class="footnote-back">↩</a></p></li>
<li id="fn2"><p>Queries are filter operators to narrow down the amount of tweets which should be retrieved<a href="#fnref2" class="footnote-back">↩</a></p></li>
<li id="fn3"><p>API stands for Application Programming Interface and allows, simply speaking, the communication between software.<a href="#fnref3" class="footnote-back">↩</a></p></li>
<li id="fn4"><p><a href="https://developer.twitter.com/en/products/twitter-api/academic-research/application-info">Twitter Developer Platform product page</a><a href="#fnref4" class="footnote-back">↩</a></p></li>
<li id="fn5"><p>There is a maximum of tweets which can be retrieved via the API which gets resetted once in a month.<a href="#fnref5" class="footnote-back">↩</a></p></li>
<li id="fn6"><p>Use such lists with caution as they may do not come from verified sources.<a href="#fnref6" class="footnote-back">↩</a></p></li>
<li id="fn7"><p>You can find a detailed description of the content redistribution of Twitter data in the <a href="https://developer.twitter.com/en/developer-terms/policy#:~:text=Academic%20researchers%20are%20permitted%20to,purpose%20of%20non%2Dcommercial%20research.">official developer policies</a>.<a href="#fnref7" class="footnote-back">↩</a></p></li>
</ol>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Advancing Text Mining with R and quanteda</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/</link>
      <pubDate>Thu, 17 Oct 2019 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/</guid>
      <description><![CDATA[
        
<script src="/rmarkdown-libs/header-attrs/header-attrs.js"></script>
<script src="/rmarkdown-libs/kePrint/kePrint.js"></script>


<p>Everyone is talking about text analysis. Is it puzzling that this data source is so popular right now? Actually no. Most of our datasets rely on (hand-coded) textual information. Extracting, processing, and analyzing this oasis of information becomes increasingly relevant for a large variety of research fields. This Methods Bites Tutorial by <a href="https://twitter.com/cosima_meyer">Cosima Meyer</a> summarizes <a href="http://cbpuschmann.net">Cornelius Puschmann</a>’s workshop in the <a href="https://www.mzes.uni-mannheim.de/d7/">MZES</a> Social Science Data Lab in January 2019 on <strong>advancing text mining with R and the package <code>quanteda</code></strong>. The workshop offered guidance through the use of <code>quanteda</code> and covered various classification methods, including classification with <a href="#knowncategories">known categories (dictionaries and supervised machine learning)</a> and with <a href="#unknowncategories">unknown categories (unsupervised machine learning)</a>.
<!-- This post does not cover cross-validation. --></p>
<blockquote>
<p><em>This post was updated in December 2020 to be consistent with quanteda’s version 2.1.2. For more information on differences between quanteda versions, have a look at <a href="https://blog.quanteda.org/2020/02/27/whats-new-in-quanteda-version-2.0/">this excellent overview</a>.</em></p>
</blockquote>
<div id="overview" class="section level3">
<h3>Overview</h3>
<ol style="list-style-type: decimal">
<li><a href="#whyquanteda"><strong>What is quanteda?</strong></a></li>
<li><a href="#usequanteda"><strong>How do we use quanteda?</strong></a>
<!-- 1. [Calculate the DFM](#dfm) --></li>
<li><a href="#classification"><strong>Classification</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#knowncategories"><strong>Known categories</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#dictionaries">Dictionaries</a></li>
<li><a href="#supervised">Supervised machine learning</a>
<ol style="list-style-type: decimal">
<li><a href="#nb">Naive Bayes (NB)</a></li>
</ol></li>
</ol></li>
<li><a href="#unknowncategories"><strong>Unknown categories</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#unsupervised">Unsupervised machine learning</a>
<ol style="list-style-type: decimal">
<li><a href="#lsa">Latent semantic analysis (LSA)</a></li>
<li><a href="#lda">Latent Dirichlet Allocation (LDA)</a></li>
<li><a href="#stm">Structural topic models (STM)</a></li>
</ol></li>
</ol></li>
</ol></li>
<li><a href="#furtherreadings"><strong>Further readings</strong></a></li>
</ol>
<p>This blog post is based on <a href="http://cbpuschmann.net/quanteda_mzes/">this report</a> and on <a href="http://inhaltsanalyse-mit-r.de/themenmodelle.html">Cornelius’ post on topic models in R</a>.</p>
</div>
<div id="what-is-quanteda" class="section level3">
<h3>What is quanteda? <a name="whyquanteda"></a></h3>
<p>In order to analyze text data, R has several packages available. In this blog post we focus on <code>quanteda</code>. <code>quanteda</code> is one of the most popular R packages for the <strong>qu</strong>antitative <strong>an</strong>alysis of <strong>te</strong>xtual <strong>da</strong>ta that is <a href="http://quanteda.io">fully-featured and allows the user to easily perform natural language processing tasks. It was originally developed by Ken Benoit and other contributors</a>. It offers an extensive documentation and is regularly updated. <code>quanteda</code> is most useful for preparing data that can then be further analyzed using unsupervised/supervised machine learning or other techniques. A combination with <code>tidyverse</code> leads to a more transparent code structure and offers a mere variety of useful areas that could not be addressed within the limited time of the workshop (e.g., scaling models, part-of-speech (POS) tagging, named entities, word embeddings, etc.).</p>
<p>There are also similar R packages such as <code>tm</code>, <code>tidytext</code>, and <code>koRpus</code>. <a href="https://cran.r-project.org/web/packages/tm/index.html"><code>tm</code></a> has simpler grammer but slightly fewer features, <a href="https://cran.r-project.org/web/packages/tidytext/index.html"><code>tidytext</code></a> is very closely integrated with <code>dplyr</code> and well-documented, and <a href="https://cran.r-project.org/web/packages/koRpus/index.html"><code>koRpus</code></a> is good for tasks such as <a href="https://reaktanz.de/R/pckg/koRpus/koRpus_vignette.html">part-of-speech (POS) tagging</a>).</p>
</div>
<div id="how-do-we-use-quanteda" class="section level3">
<h3>How do we use quanteda? <a name="usequanteda"></a></h3>
<p>Most analyses in quanteda require three steps:</p>
<blockquote>
<p><sub><b> 1. Import the data </b></sub></br></p>
</blockquote>
<p>The data that we usually use for text analysis is available in text formats (e.g., .txt or .csv files).</p>
<blockquote>
<p><sub><b> 2. Build a corpus </b></sub></br></p>
</blockquote>
<p>After reading in the data, we need to generate a <strong>corpus</strong>. A corpus is a type of dataset that is used in text analysis. It contains “a collection of text or speech material that has been brought together according to a certain set of predetermined criteria” <a href="https://www.igi-global.com/book/automated-systems-aviation-aerospace-industries/209468">(Shmelova et al. 2019, p. 33)</a>. These criteria are usually set by the researchers and are in concordance with the guiding question. For instance, if you are interested in analyzing speeches in the UN General Debate, these predetermined criteria are the time and scope conditions of these debates (speeches by countries at different points in time).</p>
<blockquote>
<p><sub><b> 3. Calculate a document-feature matrix (DFM) </b></sub></p>
</blockquote>
<p>Another essential component for text analysis is a <strong>document-feature matrix (DFM)</strong>; also called <strong>document-term matrix (DTM)</strong>. These two terms are synonyms but <code>quanteda</code> refers to a DFM whereas others will refer to DTM. It describes how frequently terms occur in the corpus by counting single terms.
To generate a DFM, we first split the text into its single terms (tokens). We then count how frequently each term (token) occurs in each document.</p>
<p>The following graphic describes visually how we turn raw text into a vector-space representation that is easily accessible and analyzable with quantitative statistical tools. It also visualizes how we can think of a DFM. The rows represent the documents that are part of the corpus and the columns show the different terms (tokens). The values in the cells indicate how frequently these terms (tokens) are used across the documents.</p>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-3"></span>
<img src="/../../../../article/advancing-text-mining/figures/dfm.png" alt="Model of a DFM" width="70%" />
<p class="caption">
Figure 1: Model of a DFM
</p>
</div>
<p>       </p>
<p><strong>Important things to remember about DFMs:</strong></p>
<ul>
<li>A <strong>corpus is positional (string of words)</strong> and a <strong>DFM is non-positional (bag of words)</strong>. Put differently, the order of the words matters in a corpus whereas a DFM does not have information on the position of words.</li>
<li>A <strong>token</strong> is each individual word in a text (but it could also be a sentence, paragraph, or character). This is why we call creating a “bag of words” also <strong>tokenizing text</strong>. In a nutshell, a DFM is a very efficient way of organizing the frequency of features/tokens but does not contain any information on their position. In our example, the <strong>features</strong> of a text are represented by the columns of a DFM and aggregate the frequency of each <strong>token</strong>.</li>
<li>In most projects you want <strong>one corpus to contain all your data</strong> and <strong>generate many DFMs</strong> from that.</li>
<li>The <strong>rows of a DFM</strong> can contain <strong>any unit</strong> on which you can <strong>aggregate documents</strong>. In the example above, we used the single documents as the unit. It may also well be more fine-grained with sub-documents or more aggregated with a larger collection of documents.</li>
<li>The <strong>columns of a DFM</strong> are <strong>any unit</strong> on which you can <strong>aggregate features</strong>. <strong>Features</strong> are extracted from the texts and quantitatively measurable. Features can be words, parts of the text, content categories, word counts, etc. In the example above, we used single words such as “united”, “nations”, and “peace”.</li>
</ul>
<p>       </p>
<p>To showcase the three steps introduced above, we are using the <a href="https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/0TJX8Y">UN General Debate data by Mikhaylov, Baturo, and Dasandi</a> dataset. There is also a <a href="https://rdrr.io/github/quanteda/quanteda.corpora/man/data_corpus_ungd2017.html">pre-processed version of the dataset</a> accessible with <code>quanteda.corpora</code>.</p>
<br/>
<details>
<p><summary>How to access the UNGD data with <code>quanteda.corpora</code></summary></p>
<pre class="r"><code># Install package quanteda.corpora
devtools::install_github(&quot;quanteda/quanteda.corpora&quot;)

# Load the dataset
quanteda.corpora::data_corpus_ungd2017</code></pre>
</details>
<p><br/></p>
<p>We will, however, mainly rely on the original dataset throughout the following explanations to match closely the regular workflow of textual data in R. If you want to replicate the steps, please download the data <a href="https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/0TJX8Y">here</a> and unzip the zip file. Your <code>global_path</code> should direct you to the text file folders.</p>
<p>In a first step, we need to load the necessary packages and read in the data.</p>
<pre class="r"><code># Load all required packages
library(tidyverse)        # Also loads dplyr, ggplot2, and haven
library(quanteda)         # For NLP
library(readtext)         # To read .txt files
library(stm)              # For structural topic models
library(stminsights)      # For visual exploration of STM
library(wordcloud)        # To generate wordclouds
library(gsl)              # Required for the topicmodels package
library(topicmodels)      # For topicmodels
library(caret)            # For machine learning

# Download data here: 
# https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/0TJX8Y 
# and unzip the zip file

# Read in data (.txt files)
global_path &lt;- &quot;path/to/folder/UN-data/&quot;

# We load the data (.txt files) from all subfolders (readtext can handle 
# this without specification)  and store them in the main UNGDspeeches 
# dataframe. Beyond the speech text, this data also includes the 
# meta-data from the text filenames and add variables for the country, 
# UN session, and year.
# The code is based on https://github.com/quanteda/quanteda.corpora/issues/6
# and https://github.com/sjankin/UnitedNations/blob/master/files/UNGD_analysis_example.Rmd 

# For the purpose of this blog post, we use the data from all sessions.
UNGDspeeches &lt;- readtext(
  paste0(global_path, &quot;*/*.txt&quot;),
  docvarsfrom = &quot;filenames&quot;,
  docvarnames = c(&quot;country&quot;, &quot;session&quot;, &quot;year&quot;),
  dvsep = &quot;_&quot;,
  encoding = &quot;UTF-8&quot;
)</code></pre>
<p>We can then proceed and generate a corpus.</p>
<pre class="r"><code>mycorpus &lt;- corpus(UNGDspeeches)

# Assigns a unique identifier to each text
docvars(mycorpus, &quot;Textno&quot;) &lt;-
  sprintf(&quot;%02d&quot;, 1:ndoc(mycorpus)) </code></pre>
<p>As we can see (by calling the object <code>mycorpus</code>), the corpus consists of 8,093 documents.</p>
<details>
<p><summary>Output: <code>mycorpus</code></summary></p>
<pre class="r"><code>mycorpus</code></pre>
<pre><code>Corpus consisting of 8,093 documents and 4 docvars.</code></pre>
</details>
<p><br/></p>
<p>With this data, we can already generate first descriptive statistics.</p>
<pre class="r"><code># Save statistics in &quot;mycorpus.stats&quot;
mycorpus.stats &lt;- summary(mycorpus)

# And print the statistics of the first 10 observations
head(mycorpus.stats, n = 10)</code></pre>
<pre><code>#               Text Types Tokens Sentences country session year Textno
# 1  ALB_25_1970.txt  1727   9077       256     ALB      25 1970     01
# 2  ARG_25_1970.txt  1425   5192       218     ARG      25 1970     02
# 3  AUS_25_1970.txt  1611   5688       270     AUS      25 1970     03
# 4  AUT_25_1970.txt  1340   4717       164     AUT      25 1970     04
# 5  BEL_25_1970.txt  1289   4783       207     BEL      25 1970     05
# 6  BLR_25_1970.txt  1427   6138       204     BLR      25 1970     06
# 7  BOL_25_1970.txt  1559   5612       225     BOL      25 1970     07
# 8  BRA_25_1970.txt  1333   4422       154     BRA      25 1970     08
# 9  CAN_25_1970.txt   728   1887        97     CAN      25 1970     09
# 10 CMR_25_1970.txt   928   3144       106     CMR      25 1970     10</code></pre>
<!-- ##### Calculate the DFM <a name="dfm"></a> -->
<p>In a next step, we can also calculate the <strong>document-feature matrix</strong>. To do so, first we need to generate tokens (<code>tokens()</code>) and can also already <a href="https://github.com/sjankin/UnitedNations/blob/master/files/UNGD_analysis_example.Rmd"><strong>pre-process the data</strong></a>. This includes removing the numbers (<code>remove_numbers</code>), punctuations (<code>remove_punct</code>), symbols (<code>remove_symbols</code>), and urls beginning with http(s) (<code>remove_url</code>).</p>
<p>An earlier version of this blog post used <code>remove_hyphens</code> to remove hyphens as well as <code>remove_twitter</code>to remove symbols such as @ and #. Both commands are either deprecated or defunctional. To remove hyphens, it is recommended to use <code>split_hyphens</code> instead. For twitter symbols there is no new command. Quanteda’s manual recommends to use <a href="https://quanteda.io/reference/tokens.html">``an alternative tokenizer, including non-quanteda options’’</a>.</p>
<p>We further include the docvars from our corpus (<code>include_docvars</code>).</p>
<pre class="r"><code># Preprocess the text

# Create tokens
token &lt;-
  tokens(
    mycorpus,
    split_hyphens = TRUE,
    remove_numbers = TRUE,
    remove_punct = TRUE,
    remove_symbols = TRUE,
    remove_url = TRUE,
    include_docvars = TRUE
  )</code></pre>
<p>Since the pre-1994 documents were scanned with OCR scanners, several tokens with combinations of digits and characters were introduced. We clean them manually following <a href="https://github.com/sjankin/UnitedNations/blob/master/files/UNGD_analysis_example.Rmd">this guideline</a>.</p>
<pre class="r"><code># Clean tokens created by OCR
token_ungd &lt;- tokens_select(
  token,
  c(&quot;[\\d-]&quot;, &quot;[[:punct:]]&quot;, &quot;^.{1,2}$&quot;),
  selection = &quot;remove&quot;,
  valuetype = &quot;regex&quot;,
  verbose = TRUE
)</code></pre>
<p>In the next step, we then create the <strong>document-feature matrix</strong>. We lower and stem the words (<code>tolower</code> and <code>stem</code>) and remove common stop words (<code>remove=stopwords()</code>). Stopwords are words that appear in texts but do not give the text a substantial meaning (e.g., “the”, “a”, or “for”). Since the language of all documents is English, we only remove English stopwords here. <code>quanteda</code> can also deal with stopwords from other languages (for more information see <a href="https://quanteda.io/reference/stopwords.html">here</a>).</p>
<pre class="r"><code>mydfm &lt;- dfm(token_ungd,
             tolower = TRUE,
             stem = TRUE,
             remove = stopwords(&quot;english&quot;)
             )</code></pre>
<p>We can also trim the text with <code>dfm_trim</code>. Using the command and its respective specifications, we filter words that appear less than 7.5% and more than 90%. This rather conservative approach is possible because we have a sufficiently large corpus.</p>
<pre class="r"><code>mydfm.trim &lt;-
  dfm_trim(
    mydfm,
    min_docfreq = 0.075,
    # min 7.5%
    max_docfreq = 0.90,
    #  max 90%
    docfreq_type = &quot;prop&quot;
  ) </code></pre>
<p>To get a look at the DFM, we now print their first 5 observations and first 10 features:</p>
<pre class="r"><code># And print the results of the first 10 observations and first 10 features in a DFM
head(dfm_sort(mydfm.trim, decreasing = TRUE, margin = &quot;both&quot;),
     n = 10,
     nf = 10) </code></pre>
<pre><code>Document-feature matrix of: 5 documents, 10 features (4.0% sparse) and 4 docvars.
                 features
docs              problem session conflict council africa global resolut hope south situat
  CUB_34_1979.txt      36       8        1       0     13      3      10    8    10     23
  IRL_39_1984.txt      41       9       18      11     14      5      16   21    19      9
  PAN_37_1982.txt      14      12       12       8     11      2       6   11    20     10
  BFA_29_1974.txt      25      17        1       4     15      0      10   20     6      9
  GRC_43_1988.txt      27       9       13      14     10      2      10   10    11      5</code></pre>
<!-- ``` -->
<!-- ## Document-feature matrix of: 10 documents, 10 features (3.0% sparse). -->
<!-- ## 10 x 10 sparse Matrix of class "dfm" -->
<!-- ##                  features -->
<!-- ## docs              human government organization rights political session council africa -->
<!-- ##   CUB_34_1979.txt    11         27            3      8        12       8       0     12 -->
<!-- ##   IRL_39_1984.txt    24         15            8     22        22       9      11     14 -->
<!-- ##   BFA_29_1974.txt     2         18           11      6        15      17       4     15 -->
<!-- ##   PAN_37_1982.txt     8         15           14      7        22       9       8     11 -->
<!-- ##   GRC_43_1988.txt    17          3            7     14        20       9      14     10 -->
<!-- ##   PRY_38_1983.txt     8         20           12      8        14      17       3      0 -->
<!-- ##   UGA_30_1975.txt    16         16           17     10        10      17       7     54 -->
<!-- ##   RUS_32_1977.txt     6          2            2      5         8       8       1     12 -->
<!-- ##   RUS_31_1976.txt     4          2            3      8        14       9       2      8 -->
<!-- ##   ALB_28_1973.txt     0         22            4      8        10       4       1      5 -->
<!-- ##                  features -->
<!-- ## docs              developing time -->
<!-- ##   CUB_34_1979.txt         69    8 -->
<!-- ##   IRL_39_1984.txt         19    8 -->
<!-- ##   BFA_29_1974.txt         33   24 -->
<!-- ##   PAN_37_1982.txt          5    7 -->
<!-- ##   GRC_43_1988.txt         11    6 -->
<!-- ##   PRY_38_1983.txt          7   15 -->
<!-- ##   UGA_30_1975.txt          6    8 -->
<!-- ##   RUS_32_1977.txt          7   12 -->
<!-- ##   RUS_31_1976.txt          6   10 -->
<!-- ##   ALB_28_1973.txt          3   18 -->
<!-- ``` -->
<p>The sparsity gives us information about the proportion of cells that have zero counts.</p>
</div>
<div id="classification" class="section level3">
<h3>Classification <a name="classification"></a></h3>
<p>A next step can involve the classification of the text. The article by <a href="https://www.cambridge.org/core/journals/political-analysis/article/text-as-data-the-promise-and-pitfalls-of-automatic-content-analysis-methods-for-political-texts/F7AAC8B2909441603FEB25C156448F20">Grimmer and Stewart (2013)</a> provides a good overview for this step. The upcoming section follows their structure. Classification sorts texts into categories. The following picture is leaned on the figure by <a href="https://www.cambridge.org/core/journals/political-analysis/article/text-as-data-the-promise-and-pitfalls-of-automatic-content-analysis-methods-for-political-texts/F7AAC8B2909441603FEB25C156448F20">Grimmer and Stewart (2013, 268)</a> and illustrates a possible structure of classification.</p>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-14"></span>
<img src="/../../../../article/advancing-text-mining/figures/overview.png" alt="Overview of classification (own illustration, based on [Grimmer and Stewart (2013, 268)](https://www.cambridge.org/core/journals/political-analysis/article/text-as-data-the-promise-and-pitfalls-of-automatic-content-analysis-methods-for-political-texts/F7AAC8B2909441603FEB25C156448F20))" width="70%" />
<p class="caption">
Figure 2: Overview of classification (own illustration, based on <a href="https://www.cambridge.org/core/journals/political-analysis/article/text-as-data-the-promise-and-pitfalls-of-automatic-content-analysis-methods-for-political-texts/F7AAC8B2909441603FEB25C156448F20">Grimmer and Stewart (2013, 268)</a>)
</p>
</div>
<p>       </p>
<p>A researcher usually faces one of the following situations: <strong>The categories are known beforehand</strong> or <strong>the categories are unknown</strong>. If the researcher knows the categories, s/he can use automated methods to minimize the workload that is associated with the categorization of the texts. Throughout the workshop, two methods were presented: a <strong>dictionary method</strong> and a <strong>supervised method</strong>. If the researcher does not know the categories, s/he is likely to resort to <strong>unsupervised machine learning</strong>. The following section provides illustrative examples for both methods.</p>
<div id="known-categories" class="section level4">
<h4>Known categories <a name="knowncategories"></a></h4>
<div id="known-categories-dictionaries" class="section level5">
<h5>Known categories: Dictionaries <a name="dictionaries"></a></h5>
<p><strong>Dictionaries</strong> contain lists of words that correspond to different categories. If we apply a dictionary approach, we count how often words that are associated with different categories are represented in each document. These dictionaries help us to classify (or categorize) the speeches based on the frequency of the words that they contain. Popular dictionaries are sentiment dictionaries (such as <a href="https://www.cs.uic.edu/~liub/FBS/sentiment-analysis.html">Bing</a>, <a href="http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6010">Afinn</a> or <a href="http://liwc.wpengine.com">LIWC</a>) or <a href="http://www.lexicoder.com/index.html">LexiCoder</a>.
<!-- [You may also want to normalize the length of the document.](http://pablobarbera.com/big-data-upf/slides/05-text.pdf) --></p>
<p>We use the “LexiCoder Policy Agenda” dictionary that can be accessed <a href="http://www.lexicoder.com/download.html">here</a> in a .lcd format.
The “LexiCoder Policy Agenda” dictionary captures major topics from the <a href="https://www.comparativeagendas.net">comparative Policy Agenda project</a> and is currently available in Dutch and English.</p>
<p>To read in the dictionary, we use <code>quanteda</code>’s built-in function <code>dictionary()</code>.</p>
<pre class="r"><code># load the dictionary with quanteda&#39;s built-in function
dict &lt;- dictionary(file = &quot;policy_agendas_english.lcd&quot;)</code></pre>
<p>We apply this dictionary to filter the share of each country’s speeches on immigration, international affair and defence.</p>
<pre class="r"><code>mydfm.un &lt;- dfm(mydfm.trim, groups = &quot;country&quot;, dictionary = dict)

un.topics.pa &lt;- convert(mydfm.un, &quot;data.frame&quot;) %&gt;%
  dplyr::rename(country = doc_id) %&gt;%
  select(country, immigration, intl_affairs, defence) %&gt;%
  tidyr::gather(immigration:defence, key = &quot;Topic&quot;, value = &quot;Share&quot;) %&gt;%
  group_by(country) %&gt;%
  mutate(Share = Share / sum(Share)) %&gt;%
  mutate(Topic = haven::as_factor(Topic))</code></pre>
<p>In a next step, we can visualize the results with <code>ggplot</code>. This gives us a first impression of the distribution of the topics in the 2018 UN General Debate across countries.</p>
<pre class="r"><code>un.topics.pa %&gt;%
  ggplot(aes(country, Share, colour = Topic, fill = Topic)) +
  geom_bar(stat = &quot;identity&quot;) +
  scale_colour_brewer(palette = &quot;Set1&quot;) +
  scale_fill_brewer(palette = &quot;Pastel1&quot;) +
  ggtitle(&quot;Distribution of PA topics in the UN General Debate corpus&quot;) +
  xlab(&quot;&quot;) +
  ylab(&quot;Topic share (%)&quot;) +
  theme(axis.text.x = element_blank(),
        axis.ticks.x = element_blank())</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-18"></span>
<img src="/../../../../article/advancing-text-mining/figures/distribution_topics_un.png" alt="Distribution of PA topics in the UN General Debate corpus" width="70%" />
<p class="caption">
Figure 3: Distribution of PA topics in the UN General Debate corpus
</p>
</div>
<p><br/></p>
<p>We observe a relatively high share for both defence and international affairs whereas immigration receives fewer attention in the speeches.</p>
</div>
<div id="known-categories-supervised-machine-learning---naive-bayes-nb" class="section level5">
<h5>Known categories: Supervised machine learning - Naive Bayes (NB) <a name="nb"></a></h5>
<p>We now turn to <strong>supervised machine learning</strong>. Similar to the dictionary approach explained above, <strong>this method also requires some pre-existing classifications</strong>. But in contrast to a dictionary, we now divide the data into a <strong>training</strong> and a <strong>test dataset</strong>. This follows the general logic of machine learning algorithms. The training data already contains the classifications and <em>trains</em> the algorithm (e.g., our Naive Bayes classifier) to predict the class of our speech based on the features that are given. A Naive Bayes classifier now calculates the probability for each class based on the features. It eventually goes for the class with the highest probability and selects this class as the corresponding category. It is <strong>based on the Bayes theorem for conditional probability</strong>. It can be formally written as:
<span class="math display">\[ P(A | B) = \frac{P(A) * P(B | A)}{P(B)}\]</span>
In plain words, the probability of A is conditional on B.</p>
<br/>
<details>
<p><summary>Bayes’ theorem</summary></p>
<ul>
<li><span class="math inline">\(A\)</span> and <span class="math inline">\(B\)</span> are events</li>
<li><span class="math inline">\(P(A)\)</span> and <span class="math inline">\(P(B)\)</span> is the probability of observing <span class="math inline">\(A\)</span> and <span class="math inline">\(B\)</span> (respectively) independent from each other</li>
<li><span class="math inline">\(P(A) \neq 0\)</span> and <span class="math inline">\(P(B) \neq 0\)</span></li>
<li><span class="math inline">\(P(A|B)\)</span> is the <strong>conditional probability</strong> that <span class="math inline">\(A\)</span> occurs when <span class="math inline">\(B\)</span> is true
<span class="math display">\[P(A|B) = \frac{P(A \cap B)}{P(B)}, if P(B) \neq 0\]</span></li>
<li><span class="math inline">\(P(B|A)\)</span> is the <strong>conditional probability</strong> that <span class="math inline">\(B\)</span> occurs when <span class="math inline">\(A\)</span> is true
<span class="math display">\[P(B|A) = \frac{P(B \cap A)}{P(A)}, if P(A) \neq 0\]</span></li>
<li>And we also have the <strong>joint probability</strong> of $ P(A B) = P(B A)$ because</li>
</ul>
<span class="math display">\[ \Longrightarrow P(A \cap B) = P(A|B)P(B) = P(B|A)P(A)\]</span>
<span class="math display">\[ \Longrightarrow P(A | B) = \frac{P(A \cap B)}{P(B)}\]</span>
<span class="math display">\[ \Longrightarrow P(A | B) = \frac{\frac{P(A \cap B)}{P(B)}*P(A)}{P(B)}\]</span>
<span class="math display">\[ \Longrightarrow P(A | B) = \frac{P(B|A)*P(A)}{P(B)}\]</span>
</details>
<p><br/></p>
<p><strong>Why is Naive Bayes “naive”?</strong> Naive Bayes is “naive” because of its <strong>strong independence assumptions</strong>. It assumes that all features are equally important and that all features are independent. If you think of n-grams and compare unigrams and bigrams, you can intuitively understand why the last assumption is a strong assumption. A unigram counts each word as a gram (“I” “like” “walking” “in” “the” “sun”) whereas a bigram counts two words as a gram (“I like” “like walking” “walking in” “in the” “the sun”).</p>
<p>However, even when the assumptions are not fully met, Naive Bayes still performs well.</p>
<p>A Naive Bayes is a <strong>relatively simple classification algorithm</strong> because it does not require much time and working capacity of your machine. To use a Naive Bayes classifier, we rely on <code>quanteda</code>’s built-in function <a href="http://quanteda.io/reference/textmodel_nb.html"><code>textmodel_nb</code></a>.</p>
<p>To perform the Naive Bayes estimation, we proceed with the following steps:</p>
<blockquote>
<p><sub><strong>1. We set up training and test data based on the corpus.</strong></sub></br>
<sub><strong>2. Based on these two datasets, we generate a DFM.</strong></sub></br>
<sub><strong>3. We train the algorithm by feeding in the training data and eventually use the test data for performance.</strong></sub></br>
<sub><strong>4. We then check the performance (accuracy) of our results.</strong></sub></br>
<sub><strong>5. And compare it with a random prediction.</strong></sub></br></p>
</blockquote>
<p>For this example, we use the pre-labeled dataset that is used for the algorithm <a href="https://github.com/koheiw/Newsmap">newsmap</a> by <a href="https://koheiw.net/?p=293">Kohei Watanabe</a>. The dataset contains information on the geographical location of newspaper articles. We introduce this new dataset as Naive Bayes – a supervised machine learning algorithm – requires pre-labeled data.</p>
<p>We first load the dataset. To do so, we follow Kohei Watanabe’s description <a href="https://github.com/koheiw/Newsmap">here</a>, download the <a href="https://www.dropbox.com/s/e19kslwhuu9yc2z/yahoo-news.RDS?dl=1">corpus of Yahoo News from 2014</a>, and follow the subsequent processing steps he describes.</p>
<pre class="r"><code># load data
load(&quot;../newspaper.RData&quot;)

# transform variables
pred_data$text &lt;- as.character(pred_data$text)
pred_data$country &lt;- as.character(pred_data$country)</code></pre>
<p>For simplicity, we keep only the USA, Great Britain, France, Brazil, and Japan.</p>
<pre class="r"><code>pred_data &lt;- pred_data %&gt;%
  dplyr::filter(country %in% c(&quot;us&quot;, &quot;gb&quot;, &quot;fr&quot;, &quot;br&quot;, &quot;jp&quot;)) %&gt;%
                  dplyr::select(text, country)</code></pre>
<pre class="r"><code>head(pred_data)</code></pre>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:right;">
row
</th>
<th style="text-align:left;">
text
</th>
<th style="text-align:left;">
country
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right;">
1
</td>
<td style="text-align:left;">
’08 French champ Ivanovic loses to Safarova in 3rd. PARIS (AP) - Former French Open champion Ana Ivanovic lost in the third round Saturday, beaten 6-3, 6-3 by 23rd-seeded Lucie Safarova of the Czech Republic.
</td>
<td style="text-align:left;">
fr
</td>
</tr>
<tr>
<td style="text-align:right;">
2
</td>
<td style="text-align:left;">
Up to USD1,000 a day to care for child migrants. More than 57,000 unaccompanied children, mostly from Central America, have been caught entering the country illegally since last October, and President Barack Obama has asked for USD3.7 billion in emergency funding to address what he has called an ‘urgent humanitarian solution.’ ‘One of the figures that sticks in everybody’s mind is we’re paying about USD250 to USD1,000 per child,’ Senator Jeff Flake told reporters, citing figures presented at a closed-door briefing by Homeland Security Secretary Jeh Johnson. Federal authorities are struggling to find more cost-effective housing, medical care, counseling and legal services for the undocumented minors. The base cost per bed was USD250 per day, including other services, Senator Dianne Feinstein said, without providing details.
</td>
<td style="text-align:left;">
us
</td>
</tr>
<tr>
<td style="text-align:right;">
3
</td>
<td style="text-align:left;">
1,400 gay weddings in England, Wales in first three months. Just over 1,400 gay couples tied the knot in the three months after same-sex marriage was allowed in England and Wales, figures out Thursday showed. The Office for National Statistics said 1,409 marriages took place between March 29 and June 30. ‘The novelty and significance of marriage becoming available led to an initial rush among same-sex couples wanting to be among the very first to assume the same rights and protection afforded to heterosexual couples,’ said James Brown, a partner at law firm JMW Solicitors. The figures will likely surge from December once civil partnerships can be converted into marriages.
</td>
<td style="text-align:left;">
gb
</td>
</tr>
<tr>
<td style="text-align:right;">
4
</td>
<td style="text-align:left;">
1 dead after fan fighting in Brazil. SAO PAULO (AP) - Police say a 21-year-old man died after a confrontation between rival football fan groups in Brazil on Sunday.
</td>
<td style="text-align:left;">
br
</td>
</tr>
<tr>
<td style="text-align:right;">
5
</td>
<td style="text-align:left;">
1 dead as plane with French tourists crashes in US. PAGE, Arizona (AP) - Authorities say a small plane carrying French tourists crashed while trying to land at an airport in Arizona, and one person was killed and another hospitalized.
</td>
<td style="text-align:left;">
fr
</td>
</tr>
<tr>
<td style="text-align:right;">
6
</td>
<td style="text-align:left;">
1 US theory is someone diverted missing plane. WASHINGTON (AP) - A U.S. official says investigators are examining the possibility that someone caused the disappearance of a Malaysia Airlines jet with 239 people on board, and that it may have been ‘an act of piracy.’
</td>
<td style="text-align:left;">
us
</td>
</tr>
</tbody>
</table>
<p>We pre-process the data again. Our final corpus thus includes the newspaper headlines by country.</p>
<pre class="r"><code>data_corpus &lt;- corpus(pred_data, text_field = &quot;text&quot;)</code></pre>
<p>In a first step, we need to define our training and our test dataset. Based on these two datasets, we generate a DFM. This code is based on <a href="http://cbpuschmann.net/quanteda_mzes/">Cornelius code</a> and <a href="https://tutorials.quanteda.io/machine-learning/nb/">quanteda’s example</a>. To do so, we apply similar general data pre-processing steps as discussed above.</p>
<pre class="r"><code># Set a seed for replication purposes
set.seed(68159)

# Generate random 10,000 numbers without replacement
training_id &lt;- sample(1:29542, 10000, replace = FALSE)

# Create docvar with ID
docvars(data_corpus, &quot;id_numeric&quot;) &lt;- 1:ndoc(data_corpus)

# Get training set
dfmat_training &lt;-
  corpus_subset(data_corpus, id_numeric %in% training_id) %&gt;%
  dfm(stem = TRUE)

# Get test set (documents not in training_id)
dfmat_test &lt;-
  corpus_subset(data_corpus,!id_numeric %in% training_id) %&gt;%
  dfm(stem = TRUE)</code></pre>
<p>We can now check the distribution of the countries across the two DFMs:</p>
<pre class="r"><code>print(prop.table(table(docvars(
  dfmat_training, &quot;country&quot;
))) * 100)</code></pre>
<pre><code>   br    fr    gb    jp    us 
13.34 14.37 33.65 11.10 27.54 </code></pre>
<pre class="r"><code>print(prop.table(table(docvars(
  dfmat_test, &quot;country&quot;
))) * 100)</code></pre>
<pre><code>   br       fr       gb       jp       us 
13.70893 15.13151 33.02630 10.93542 27.19783 </code></pre>
<p>As we can see, the countries are equally distributed across both DFMs.</p>
<p>In a next step, we train the Naive Bayes classifier. Going back to the formula stated above, we know that A is conditional on B.</p>
<p><span class="math display">\[ P(A | B) = \frac{P(A) * P(B | A)}{P(B)}\]</span></p>
<p><strong>A is what we want to know</strong> (the country that is mainly addressed in each text) and <strong>B is what we see</strong> (the text). We can now proceed and replace A and B with the respective terms. This leads us to the next equation:</p>
<p><span class="math display">\[ P(Country | Text) = \frac{P(Country) * P(Text | Country)}{P(Text)}\]</span></p>
<p>We can then proceed and train our algorithm using quanteda’s built-in function <code>textmodel_nb</code>.</p>
<pre class="r"><code># Train naive Bayes
# The function takes a DFM as the first argument 
model.NB &lt;-
  textmodel_nb(dfmat_training, docvars(dfmat_training, &quot;country&quot;), prior = &quot;docfreq&quot;)

# The prior indicates an assumed distribution. 
# Here we choose how frequently the categories occur in our data.</code></pre>
<pre class="r"><code>dfmat_matched &lt;-
  dfm_match(dfmat_test, features = featnames(dfmat_training))</code></pre>
<p>The command <code>summary(model.NB)</code> gives us the results of our prediction. Click unfold to see the results.</p>
<br/>
<details>
<p><summary>Code: <code>summary(model.NB)</code></summary></p>
<pre class="r"><code>summary(model.NB)</code></pre>
<pre><code>Call:
textmodel_nb.dfm(x = dfmat_training, y = docvars(dfmat_training, 
    &quot;country&quot;), prior = &quot;docfreq&quot;)

Class Priors:
(showing first 5 elements)
    br     fr     gb     jp     us 
0.1334 0.1437 0.3365 0.1110 0.2754 

Estimated Feature Scores:
         &#39;     08   french   champ  ivanov    lose      to safarova     in     3rd      .     pari
br 0.08949 0.1679 0.007791 0.07636 0.08195 0.08707 0.09989   0.1474 0.1214 0.34322 0.1176 0.008863
fr 0.14212 0.2778 0.940061 0.37912 0.20345 0.16469 0.13803   0.3659 0.1473 0.24345 0.1296 0.947706
gb 0.43332 0.1532 0.032008 0.24397 0.14963 0.49395 0.35845   0.1346 0.3540 0.17905 0.3321 0.032363
jp 0.06507 0.1184 0.004396 0.10771 0.28900 0.06580 0.10287   0.1040 0.1007 0.06916 0.1059 0.002778
us 0.27000 0.2827 0.015743 0.19285 0.27598 0.18850 0.30077   0.2482 0.2766 0.16512 0.3148 0.008290
         (      ap       )      -  former    open champion     ana    lost    the   third   round
br 0.16080 0.24091 0.16100 0.1611 0.09034 0.17116  0.12708 0.08142 0.08045 0.1101 0.08964 0.30833
fr 0.14221 0.14820 0.14219 0.1363 0.17312 0.18636  0.24110 0.13475 0.16643 0.1350 0.17178 0.19535
gb 0.34860 0.28303 0.34870 0.3403 0.45720 0.36654  0.51497 0.07433 0.38923 0.3603 0.39194 0.24189
jp 0.09123 0.08125 0.09126 0.0922 0.04583 0.07443  0.05684 0.22969 0.07943 0.1005 0.07653 0.04757
us 0.25715 0.24661 0.25685 0.2702 0.23351 0.20152  0.06002 0.47982 0.28446 0.2940 0.27010 0.20686
   saturday      ,  beaten     6-3      by 23rd-seed
br  0.14287 0.1078 0.19323 0.21226 0.09731    0.1955
fr  0.19921 0.1384 0.20558 0.40985 0.12900    0.3236
gb  0.44845 0.3437 0.45359 0.21530 0.33256    0.1785
jp  0.09248 0.1099 0.07787 0.08317 0.11636    0.1379
us  0.11699 0.3001 0.06972 0.07942 0.32477    0.1646</code></pre>
</details>
<p><br/></p>
<p>To better understand how well we did, we can also generate two frequency tables for right and wrong predictions.</p>
<pre class="r"><code>prop.table(table(predict(model.NB) == docvars(dfmat_training, &quot;country&quot;))) * 100</code></pre>
<pre><code>FALSE  TRUE 
 2.71 97.29  </code></pre>
<p>To check if this result indicates a good performance, we compare it with a random result. We randomize the list of countries (and keep the overall frequency distribution of our countries constant) to allow our random algorithm a legitimate chance for a correct classification.</p>
<pre class="r"><code>prop.table(table(sample(predict(model.NB)) == docvars(dfmat_training, &quot;country&quot;))) * 100</code></pre>
<pre><code>FALSE  TRUE 
76.63 23.37  </code></pre>
<p>As we can see from the the summarized table below, our Naive Bayes classifier clearly outperforms a random algorithm.</p>
<table class="table" style="width: auto !important; margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
</th>
<th style="text-align:right;">
Naive Bayes
</th>
<th style="text-align:right;">
Random
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
False
</td>
<td style="text-align:right;">
2.71
</td>
<td style="text-align:right;">
76.63
</td>
</tr>
<tr>
<td style="text-align:left;">
True
</td>
<td style="text-align:right;">
97.29
</td>
<td style="text-align:right;">
23.37
</td>
</tr>
</tbody>
</table>
<p>We are likely to increase our accuracy even more by pre-processing our text data.</p>
<p>A confusion matrix helps us assess how well our algorithm performed. It shows us the prediction for all five countries in contrast to the actual class that is given by our data. For example, we predict 2580 articles as belonging to Great Britain that actually belong to Great Britain. However, we also predict 39 articles as British articles while they are actually French. Overall, when we look at the diagonal, we see that most predictions correctly classify the articles and that our algorithm performs well.</p>
<pre class="r"><code>actual_class &lt;- docvars(dfmat_matched, &quot;country&quot;)
predicted_class &lt;- predict(model.NB, newdata = dfmat_matched)
tab_class &lt;- table(actual_class, predicted_class)
tab_class</code></pre>
<pre><code>             predicted_class
actual_class   br   fr   gb   jp   us
          br 2580    6   50    6   37
          fr   39 2697  124   11   86
          gb   24   52 6072   24  282
          jp   33    9   31 1945  119
          us   56   32  194   66 4967</code></pre>
<p>We store our confusion matrix in an object because we need it later to visualize the results.</p>
<pre class="r"><code>confusion &lt;- confusionMatrix(tab_class, mode = &quot;everything&quot;)</code></pre>
<pre><code>Confusion Matrix and Statistics

            predicted_class
actual_class   br   fr   gb   jp   us
          br 2580    6   50    6   37
          fr   39 2697  124   11   86
          gb   24   52 6072   24  282
          jp   33    9   31 1945  119
          us   56   32  194   66 4967

Overall Statistics
                                          
               Accuracy : 0.9344          
                 95% CI : (0.9309, 0.9379)
    No Information Rate : 0.3311          
    P-Value [Acc &gt; NIR] : &lt; 2.2e-16       
                                          
                  Kappa : 0.914           
                                          
 Mcnemar&#39;s Test P-Value : &lt; 2.2e-16       

Statistics by Class:

                     Class: br Class: fr Class: gb Class: jp Class: us
Sensitivity             0.9444    0.9646    0.9383   0.94786    0.9046
Specificity             0.9941    0.9845    0.9708   0.98902    0.9752
Pos Pred Value          0.9630    0.9121    0.9408   0.91015    0.9345
Neg Pred Value          0.9910    0.9940    0.9695   0.99385    0.9632
Precision               0.9630    0.9121    0.9408   0.91015    0.9345
Recall                  0.9444    0.9646    0.9383   0.94786    0.9046
F1                      0.9536    0.9376    0.9396   0.92862    0.9193
Prevalence              0.1398    0.1431    0.3311   0.10500    0.2810
Detection Rate          0.1320    0.1380    0.3107   0.09953    0.2542
Detection Prevalence    0.1371    0.1513    0.3303   0.10935    0.2720
Balanced Accuracy       0.9692    0.9745    0.9546   0.96844    0.9399</code></pre>
<p>To display our confusion matrix visually, we could either produce a heatmap or a confusion matrix.</p>
<p>We first plot a heatmap using the code below.</p>
<details>
<p><summary>Code: Heatmap using <code>ggplot2</code></summary></p>
<pre class="r"><code># Save confusion matrix as data frame
confusion.data &lt;- as.data.frame(confusion[[&quot;table&quot;]])

# Reverse the order
level_order_y &lt;-
  factor(confusion.data$actual_class,
         level = c(&#39;us&#39;, &#39;jp&#39;, &#39;gb&#39;, &#39;fr&#39;, &#39;br&#39;))

ggplot(confusion.data,
       aes(x = predicted_class, y = level_order_y, fill = Freq)) +
  xlab(&quot;Predicted class&quot;) +
  ylab(&quot;Actual class&quot;) +
  geom_tile() + theme_bw() + coord_equal() +
  scale_fill_distiller(palette = &quot;Blues&quot;, direction = 1) +
  scale_x_discrete(labels = c(&quot;Brazil&quot;, &quot;France&quot;, &quot;Great \n Britain&quot;, &quot;Japan&quot;, &quot;USA&quot;)) +
  scale_y_discrete(labels = c(&quot;USA&quot;, &quot;Japan&quot;, &quot;Great \n Britain&quot;, &quot;France&quot;, &quot;Brazil&quot;))</code></pre>
</details>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-37"></span>
<img src="/../../../../article/advancing-text-mining/figures/confusion.png" alt="Contingency table" width="80%" />
<p class="caption">
Figure 4: Contingency table
</p>
</div>
<p><br/></p>
<p>To plot the following confusion matrix, we need to slightly adjust the code from <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/datavis/">this post on data visualization</a> by <a href="http://richardtraunmueller.com">Richard Traunmüller</a>.</p>
<details>
<p><summary>Code: Confusion matrix</summary></p>
<pre class="r"><code># Generate a data matrix from the confusion table
dat &lt;- data.matrix(confusion$table)

# Change order of column names
order.columns &lt;- c(5, 4, 3, 2, 1) 
dat &lt;- dat[order.columns,]

par(mgp = c(1.5, .3, 0))

# Plot
plot(
  0,
  0,
  # Type of plotting symbol
  pch = &quot;&quot;,
  # Range of x-axis
  xlim = c(0.5, 5.5),
  # Range of y-axis
  ylim = c(0.5, 6.5),
  # Suppresses both x and y axes
  axes = FALSE,
  # Label of x-axis
  xlab = &quot;Predicted class&quot;,
  # Label of y-axis
  ylab = &quot;Actual class&quot;,
)

# Write a for-loop that adds the bubbles to the plot
for (i in 1:dim(dat)[1]) {
  symbols(
    c(1:dim(dat)[2]),
    rep(i, dim(dat)[2]),
    circle = sqrt(dat[i,] / 9000 / pi),
    add = TRUE,
    inches = FALSE,
    fg = brewer.pal(sqrt(dat[i,] / 9000 / pi), &quot;Blues&quot;),
    bg = brewer.pal(sqrt(dat[i,] / 9000 / pi), &quot;Blues&quot;)
  )
}

axis(
  1,
  col = &quot;white&quot;,
  col.axis = &quot;black&quot;,
  at = c(1:5),
  label = colnames(dat)
)
axis(
  2,
  at = c(1:5),
  label = rownames(dat),
  las = 1,
  col.axis = &quot;black&quot;,
  col = &quot;white&quot;
)

# Add numbers to plot
for (i in 1:5) {
  text(c(1:5), rep(i, 5), dat[i,], cex = 0.8)
}</code></pre>
</details>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-39"></span>
<img src="/../../../../article/advancing-text-mining/figures/cont-plot.png" alt="Contingency table" width="80%" />
<p class="caption">
Figure 5: Contingency table
</p>
</div>
<p><br/></p>
<p>Both figures show us that our prediction performs well for all countries but it performs particularly well for the USA and Great Britain. Darker colors show a higher frequency in both plots, the contingency table also indicates a greater frequency with the size of the bubbles.</p>
</div>
</div>
<div id="unknown-categories" class="section level4">
<h4>Unknown categories <a name="unknowncategories"></a></h4>
<!-- ##### Unknown categories: Unsupervised machine learning <a name="unsupervised"></a> -->
<div id="unknown-categories-unsupervised-machine-learning---latent-semantic-analysis-lsa" class="section level5">
<h5>Unknown categories: Unsupervised machine learning - Latent semantic analysis (LSA) <a name="lsa"></a></h5>
<p>The next section addresses how to analyze texts with unknown categories. <strong>Latent Semantic Analysis (LSA)</strong> evaluates documents and seeks to find the underlying meaning or concept of these documents. If each word only had one meaning, LSA would have an easy job. However, oftentimes, words are ambiguous, have multiple meanings or are synonyms. One example from our corpus is “may” - it could be a verb, a noun for a month, or a name. To overcome this problem, LSA essentially compares how often words appear together in one document and then compares this across all other documents. By grouping words with other words, we try to identify those words that are semantically related and eventually also get the true meaning of ambiguous words.
More technically, LSA is a useful technique for aligning feature distributions to an n-dimensional space. This is achieved via <a href="https://blog.statsbot.co/singular-value-decomposition-tutorial-52c695315254">singular value decomposition (SVD)</a>. This decomposition allows us to decompose both a quadratic and a rectangular matrix.
LSA can (among other things) be used to compare similarity of documents/documents grouped by some variable.</p>
<p><a href="https://technowiki.wordpress.com/2011/08/27/latent-semantic-analysis-lsa-tutorial/">What are the major assumptions and simplifications that LSA has?</a></p>
<ol style="list-style-type: decimal">
<li><strong>Documents are non-positional</strong> (“bag of words”). The “bag of words” approach assumes that the order of the words does not matter. What matters is only the frequency of the single words.</li>
<li><strong>Concepts are</strong> understood as <strong>patterns of words</strong> where certain words often go together in similar documents.</li>
<li><strong>Words only have one meaning</strong> given the contexts surrounding the patterns of words.</li>
</ol>
<p>For the next example, we go back to the UN General Assembly speech data set.</p>
<pre class="r"><code>corpus.un.sample &lt;- corpus_sample(mycorpus, size = 500)</code></pre>
<p>We again follow the cleaning steps described above.</p>
<br/>
<details>
<p><summary>Code: Data pre-processing steps</summary></p>
<pre class="r"><code># Create tokens
token_sample &lt;-
  tokens(
    split_hyphens = TRUE,
    corpus.un.sample,
    remove_numbers = TRUE,
    remove_punct = TRUE,
    remove_symbols = TRUE,
    remove_url = TRUE,
    include_docvars = TRUE
  )

# Clean tokens created by OCR
token_ungd_sample &lt;- tokens_select(
  token_sample,
  c(&quot;[\\d-]&quot;, &quot;[[:punct:]]&quot;, &quot;^.{1,2}$&quot;),
  selection = &quot;remove&quot;,
  valuetype = &quot;regex&quot;,
  verbose = TRUE
)

dfmat &lt;- dfm(token_ungd_sample,
             tolower = TRUE,
             stem = TRUE,
             remove = stopwords(&quot;english&quot;)
             )</code></pre>
</details>
<p><br/></p>
<p>In an earlier version of this post, the function <code>textmodel_lsa</code> was already implemented in <code>quanteda</code>. It is now part of the package <a href="https://cran.r-project.org/web/packages/quanteda.textmodels/quanteda.textmodels.pdf"><code>quanteda.textmodels</code></a>. After loading the package, we can run the <code>textmodel_lsa</code> command.</p>
<pre class="r"><code># Load the package
library(quanteda.textmodels)

# Run the textmodel_lsa command
mylsa &lt;- quanteda.textmodels::textmodel_lsa(dfmat, nd = 10)</code></pre>
<p>One interesting question would be: How similar are the USA and Russia? Each dot represents a country-year observation. The USA are colored blue, Russia is colored red, and all other countries are grey.</p>
<pre class="r"><code># We need the &quot;stringr&quot; package for the following command
sources &lt;-
  str_remove_all(rownames(mylsa$docs), &quot;[0-9///&#39;._txt]&quot;) 

sources.color &lt;- rep(&quot;gray&quot;, times = length(sources))
sources.color[sources %in% &quot;USA&quot;] &lt;- &quot;blue&quot;
sources.color[sources %in% &quot;RUS&quot;] &lt;- &quot;red&quot;

plot(
  mylsa$docs[, 4:5],
  col = alpha(sources.color, 0.3),
  pch = 19,
  xlab = &quot;Dimension 4&quot;,
  ylab = &quot;Dimension 5&quot;,
  main = &quot;LSA dimensions by subcorpus&quot;
)</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-44"></span>
<img src="/../../../../article/advancing-text-mining/figures/lsa.png" alt="Distribution of PA topics in the UN General Debate corpus" width="80%" />
<p class="caption">
Figure 6: Distribution of PA topics in the UN General Debate corpus
</p>
</div>
<p><br/></p>
<p>On Dimension 5 we do not really observe a difference between documents from the US and Russia while we do see a topical divide on Dimension 4.
<!-- Documents from Russia clearly cluster on Dimension 2. --></p>
<pre class="r"><code># create an LSA space; return its truncated representation in the low-rank space
tmod &lt;- quanteda.textmodels::textmodel_lsa(dfmat[1:10, ])</code></pre>
<pre class="r"><code># matrix in low_rank LSA space
tmod$matrix_low_rank[, 1:5]</code></pre>
<pre><code>                        forti         third session general assembl
GNQ_43_1988.txt  4.000000e+00  6.000000e+00      10      12       9
UZB_64_2009.txt -6.306067e-14 -1.748601e-15       2       1       1
PRT_50_1995.txt  2.000000e+00 -2.962543e-13       5       8       4
VCT_70_2015.txt -4.979003e-14 -1.879096e-13       3       1       6
GUY_67_2012.txt -7.959952e-14 -1.302430e-14       3       6       2
MDG_34_1979.txt -1.627830e-13  7.000000e+00      14      13       6
CHL_60_2005.txt -1.064461e-13  2.000000e+00       5       3       4
GTM_68_2013.txt -1.319652e-13  5.002986e-14       6       7      11
GIN_64_2009.txt -1.256287e-13  1.000000e+00       5       5       2
LBN_59_2004.txt -9.672818e-14  1.000000e+00       2       4       5</code></pre>
<p>We now fold the queries into the space generated by <code>dfmat[1:10,]</code> and return its truncated versions of its representation in the new low-rank space. For more information on this, see <a href="https://onlinelibrary.wiley.com/doi/abs/10.1002/(SICI)1097-4571(199009)41:6%3C391::AID-ASI1%3E3.0.CO;2-9?casa_token=04042MH098kAAAAA:fScvmoc2WWrFxM4w2XTOkg1hAmBfaNuulZ3WKEnwjCpH727SAdVmphzv29VIvcAtcKkutMcKKVhaiZQ_">Deerwester et al. (1990)</a> and <a href="http://www.cse.msu.edu/~cse960/Papers/LSI/LSI.pdf">Rosario (2000)</a>.</p>
<pre class="r"><code>pred &lt;- predict(tmod, newdata = dfmat[1:10, ])
pred$docs_newspace</code></pre>
<pre><code>10 x 10 Matrix of class &quot;dgeMatrix&quot;
                      [,1]        [,2]        [,3]        [,4]         [,5]        [,6]
GNQ_43_1988.txt -0.3587033  0.04626331 -0.40268736  0.75368464 -0.198343099  0.15890426
UZB_64_2009.txt -0.1304653  0.04983523 -0.06010142 -0.07259054  0.057245087 -0.03112114
PRT_50_1995.txt -0.5120486  0.45389699  0.69697128  0.03247785 -0.071841805  0.18464906
VCT_70_2015.txt -0.2241820  0.09965473 -0.36666157 -0.45680821 -0.462552853  0.28818827
GUY_67_2012.txt -0.2905706  0.05927260 -0.29307648 -0.35382563  0.366086659  0.47269539
MDG_34_1979.txt -0.4919563 -0.83849245  0.20130813 -0.06146108 -0.038858129 -0.05927530
CHL_60_2005.txt -0.2663441  0.20190586 -0.13824724 -0.23099427 -0.006964881 -0.67210030
GTM_68_2013.txt -0.2074862  0.05127196 -0.13858784 -0.11930716  0.021618854 -0.33022555
GIN_64_2009.txt -0.2738299  0.12184387 -0.19025526  0.13644592  0.659835350 -0.13044787
LBN_59_2004.txt -0.1625876  0.12082658 -0.11439537  0.04220718 -0.408458883 -0.22780319
                       [,7]        [,8]         [,9]        [,10]
GNQ_43_1988.txt  0.13239767  0.10535490  0.203828010 -0.065029344
UZB_64_2009.txt -0.21573385 -0.08282906 -0.130474714 -0.947070625
PRT_50_1995.txt  0.07183401  0.00858728 -0.005558773  0.020944555
VCT_70_2015.txt  0.35837271 -0.38370263 -0.152115117  0.029859727
GUY_67_2012.txt -0.34813601  0.38234056  0.254633069  0.106243837
MDG_34_1979.txt -0.01837620 -0.06738449  0.012211891  0.023580112
CHL_60_2005.txt  0.04450401 -0.07554420  0.596888043  0.009695998
GTM_68_2013.txt  0.35028911  0.66063039 -0.502174774 -0.007009133
GIN_64_2009.txt  0.03757466 -0.49043679 -0.361551806  0.174062011
LBN_59_2004.txt -0.74478679 -0.03668237 -0.337785293  0.234975826</code></pre>
</div>
<div id="unknown-categories-unsupervised-machine-learning---latent-dirichlet-allocation-lda" class="section level5">
<h5>Unknown categories: Unsupervised machine learning - Latent Dirichlet Allocation (LDA) <a name="lda"></a></h5>
<p>Both Latent Dirichlet Allocation (LDA) and Structural Topic Modeling (STM) belong to <strong>topic modelling</strong>. Topic models find patterns of words that appear together and group them into topics. The researcher decides on the number of topics and the algorithms then discover the main topics of the texts without prior information, training sets or human annotations.</br></p>
<p><a href="http://pablobarbera.com/ECPR-SC105/slides/07-slides-text-unsupervised.pdf">LDA is a Bayesian mixture model for discrete data where topics are assumed to be uncorrelated.</a> It is a model that describes how the documents in a dataset were created. We assign an arbitrary number of topics (K) where each topic is a distribution over a fixed vocabulary. Each document is considered as a collection of words, one for each of K topics. It also follows the “bag of words” approach that considers each word in a document separately.</p>
<p>To calculate the LDA models, we need to load the package <code>topicmodels</code>. If you use this package for the first time on your machine, you need to execute a specific sequence of commands, detailed in the code below.</p>
<br/>
<details>
<p><summary>Code: Installing the package <code>topicmodels</code></summary></p>
<pre class="r"><code># 1) Install GSL
# We first need to make sure that GSL (e.g. &#39;brew install gsl&#39; in the terminal) is installed on our machine

# 2) Install gsl
# Then we proceed and choose either of the following commands:
# A)
install.packages(&quot;gsl&quot;)
# or B)
install.packages(
  &quot;https://cran.rstudio.com/src/contrib/gsl_2.1-6.tar.gz&quot;,
  repos = NULL,
  method = &quot;libcurl&quot;
)

# 3) Load the gsl package
library(gsl)

# 4) Install topicmodels
# Here we choose again either of the following commands:
# A)
install.packages(&quot;topicmodels&quot;)
# or B)
install.packages(
  &quot;https://cran.r-project.org/src/contrib/topicmodels_0.2-8.tar.gz&quot;,
  repos = NULL,
  method = &quot;libcurl&quot;
)

# 5) Load the topicmodels package
library(topicmodels)

# Now you are all set for the following models.</code></pre>
</details>
<p><br/></p>
<p>For the LDA, we again first trim our DFM. As above, the command <code>dfm_trim</code> trimms the text. This allows us to filter words that appear less than 7.5% and more than 90%.</p>
<pre class="r"><code>mydfm.un.trim &lt;-
  dfm_trim(
    mydfm,
    min_docfreq = 0.075,
    # min 7.5%
    max_docfreq = 0.90,
    # max 90%
    docfreq_type = &quot;prop&quot;
  ) </code></pre>
<p>We then assign an arbitrary topic number and convert the trimmed DFM to a topicmodels object.</p>
<pre class="r"><code># Assign an arbitrary number of topics
topic.count &lt;- 15

# Convert the trimmed DFM to a topicmodels object
dfm2topicmodels &lt;- convert(mydfm.un.trim, to = &quot;topicmodels&quot;)</code></pre>
<p>Eventually, we can calculate the LDA model with <code>quanteda</code>’s <code>LDA()</code> command.</p>
<pre class="r"><code>lda.model &lt;- LDA(dfm2topicmodels, topic.count)</code></pre>
<br/>
<details>
<p><Summary>Output for <code>lda.model</code></Summary></p>
<pre class="r"><code>lda.model</code></pre>
<pre><code>A LDA_VEM topic model with 15 topics.</code></pre>
<pre class="r"><code>as.data.frame(terms(lda.model, 6))</code></pre>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
topic1
</th>
<th style="text-align:left;">
topic2
</th>
<th style="text-align:left;">
topic3
</th>
<th style="text-align:left;">
topic4
</th>
<th style="text-align:left;">
topic5
</th>
<th style="text-align:left;">
topic6
</th>
<th style="text-align:left;">
topic7
</th>
<th style="text-align:left;">
topic8
</th>
<th style="text-align:left;">
topic9
</th>
<th style="text-align:left;">
topic10
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
nuclear
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
trade
</td>
<td style="text-align:left;">
america
</td>
<td style="text-align:left;">
arab
</td>
<td style="text-align:left;">
arab
</td>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
oper
</td>
<td style="text-align:left;">
war
</td>
</tr>
<tr>
<td style="text-align:left;">
weapon
</td>
<td style="text-align:left;">
council
</td>
<td style="text-align:left;">
interest
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
american
</td>
<td style="text-align:left;">
small
</td>
<td style="text-align:left;">
palestinian
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
negoti
</td>
<td style="text-align:left;">
today
</td>
</tr>
<tr>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
reform
</td>
<td style="text-align:left;">
hope
</td>
<td style="text-align:left;">
product
</td>
<td style="text-align:left;">
latin
</td>
<td style="text-align:left;">
nuclear
</td>
<td style="text-align:left;">
israel
</td>
<td style="text-align:left;">
social
</td>
<td style="text-align:left;">
conflict
</td>
<td style="text-align:left;">
now
</td>
</tr>
<tr>
<td style="text-align:left;">
soviet
</td>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
possibl
</td>
<td style="text-align:left;">
per
</td>
<td style="text-align:left;">
respect
</td>
<td style="text-align:left;">
issu
</td>
<td style="text-align:left;">
territori
</td>
<td style="text-align:left;">
order
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
power
</td>
</tr>
<tr>
<td style="text-align:left;">
relat
</td>
<td style="text-align:left;">
effect
</td>
<td style="text-align:left;">
now
</td>
<td style="text-align:left;">
economi
</td>
<td style="text-align:left;">
law
</td>
<td style="text-align:left;">
pacif
</td>
<td style="text-align:left;">
isra
</td>
<td style="text-align:left;">
futur
</td>
<td style="text-align:left;">
confer
</td>
<td style="text-align:left;">
live
</td>
</tr>
<tr>
<td style="text-align:left;">
disarma
</td>
<td style="text-align:left;">
process
</td>
<td style="text-align:left;">
even
</td>
<td style="text-align:left;">
increas
</td>
<td style="text-align:left;">
principl
</td>
<td style="text-align:left;">
weapon
</td>
<td style="text-align:left;">
resolut
</td>
<td style="text-align:left;">
common
</td>
<td style="text-align:left;">
south
</td>
<td style="text-align:left;">
mani
</td>
</tr>
</tbody>
</table>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
topic11
</th>
<th style="text-align:left;">
topic12
</th>
<th style="text-align:left;">
topic13
</th>
<th style="text-align:left;">
topic14
</th>
<th style="text-align:left;">
topic15
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
terror
</td>
<td style="text-align:left;">
african
</td>
<td style="text-align:left;">
deleg
</td>
<td style="text-align:left;">
africa
</td>
</tr>
<tr>
<td style="text-align:left;">
sustain
</td>
<td style="text-align:left;">
council
</td>
<td style="text-align:left;">
africa
</td>
<td style="text-align:left;">
session
</td>
<td style="text-align:left;">
south
</td>
</tr>
<tr>
<td style="text-align:left;">
climat
</td>
<td style="text-align:left;">
terrorist
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
independ
</td>
</tr>
<tr>
<td style="text-align:left;">
chang
</td>
<td style="text-align:left;">
iraq
</td>
<td style="text-align:left;">
conflict
</td>
<td style="text-align:left;">
concern
</td>
<td style="text-align:left;">
african
</td>
</tr>
<tr>
<td style="text-align:left;">
challeng
</td>
<td style="text-align:left;">
resolut
</td>
<td style="text-align:left;">
democrat
</td>
<td style="text-align:left;">
great
</td>
<td style="text-align:left;">
namibia
</td>
</tr>
<tr>
<td style="text-align:left;">
goal
</td>
<td style="text-align:left;">
law
</td>
<td style="text-align:left;">
elect
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
struggl
</td>
</tr>
</tbody>
</table>
</details>
<p><br/></p>
<p>How similar are the fifteen topics? This question is particularly interesting because it allows us to (possibly) cluster homogeneous topics. To get a better idea of our LDA model and about the similarity among the different topics, we can plot our results using the following chunck of code. <code>dist</code> and <code>hclust</code> are standard R commands that allow us to calculate the similarity.</p>
<pre class="r"><code>lda.similarity &lt;- as.data.frame(lda.model@beta) %&gt;%
  scale() %&gt;%
  dist(method = &quot;euclidean&quot;) %&gt;%
  hclust(method = &quot;ward.D2&quot;)

par(mar = c(0, 4, 4, 2))
plot(lda.similarity,
     main = &quot;LDA topic similarity by features&quot;,
     xlab = &quot;&quot;,
     sub = &quot;&quot;)</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-57"></span>
<img src="/../../../../article/advancing-text-mining/figures/lda2.png" alt="LDA topic similarity by features"  />
<p class="caption">
Figure 7: LDA topic similarity by features
</p>
</div>
<p><br/></p>
<p>The plot is called dendogram and visualizes a hierarchial clustering. The x-axis gives you the topics and the clusters of these topics. Put differently, it gives you information on the smilarity of the topics. On the y-axis, we see the dissmilarity (or distance) between our fifteen topics.</p>
</div>
<div id="unknown-categories-unsupervised-machine-learning---structural-topic-models-stm" class="section level5">
<h5>Unknown categories: Unsupervised machine learning - Structural topic models (STM) <a name="stm"></a></h5>
<p>The <a href="https://onlinelibrary.wiley.com/doi/full/10.1111/ajps.12103">structural topic models (STM) are a popular extension of the standard LDA models</a>. The STM allows to include metadata (the information about each document) into the topicmodel and it offers an alternative initialization mechanism (“Spectral”). For STMs, the covariates can be used in priors. The <a href="https://cran.r-project.org/web/packages/stm/vignettes/stmVignette.pdf">stm vignette</a> provides a good overview how to use a STM. The <a href="https://www.structuraltopicmodel.com">package includes estimation algorithms and tools for every stage of the workflow</a>. A particularly large emphasis is on a number of diagnostic functions that are integrated into the R package.</p>
<p>The <a href="https://github.com/cschwem2er/stminsights">package <code>stiminsights</code></a> is very useful for visual exploration. It allows the user to process <strong>interactive validation, interpretation and visualization</strong> of one or several Structural Topic Models (stm).</p>
<p>We again trim our dfm with the command <code>dfm_trim</code>.</p>
<pre class="r"><code>mydfm.un.trim &lt;-
  dfm_trim(
    mydfm,
    min_docfreq = 0.075,
    # min 7.5%
    max_docfreq = 0.90,
    # max 90%
    docfreq_type = &quot;prop&quot;
  ) </code></pre>
<p>We then assign the number of topics arbitrarily.</p>
<pre class="r"><code>topic.count &lt;- 25 # Assigns the number of topics</code></pre>
<p>And eventually convert the DFM (with <code>convert()</code>) and calculate the STM (with <code>stm()</code>).</p>
<pre class="r"><code># Calculate the STM 
dfm2stm &lt;- convert(mydfm.un.trim, to = &quot;stm&quot;)

model.stm &lt;- stm(
  dfm2stm$documents,
  dfm2stm$vocab,
  K = topic.count,
  data = dfm2stm$meta,
  init.type = &quot;Spectral&quot;
)</code></pre>
<p>To get a first insight, we print the terms that appear in each topic.</p>
<pre class="r"><code>as.data.frame(t(labelTopics(model.stm, n = 10)$prob))</code></pre>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
V1
</th>
<th style="text-align:left;">
V2
</th>
<th style="text-align:left;">
V3
</th>
<th style="text-align:left;">
V4
</th>
<th style="text-align:left;">
V5
</th>
<th style="text-align:left;">
V6
</th>
<th style="text-align:left;">
V7
</th>
<th style="text-align:left;">
V8
</th>
<th style="text-align:left;">
V9
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
council
</td>
<td style="text-align:left;">
lebanon
</td>
<td style="text-align:left;">
soviet
</td>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
african
</td>
<td style="text-align:left;">
nuclear
</td>
<td style="text-align:left;">
european
</td>
<td style="text-align:left;">
trade
</td>
<td style="text-align:left;">
african
</td>
</tr>
<tr>
<td style="text-align:left;">
reform
</td>
<td style="text-align:left;">
arab
</td>
<td style="text-align:left;">
union
</td>
<td style="text-align:left;">
sustain
</td>
<td style="text-align:left;">
africa
</td>
<td style="text-align:left;">
weapon
</td>
<td style="text-align:left;">
europ
</td>
<td style="text-align:left;">
economi
</td>
<td style="text-align:left;">
situat
</td>
</tr>
<tr>
<td style="text-align:left;">
effect
</td>
<td style="text-align:left;">
resolut
</td>
<td style="text-align:left;">
relat
</td>
<td style="text-align:left;">
challeng
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
treati
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
product
</td>
<td style="text-align:left;">
guinea
</td>
</tr>
<tr>
<td style="text-align:left;">
activ
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
militari
</td>
<td style="text-align:left;">
chang
</td>
<td style="text-align:left;">
democrat
</td>
<td style="text-align:left;">
disarma
</td>
<td style="text-align:left;">
union
</td>
<td style="text-align:left;">
industri
</td>
<td style="text-align:left;">
africa
</td>
</tr>
<tr>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
territori
</td>
<td style="text-align:left;">
forc
</td>
<td style="text-align:left;">
climat
</td>
<td style="text-align:left;">
millennium
</td>
<td style="text-align:left;">
arm
</td>
<td style="text-align:left;">
conflict
</td>
<td style="text-align:left;">
resourc
</td>
<td style="text-align:left;">
particular
</td>
</tr>
<tr>
<td style="text-align:left;">
strengthen
</td>
<td style="text-align:left;">
palestinian
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
goal
</td>
<td style="text-align:left;">
commit
</td>
<td style="text-align:left;">
test
</td>
<td style="text-align:left;">
process
</td>
<td style="text-align:left;">
increas
</td>
<td style="text-align:left;">
hope
</td>
</tr>
<tr>
<td style="text-align:left;">
convent
</td>
<td style="text-align:left;">
withdraw
</td>
<td style="text-align:left;">
arm
</td>
<td style="text-align:left;">
commit
</td>
<td style="text-align:left;">
poverti
</td>
<td style="text-align:left;">
non
</td>
<td style="text-align:left;">
stabil
</td>
<td style="text-align:left;">
market
</td>
<td style="text-align:left;">
concern
</td>
</tr>
<tr>
<td style="text-align:left;">
role
</td>
<td style="text-align:left;">
principl
</td>
<td style="text-align:left;">
war
</td>
<td style="text-align:left;">
agenda
</td>
<td style="text-align:left;">
particular
</td>
<td style="text-align:left;">
prolifer
</td>
<td style="text-align:left;">
contribut
</td>
<td style="text-align:left;">
price
</td>
<td style="text-align:left;">
solut
</td>
</tr>
<tr>
<td style="text-align:left;">
confer
</td>
<td style="text-align:left;">
solut
</td>
<td style="text-align:left;">
socialist
</td>
<td style="text-align:left;">
respons
</td>
<td style="text-align:left;">
like
</td>
<td style="text-align:left;">
pakistan
</td>
<td style="text-align:left;">
bosnia
</td>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
session
</td>
</tr>
<tr>
<td style="text-align:left;">
contribut
</td>
<td style="text-align:left;">
war
</td>
<td style="text-align:left;">
polici
</td>
<td style="text-align:left;">
address
</td>
<td style="text-align:left;">
summit
</td>
<td style="text-align:left;">
india
</td>
<td style="text-align:left;">
respect
</td>
<td style="text-align:left;">
financi
</td>
<td style="text-align:left;">
problem
</td>
</tr>
</tbody>
</table>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
V10
</th>
<th style="text-align:left;">
V11
</th>
<th style="text-align:left;">
V12
</th>
<th style="text-align:left;">
V13
</th>
<th style="text-align:left;">
V14
</th>
<th style="text-align:left;">
V15
</th>
<th style="text-align:left;">
V16
</th>
<th style="text-align:left;">
V17
</th>
<th style="text-align:left;">
V18
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
council
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
power
</td>
<td style="text-align:left;">
africa
</td>
<td style="text-align:left;">
order
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
israel
</td>
</tr>
<tr>
<td style="text-align:left;">
iraq
</td>
<td style="text-align:left;">
now
</td>
<td style="text-align:left;">
stabil
</td>
<td style="text-align:left;">
korea
</td>
<td style="text-align:left;">
great
</td>
<td style="text-align:left;">
south
</td>
<td style="text-align:left;">
principl
</td>
<td style="text-align:left;">
session
</td>
<td style="text-align:left;">
arab
</td>
</tr>
<tr>
<td style="text-align:left;">
resolut
</td>
<td style="text-align:left;">
oper
</td>
<td style="text-align:left;">
dialogu
</td>
<td style="text-align:left;">
democrat
</td>
<td style="text-align:left;">
viet
</td>
<td style="text-align:left;">
debt
</td>
<td style="text-align:left;">
social
</td>
<td style="text-align:left;">
oper
</td>
<td style="text-align:left;">
palestinian
</td>
</tr>
<tr>
<td style="text-align:left;">
law
</td>
<td style="text-align:left;">
negoti
</td>
<td style="text-align:left;">
terror
</td>
<td style="text-align:left;">
china
</td>
<td style="text-align:left;">
deleg
</td>
<td style="text-align:left;">
deleg
</td>
<td style="text-align:left;">
univers
</td>
<td style="text-align:left;">
confer
</td>
<td style="text-align:left;">
isra
</td>
</tr>
<tr>
<td style="text-align:left;">
aggress
</td>
<td style="text-align:left;">
hope
</td>
<td style="text-align:left;">
sudan
</td>
<td style="text-align:left;">
south
</td>
<td style="text-align:left;">
coloni
</td>
<td style="text-align:left;">
hope
</td>
<td style="text-align:left;">
cultur
</td>
<td style="text-align:left;">
solut
</td>
<td style="text-align:left;">
palestin
</td>
</tr>
<tr>
<td style="text-align:left;">
charter
</td>
<td style="text-align:left;">
agreement
</td>
<td style="text-align:left;">
call
</td>
<td style="text-align:left;">
korean
</td>
<td style="text-align:left;">
territori
</td>
<td style="text-align:left;">
process
</td>
<td style="text-align:left;">
today
</td>
<td style="text-align:left;">
resolut
</td>
<td style="text-align:left;">
east
</td>
</tr>
<tr>
<td style="text-align:left;">
violat
</td>
<td style="text-align:left;">
mani
</td>
<td style="text-align:left;">
process
</td>
<td style="text-align:left;">
asia
</td>
<td style="text-align:left;">
nam
</td>
<td style="text-align:left;">
environ
</td>
<td style="text-align:left;">
life
</td>
<td style="text-align:left;">
cyprus
</td>
<td style="text-align:left;">
middl
</td>
</tr>
<tr>
<td style="text-align:left;">
iraqi
</td>
<td style="text-align:left;">
last
</td>
<td style="text-align:left;">
promot
</td>
<td style="text-align:left;">
north
</td>
<td style="text-align:left;">
republ
</td>
<td style="text-align:left;">
confer
</td>
<td style="text-align:left;">
respect
</td>
<td style="text-align:left;">
concern
</td>
<td style="text-align:left;">
resolut
</td>
</tr>
<tr>
<td style="text-align:left;">
islam
</td>
<td style="text-align:left;">
way
</td>
<td style="text-align:left;">
commit
</td>
<td style="text-align:left;">
east
</td>
<td style="text-align:left;">
sea
</td>
<td style="text-align:left;">
welcom
</td>
<td style="text-align:left;">
societi
</td>
<td style="text-align:left;">
negoti
</td>
<td style="text-align:left;">
occupi
</td>
</tr>
<tr>
<td style="text-align:left;">
iran
</td>
<td style="text-align:left;">
possibl
</td>
<td style="text-align:left;">
issu
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
charter
</td>
<td style="text-align:left;">
conflict
</td>
<td style="text-align:left;">
becom
</td>
<td style="text-align:left;">
disarma
</td>
<td style="text-align:left;">
territori
</td>
</tr>
</tbody>
</table>
<table class="table" style="margin-left: auto; margin-right: auto;">
<thead>
<tr>
<th style="text-align:left;">
V19
</th>
<th style="text-align:left;">
V20
</th>
<th style="text-align:left;">
V21
</th>
<th style="text-align:left;">
V22
</th>
<th style="text-align:left;">
V23
</th>
<th style="text-align:left;">
V24
</th>
<th style="text-align:left;">
V25
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">
island
</td>
<td style="text-align:left;">
today
</td>
<td style="text-align:left;">
per
</td>
<td style="text-align:left;">
terror
</td>
<td style="text-align:left;">
america
</td>
<td style="text-align:left;">
conflict
</td>
<td style="text-align:left;">
africa
</td>
</tr>
<tr>
<td style="text-align:left;">
small
</td>
<td style="text-align:left;">
war
</td>
<td style="text-align:left;">
cent
</td>
<td style="text-align:left;">
afghanistan
</td>
<td style="text-align:left;">
american
</td>
<td style="text-align:left;">
refuge
</td>
<td style="text-align:left;">
south
</td>
</tr>
<tr>
<td style="text-align:left;">
pacif
</td>
<td style="text-align:left;">
live
</td>
<td style="text-align:left;">
social
</td>
<td style="text-align:left;">
afghan
</td>
<td style="text-align:left;">
latin
</td>
<td style="text-align:left;">
war
</td>
<td style="text-align:left;">
independ
</td>
</tr>
<tr>
<td style="text-align:left;">
caribbean
</td>
<td style="text-align:left;">
want
</td>
<td style="text-align:left;">
educ
</td>
<td style="text-align:left;">
cooper
</td>
<td style="text-align:left;">
central
</td>
<td style="text-align:left;">
somalia
</td>
<td style="text-align:left;">
african
</td>
</tr>
<tr>
<td style="text-align:left;">
chang
</td>
<td style="text-align:left;">
terror
</td>
<td style="text-align:left;">
poverti
</td>
<td style="text-align:left;">
terrorist
</td>
<td style="text-align:left;">
respect
</td>
<td style="text-align:left;">
assist
</td>
<td style="text-align:left;">
struggl
</td>
</tr>
<tr>
<td style="text-align:left;">
ocean
</td>
<td style="text-align:left;">
mani
</td>
<td style="text-align:left;">
health
</td>
<td style="text-align:left;">
problem
</td>
<td style="text-align:left;">
social
</td>
<td style="text-align:left;">
humanitarian
</td>
<td style="text-align:left;">
regim
</td>
</tr>
<tr>
<td style="text-align:left;">
issu
</td>
<td style="text-align:left;">
let
</td>
<td style="text-align:left;">
drug
</td>
<td style="text-align:left;">
global
</td>
<td style="text-align:left;">
process
</td>
<td style="text-align:left;">
mani
</td>
<td style="text-align:left;">
namibia
</td>
</tr>
<tr>
<td style="text-align:left;">
sea
</td>
<td style="text-align:left;">
now
</td>
<td style="text-align:left;">
democraci
</td>
<td style="text-align:left;">
asia
</td>
<td style="text-align:left;">
solut
</td>
<td style="text-align:left;">
elect
</td>
<td style="text-align:left;">
apartheid
</td>
</tr>
<tr>
<td style="text-align:left;">
climat
</td>
<td style="text-align:left;">
everi
</td>
<td style="text-align:left;">
programm
</td>
<td style="text-align:left;">
central
</td>
<td style="text-align:left;">
possibl
</td>
<td style="text-align:left;">
situat
</td>
<td style="text-align:left;">
coloni
</td>
</tr>
<tr>
<td style="text-align:left;">
call
</td>
<td style="text-align:left;">
know
</td>
<td style="text-align:left;">
million
</td>
<td style="text-align:left;">
issu
</td>
<td style="text-align:left;">
express
</td>
<td style="text-align:left;">
forc
</td>
<td style="text-align:left;">
racist
</td>
</tr>
</tbody>
</table>
<p>The following plot allows us to intuitively get information on the share of the different topics at the overall corpus.</p>
<pre class="r"><code>plot(
  model.stm,
  type = &quot;summary&quot;,
  text.cex = 0.5,
  main = &quot;STM topic shares&quot;,
  xlab = &quot;Share estimation&quot;
)</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-66"></span>
<img src="/../../../../article/advancing-text-mining/figures/stm.png" alt="STM topic shares"  />
<p class="caption">
Figure 8: STM topic shares
</p>
</div>
<p><br/></p>
<p>Using the package <code>stm</code>, we can now visualize the different words of a topic with a wordcloud. Since topic 4 has the highest share, we use it for the next visualization. The location of the words is randomized and changes each time we plot the wordcloud while the size of the words is relative to their frequency and remains the same.</p>
<pre class="r"><code>stm::cloud(model.stm,
           topic = 4,
           scale = c(2.25, .5))</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-68"></span>
<img src="/../../../../article/advancing-text-mining/figures/cloudstm.png" alt="Wordcloud with `stm`"  />
<p class="caption">
Figure 9: Wordcloud with <code>stm</code>
</p>
</div>
<p><br/></p>
<p>If we want, we can also put several different topics in visually perspective using the following lines of code:</p>
<pre class="r"><code>plot(model.stm,
     type = &quot;perspectives&quot;,
     topics = c(4, 5),
     main = &quot;Putting two different topics in perspective&quot;)</code></pre>
<div class="figure" style="text-align: center"><span id="fig:unnamed-chunk-70"></span>
<img src="/../../../../article/advancing-text-mining/figures/cloudstmperspective.png" alt="Wordcloud using `stm` -- Perspective plots"  />
<p class="caption">
Figure 10: Wordcloud using <code>stm</code> – Perspective plots
</p>
</div>
<p><br/></p>
<p>The <a href="https://www.rdocumentation.org/packages/stm/versions/1.3.3/topics/plot.STM">perspective plot</a> visualizes the combination of two topics (here topic 4 and topic 5). The size of the words is again relative to their frequency (within the combination of the two topics). The x-axis shows the dregree that specific words align with Topic 4 or Topic 5. <em>Global</em> is closely aligned with Topic 4 whereas <em>commit</em> is more central in both topics.
       </p>
</div>
</div>
</div>
<div id="further-readings" class="section level3">
<h3>Further readings <a name="furtherreadings"></a></h3>
<ul>
<li><a href="http://quanteda.io">quanteda: Quantitative Analysis of Textual Data</a></li>
<li><a href="https://CRAN.R-project.org/package=quanteda">Benoit, K., &amp; Nulty, P.. 2016. Quanteda: Quantitative Analysis of Textual Data.</a></li>
<li><a href="https://www.theoj.org/joss-papers/joss.00774/10.21105.joss.00774.pdf">Benoit, K., Watanabe, K., Wang, H., Nulty, P., Obeng, A., Müller, S., &amp; Matsuo, A. (2018). quanteda: An R package for the quantitative analysis of textual data. Journal of Open Source Software, 3(30), 774.</a></li>
<li><a href="https://onlinelibrary.wiley.com/doi/abs/10.1002/(SICI)1097-4571(199009)41:6%3C391::AID-ASI1%3E3.0.CO;2-9?casa_token=04042MH098kAAAAA:fScvmoc2WWrFxM4w2XTOkg1hAmBfaNuulZ3WKEnwjCpH727SAdVmphzv29VIvcAtcKkutMcKKVhaiZQ_">Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K., &amp; Harshman, R. (1990). Indexing by latent semantic analysis. Journal of the American society for information science, 41(6), 391-407.
Chicago</a></li>
<li><a href="https://www.cambridge.org/core/journals/political-analysis/article/text-as-data-the-promise-and-pitfalls-of-automatic-content-analysis-methods-for-political-texts/F7AAC8B2909441603FEB25C156448F20">Grimmer, J., &amp; Stewart, B. M. (2013). Text as data: The promise and pitfalls of automatic content analysis methods for political texts. Political Analysis, 21(3), 267-297.</a>
<!-- * [Jurka, T., Collingwood, L.,Boydstun, A. E., Grossman, E., & van Atteveldt, W. H. (2013). RTextTools: A supervised learning package for text classification.](http://rjournal.github.io/archive/2013-1/collingwood-jurka-boydstun-etal.pdf) --></li>
<li><a href="https://muellerstefan.net/files/quanteda-cheatsheet.pdf">Mueller, S. Quanteda Cheat Sheet.</a></li>
<li><a href="http://inhaltsanalyse-mit-r.de/">Puschmann, C. Inhaltsanalyse mit R.</a></li>
<li><a href="http://inhaltsanalyse-mit-r.de/0_einleitung.html">Puschmann, C. Automatisierte Inhaltsanalyse mit R.</a></li>
<li><a href="https://api.rpubs.com/cbpuschmann/AIR6">Puschmann, C. Automatisierte Inhaltsanalyse mit R. Überwachtes maschinelles Lernen.</a></li>
<li><a href="https://cran.r-project.org/web/packages/stm/vignettes/stmVignette.pdf">Roberts, M. E., Stewart, B. M., &amp; Tingley, D. (2014). stm: R package for structural topic models. R package, 1, 12.</a></li>
<li><a href="https://pdfs.semanticscholar.org/9598/1f057cb76a24329fcf2b572f75d8c2b1613e.pdf">Rosario, B. (2000). Latent semantic indexing: An overview. Technical Report. INFOSYS, 240, 1-16.</a></li>
<li><a href="https://www.tandfonline.com/doi/abs/10.1080/19312458.2017.1387238">Welbers, K., Van Atteveldt, W., &amp; Benoit, K. (2017). Text analysis in R. Communication Methods and Measures, 11(4), 245-265.</a></li>
</ul>
<p>       </p>
</div>
<div id="about-the-presenter" class="section level3">
<h3>About the presenter</h3>
<p><a href="http://cbpuschmann.net">Cornelius Puschmann</a> is professor of media and communication at <a href="https://uni-bremen.de/en/">ZeMKI, University of Bremen</a> and an affiliate researcher at the <a href="https://leibniz-hbi.de/en">Leibniz Institute for Media Research</a> in Hamburg. His research interests center on online hate speech, the role of algorithms for the selection of media content, and methodological aspects of computational social science.</p>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Quantitative Analysis of Political Text</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/quantitative-analysis-of-political-text/</link>
      <pubDate>Mon, 22 Jul 2019 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/quantitative-analysis-of-political-text/</guid>
      <description><![CDATA[
        </p>
<p>How can we infer actors’ positions, substantive topics, or sentiments from (political) texts? This <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/categories/tutorials/">Methods Bites Tutorial</a> by <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/page/team/">Julian Bernauer</a> summarizes <a href="https://denisetraber.net/">Denise Traber</a>’s workshop in the <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/page/events/">MZES Social Science Data Lab</a> in Spring 2018. Using exemplary sets of political documents (election manifestos and coalition agreements), it showcases tools of QTA for a variety of analytical objectives and demonstrates how to create, process, and analyse a text corpus through a series of hands-on applications.</p>
<p>After reading this blog post and engaging with the applied exercises, readers should:</p>
<ul>
<li>be able to perform some basic preprocessing of text</li>
<li>be able to estimate the sentiment of texts</li>
<li>be able to find topics in texts</li>
<li>be able to estimate (scale) positions of texts</li>
</ul>
<p>You can use these links to navigate across the main sections of this tutotial:</p>
<ol style="list-style-type: decimal">
<li><a href="#tour"><strong>A tour of Quantitative Text Analysis</strong></a></li>
<li><a href="#preprocessing"><strong>(Pre-)processing text</strong></a></li>
<li><a href="#smallcoalition"><strong>A small coalition corpus</strong></a></li>
<li><a href="#sentimentanalysis"><strong>Sentiment analysis using a dictionary</strong></a></li>
<li><a href="#lda"><strong>LDA topic modeling</strong></a></li>
<li><a href="#wordfish"><strong>Wordfish scaling</strong></a></li>
<li><a href="#intraparty"><strong>Estimating intra-party preferences: Comparing speeches to votes</strong></a></li>
<li><a href="#furtherreadings"><strong>Further readings</strong></a></li>
</ol>
<p><em>Note:</em> This blog post presents Denise’s workshop materials in condensed form. The complete workshop materials, including slides and scripts, are available from our GitHub.</p>
<div id="a-tour-of-quantitative-text-analysis" class="section level3">
<h3>A tour of Quantitative Text Analysis <a name="tour"></a></h3>
<p>The workshop started with a few basics: While QTA can be efficient and cheap, it always fails to rely on a correct model of language. It does not free us from reading texts, and validation is key. We learned about the basic distinction between classification (organizing text into categories) and scaling (estimation positions of actors), and its supervised (where hand-coded or other external data is available) and unsupervised (without such data) variants.</p>
</div>
<div id="pre-processing-text" class="section level3">
<h3>(Pre-)processing text <a name="preprocessing"></a></h3>
<p>We relied on the R package <a href="http://quanteda.io"><strong>quanteda</strong></a> developed by Ken Benoit and collaborators, which takes QTA by storm, at least for those working in R. Together with the <a href="https://cran.r-project.org/web/packages/readtext/index.html"><strong>readtext</strong></a> package, it easily allows to get your text data into R, create a so-called “corpus” of texts with the actual content as well as meta-information, and perform various tasks of corpus and text processing (subsetting a corpus, creating a document-feature matrix (dfm), stopword removal) as well as analysis (scaling, classification). A large and increasing number of extras is also available, such as ways to assess text similarity (function <code>textsta_simil()</code>) and lexical diversity (<code>textstat_lexdiv()</code>). Some of these features are demonstrated in an example below. Also see <a href="http://quanteda.io/reference/index.html">this overview</a> by quanteda for a full list of functions and the <a href="https://cran.r-project.org/web/packages/preText/vignettes/getting_started_with_preText.html"><strong>preText</strong></a> package for advise on evaluating pre-processing specifications.</p>
</div>
<div id="a-small-coalition-corpus" class="section level3">
<h3>A small coalition corpus <a name="smallcoalition"></a></h3>
<p>For a few examples from the workshop, consider a small set of three documents: The coalition agreement between the CDU/CSU and the SPD as well as the respective election manifestos from the 2017 Bundestag election. The corpus is created by:</p>
<pre class="r"><code>library(readtext)
library(quanteda)
text &lt;- readtext(paste0(wd, &quot;coalition/*.txt&quot;),
                 docvarsfrom = &quot;filenames&quot;,
                 docvarnames = &quot;Party&quot;)
text$text &lt;- gsub(&quot;\n&quot;, &quot; &quot;, text$text)
coalitioncorpus &lt;- corpus(text, docid_field = &quot;doc_id&quot;)
coalitioncorpus$metadata$source &lt;- &quot;[directory] on [system] by [user]&quot;
summary(coalitioncorpus)</code></pre>
<pre><code>## Corpus consisting of 3 documents:
## 
##           Text Types Tokens Sentences     Party
##     cducsu.txt  4738  26004      1288    cducsu
##  coalition.txt 11660  93214      3763 coalition
##        spd.txt  7650  50298      2402       spd
## 
## Source: [directory] on [system] by [user]
## Created: Wed Nov 11 15:06:41 2020
## Notes:</code></pre>
<p>The code relies on the two packages, <strong>readtext</strong> and <strong>quanteda</strong>, to create a data frame with the text files, using their names for a document-level variable called “Party”. The <code>gsub()</code> command removes whitespace, and <code>corpus()</code> turns the data frame into a corpus, which is a special case of a data frame containing texts, some meta-information and document-level variables, all optimized to perform a variety of quantitative text analysis operations using quanteda.</p>
<p>Further document-level variables are added via:</p>
<pre class="r"><code>docvars(coalitioncorpus, &quot;Year&quot;) &lt;- 2017
docvars(coalitioncorpus, &quot;Party_regex&quot;) &lt;- 
  sub(&quot;[\\.].*&quot;, &quot;&quot;, names(texts(coalitioncorpus)))
docvars(coalitioncorpus)</code></pre>
<pre><code>##                   Party Year Party_regex
## cducsu.txt       cducsu 2017      cducsu
## coalition.txt coalition 2017   coalition
## spd.txt             spd 2017         spd</code></pre>
<p>Note that this uses a regular expression (regex) to alternatively retrieve the party names from the filenames after creating the corpus. For specific analyses, we want to know the distribution of words across documents and create a document-feature matrix (dfm):</p>
<pre class="r"><code>dfm_coal &lt;- dfm(
  coalitioncorpus,
  remove = c(stopwords(&quot;german&quot;),
             &quot;dass&quot;,
             &quot;sowie&quot;,
             &quot;insbesondere&quot;),
  remove_punct = TRUE,
  stem = FALSE
)
dfm_coal[, 1:8]</code></pre>
<pre><code>## Document-feature matrix of: 3 documents, 8 features (16.7% sparse).
## 3 x 8 sparse Matrix of class &quot;dfm&quot;
##                features
## docs            gutes land zeit deutschland liebens lebenswertes gut
##   cducsu.txt        6   48   11         147       1            1  16
##   coalition.txt     1   39   14         195       0            0  13
##   spd.txt           6   44   33          97       0            0  17
##                features
## docs            wohnen
##   cducsu.txt         1
##   coalition.txt     10
##   spd.txt            6</code></pre>
<p>Creating a dfm induces a bag-of-words assumption. This means that the order in which words appear is ignored. A dfm is a means of information reduction and the most efficient way of storing text as data, but allows only analyses under this assumption. We quickly glance at the similarity (function <code>textstat_simil()</code>) and lexical diversity (function <code>textstat_lexdiv()</code>) of texts:</p>
<pre class="r"><code>simil &lt;- textstat_simil(dfm_coal,
                        margin = &quot;documents&quot;,
                        method = &quot;correlation&quot;)
simil </code></pre>
<pre><code>## textstat_simil object; method = &quot;correlation&quot;
##               cducsu.txt coalition.txt spd.txt
## cducsu.txt         1.000         0.968   0.975
## coalition.txt      0.968         1.000   0.986
## spd.txt            0.975         0.986   1.000</code></pre>
<pre class="r"><code>textstat_lexdiv(dfm_coal)[, 1:2]</code></pre>
<pre><code>##        document       TTR
## 1    cducsu.txt 0.3302084
## 2 coalition.txt 0.2487993
## 3       spd.txt 0.2802713</code></pre>
<p>From this, we learn that the SPD manifesto has more similarity to the coalition agreement than that of the CDU/CSU, a notion which somewhat resembles the assessment of the 2017 German coalition. Also, the lexical diversity of the manifestos, measured in terms of types (different words) per token (total words), appears to be higher than the coalition agreement, especially for the CDU/CSU.</p>
</div>
<div id="sentiment-analysis-using-a-dictionary" class="section level3">
<h3>Sentiment analysis using a dictionary <a name="sentimentanalysis"></a></h3>
<p>For sentiment analyis, existing dictionaries are available. It is important to note that these do not necessarily fit the research question at hand. In this example, the German “LIWC” (linguistic inquiry and word count) dictionary is used, but alternatives exist, such as “Lexicoder” for political text. LIWC features the categories “anger”, “posemo” (positive emotion) and “religion”. After obtaining the dictionary and applying it while creating a dfm from the corpus, the share of the texts in the respective categories is displayed. The results indicate that the coalition agreement features less positive emotions as compared to the manifestos and that the SPD manifesto is the most “angry” text, while the CDU/CSU speaks most about religion.</p>
<details>
<p><summary>Code: Using a Dictionary</summary></p>
<pre class="r"><code># Create dictionary
liwcdict &lt;- dictionary(file = paste0(wd, &quot;German_LIWC2001_Dictionary.dic&quot;),
                       format = &quot;LIWC&quot;)

# Create dfm
liwcdfm &lt;- dfm(
  coalitioncorpus,
  remove = c(stopwords(&quot;german&quot;)),
  remove_punct = TRUE,
  stem = FALSE,
  dictionary = liwcdict
)

# Subset and calculate percentage
liwcsub &lt;-
  dfm_select(liwcdfm,
             pattern = c(&quot;Anger&quot;, &quot;Posemo&quot;, &quot;Relig&quot;),
             selection = &quot;keep&quot;)

liwcsub &lt;- convert(liwcsub, to = &quot;data.frame&quot;)
liwcsub$sum &lt;- apply(dfm_coal, FUN = sum, 1)
liwcparties &lt;- data.frame(
  docs = liwcsub$document,
  ShareAnger = liwcsub$Anger / liwcsub$sum,
  SharePosemo = liwcsub$Posemo / liwcsub$sum,
  ShareRelig = liwcsub$Relig / liwcsub$sum
)

liwcparties </code></pre>
</details>
<pre><code>##            docs  ShareAnger SharePosemo  ShareRelig
## 1    cducsu.txt 0.004314995  0.06414959 0.005609493
## 2 coalition.txt 0.004666188  0.05050462 0.004074997
## 3       spd.txt 0.005550042  0.05697063 0.003454993</code></pre>
</div>
<div id="lda-topic-modelling" class="section level3">
<h3>LDA topic modelling <a name="lda"></a></h3>
<p>LDA stands for Latent Dirichlet allocation. In a nutshell, the method represents texts as a mixture of topics, and simultaneously topics as mixtures of words. Fixing the number of topics to <span class="math inline">\(k = 5\)</span>, and using the <strong>topicmodels</strong> package, the command <code>lda()</code> delivers posterior probabilities of the topics for each document.</p>
<details>
<p><summary>Code: LDA Topic Model</summary></p>
<pre class="r"><code>library(topicmodels)

# Preparation
dfm_coal &lt;-
  dfm(
    coalitioncorpus,
    remove = c(
      stopwords(&quot;german&quot;),
      &quot;dass&quot;,
      &quot;sowie&quot;,
      &quot;insbesondere&quot;,
      &quot;b&quot;,
      &quot;z&quot;,
      &quot;a&quot;,
      &quot;u&quot;
    ),
    remove_punct = TRUE,
    remove_numbers = TRUE,
    stem = FALSE
  )

dfm_coal &lt;- dfm_wordstem(dfm_coal, language = &quot;german&quot;)

# Define parameters
burnin &lt;- 1000
iter &lt;- 500
keep &lt;- 50
seed &lt;- 2010
ntopics &lt;- 5

# Run LDA with 5 topics
ldaOut &lt;- LDA(
  dfm_coal,
  k = ntopics,
  method = &quot;Gibbs&quot;,
  control = list(
    burnin = burnin,
    iter = iter,
    keep = keep,
    seed = seed,
    verbose = FALSE
  )
)
# Posterior probabilities of the topics for each document
k &lt;- posterior(ldaOut)</code></pre>
</details>
<p><br />
Interpretation is the difficult part. Each document can be expressed as a mixture of topics, and notwithstanding the precise meaning of the topics, we learn that all texts share content referring to topic 1, while the CDU/CSU manifesto also features topic 2 and the SPD manifesto topic 3 to some extent.</p>
<pre class="r"><code>k$topics</code></pre>
<pre><code>##                       1          2          3          4          5
## cducsu.txt    0.5815482 0.02729874 0.36122496 0.01144536 0.01848272
## coalition.txt 0.6950774 0.03700023 0.06163624 0.10570834 0.10057777
## spd.txt       0.6755292 0.17646590 0.11404313 0.01837605 0.01558576</code></pre>
</div>
<div id="wordfish-scaling" class="section level3">
<h3>Wordfish Scaling <a name="wordfish"></a></h3>
<p>Wordfish scaling derives latent positions from texts based on a bag-of-words assumption. Here is an example relying on a set of Swiss manifestos, using only the sections on immigration. In preparation, a dfm is created while removing stopwords, stemming the remaining words and removing punctuation.</p>
<details>
<p><summary>Code: Preparing Corpus for Wordfish</summary></p>
<pre class="r"><code>manifestos &lt;- readtext(paste0(wd, &quot;manifestos/*.txt&quot;))
manifestocorpus &lt;- corpus(manifestos)
dfm_manifesto &lt;-
  dfm(
    manifestocorpus,
    remove = c(
      &quot;gruen*&quot;,
      &quot;sp&quot;,
      &quot;sozialdemokrat*&quot;,
      &quot;cvp&quot;,
      &quot;fdp&quot;,
      &quot;svp&quot;,
      &quot;fuer&quot;,
      &quot;dass&quot;,
      &quot;koennen&quot;,
      &quot;koennte&quot;,
      &quot;ueber&quot;,
      &quot;waehrend&quot;,
      &quot;wuerde&quot;,
      &quot;wuerden&quot;,
      &quot;schweiz*&quot;,
      &quot;partei*&quot;,
      stopwords(&quot;german&quot;)
    ),
    valuetype = &quot;glob&quot;,
    stem = FALSE,
    remove_punct = TRUE
  )
dfm_manifesto &lt;- dfm_wordstem(dfm_manifesto, language = &quot;german&quot;)</code></pre>
</details>
<p><br />
The function <code>textmodel_wordfish()</code> computes the Wordfish model, originally decribed in an <a href="https://onlinelibrary.wiley.com/doi/full/10.1111/j.1540-5907.2008.00338.x">AJPS article</a> by Slapin and Proksch in 2009. It assumes that the distribution of words across texts follows a Poisson distribution, and can be modeled by document and word fixed effects as well as word-specific weights and document positions. The model is a variant of unsupervised scaling, only requiring the relative location of two texts on the latent dimension. Here, a text of the Swiss People’s Party (SVP) is assumed to be right to that of the Social Democratic Party of Switzerland (SPS). The results make some sense, with the other manifestos aligning as expected on what could be interpreted as a anti-immigration dimension.</p>
<pre class="r"><code>wf &lt;- textmodel_wordfish(dfm_manifesto,
                         dir = c(13, 19),
                         dispersion = &quot;poisson&quot;)
textplot_scale1d(wf)</code></pre>
<p><img src="/../../../../../article/quantitative-analysis-of-political-text_files/figure-html/fish-1.png" width="672" /></p>
<p>Or, with some improvements to the plot:</p>
<details>
<p><summary>Code: Improved Plot of Party Positions</summary></p>
<pre class="r"><code>library(ggplot2)

# Save document scores and confidence intervals in data frame
wfdata &lt;- as.data.frame(predict(wf, interval = &quot;confidence&quot;))

# Add document variables
wfdata$docs &lt;- rownames(wfdata)
wfdata$electionyear &lt;- substr(wfdata$docs, 5, 8)
wfdata$party &lt;- as.factor(substr(wfdata$docs, 1, 3))
wfdata$party &lt;-
  factor(wfdata$party, levels = c(&quot;gps&quot;, &quot;sps&quot;, &quot;cvp&quot;, &quot;fdp&quot;, &quot;svp&quot;))

ggplot(wfdata) +
  geom_pointrange(
    aes(
      x = electionyear,
      y = fit.fit,
      ymin = fit.lwr,
      ymax = fit.upr,
      group = party,
      color = party
    ),
    size = 0.5
  ) +
  geom_line(aes(
    x = electionyear,
    y = fit.fit,
    group = party,
    color = party
  )) +
  theme_bw() +
  labs(title = &quot;Wordfish analysis&quot;,
       y = &quot;Document position&quot;,
       x = &quot;Electionyear&quot;) +
  scale_color_manual(values = c(&quot;green3&quot;,
                                &quot;red1&quot;,
                                &quot;darkorange1&quot;,
                                &quot;dodgerblue4&quot;,
                                &quot;springgreen4&quot;))</code></pre>
</details>
<p><br />
<img src="/../../../../../article/quantitative-analysis-of-political-text_files/figure-html/fish3-1.png" width="672" /></p>
</div>
<div id="further-readings" class="section level3">
<h3>Further readings <a name="furtherreadings"></a></h3>
<ul>
<li>An introductory article to QTA in R, especially relying on <strong>quanteda</strong>: <a href="https://www.tandfonline.com/doi/abs/10.1080/19312458.2017.1387238">Welbers, Kasper, Wouter Van Atteveldt and Kenneth Benoit (2017): Text Analysis in R, <em>Communication Methods and Measures</em> 11(4): 245-65.</a></li>
<li>An application of the methods by the workshop host and co-authors: <a href="https://www.cambridge.org/core/journals/political-science-research-and-methods/article/estimating-intraparty-preferences-comparing-speeches-to-votes/D5812B196E0945B1341AFCD050F24858">Schwarz, Daniel, Denise Traber and Kenneth Benoit (2017): Estimating Intra-Party Preferences: Comparing Speeches to Votes. <em>Political Science Research and Methods</em> 5(2): 379-396.</a></li>
</ul>
</div>
<div id="about-the-presenter" class="section level3">
<h3>About the presenter</h3>
<p><a href="https://denisetraber.net">Denise Traber</a> is a Senior Research Fellow at the University of Lucerne, Switzerland, where she heads an Ambizione research grant project on “The divided people: polarization of political attitudes in Europe” funded by the Swiss National Science Foundation. She has a strong interest in quantitative text analysis, co-organizes the “Zurich Summer School for Women in Political Methodology” and has published the article “Estimating Intra-Party Preferences: Comparing Speeches to Votes” in Political Science Research and Methods in 2017, jointly with Daniel Schwarz and Ken Benoit.</p>
</div>
]]>
      </description>
    </item>
    
  </channel>
</rss>