<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ text mining - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ text mining - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 26 Jul 2026 22:34:31 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/text-mining/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ A Deep Dive into Word Embeddings for Sentiment Analysis ]]>
                </title>
                <description>
                    <![CDATA[ By Bert Carremans When applying one-hot encoding to words, we end up with sparse (containing many zeros) vectors of high dimensionality. On large data sets, this could cause performance issues.  Additionally, one-hot encoding does not take into accou... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/word-embeddings-for-sentiment-analysis/</link>
                <guid isPermaLink="false">66d45de6c7632f8bfbf1e411</guid>
                
                    <category>
                        <![CDATA[ keras ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nlp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Sentiment analysis ]]>
                    </category>
                
                    <category>
                        <![CDATA[ text mining ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Sun, 05 Jan 2020 14:27:33 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/01/1_u9pwb9JShvDIU7j1G9iszQ.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Bert Carremans</p>
<p>When applying one-hot encoding to words, we end up with sparse (containing many zeros) vectors of high dimensionality. On large data sets, this could cause performance issues. </p>
<p>Additionally, one-hot encoding does not take into account the semantics of the words. So words like <em>airplane</em> and <em>aircraft</em> are considered to be two different features while we know that they have a very similar meaning. Word embeddings address these two issues.</p>
<p>Word embeddings are dense vectors with much lower dimensionality. Secondly, the semantic relationships between words are reflected in the distance and direction of the vectors.</p>
<p>We will work with the <a target="_blank" href="https://www.kaggle.com/crowdflower/twitter-airline-sentiment">TwitterAirlineSentiment data set on Kaggle</a>. This data set contains roughly 15K tweets with 3 possible classes for the sentiment (positive, negative and neutral). In my previous post, we tried to <a target="_blank" href="https://www.freecodecamp.org/news/sentiment-analysis-with-text-mining/">classify the tweets</a> by tokenizing the words and applying two classifiers. Let’s see if word embeddings can outperform that.</p>
<p>After reading this tutorial you will know how to compute task-specific word embeddings with the Embedding layer of <strong>Keras</strong>. Secondly, we will investigate whether word embeddings trained on a larger corpus can improve the accuracy of our model.</p>
<p>The structure of this tutorial is:</p>
<ul>
<li>Intuition behind word embeddings</li>
<li>Project set-up</li>
<li>Data preparation</li>
<li>Keras and its Embedding layer</li>
<li>Pre-trained word embeddings — GloVe</li>
<li>Training word embeddings with more dimensions</li>
</ul>
<h1 id="heading-intuition-behind-word-embeddings">Intuition behind word embeddings</h1>
<p>Before we can use words in a classifier, we need to convert them into numbers. One way to do that is to simply map words to integers. Another way is to one-hot encode words. Each tweet could then be represented as a vector with a dimension equal to (a limited set of) the words in the corpus. The words occurring in the tweet have a value of 1 in the vector. All other vector values equal zero.</p>
<p>Word embeddings are computed differently. Each word is positioned into a <strong><em>multi-dimensional space</em></strong>. The number of dimensions in this space is chosen by the data scientist. You can experiment with different dimensions and see what provides the best result.</p>
<p>The <strong><em>vector values for a word represent its position</em></strong> in this embedding space. Synonyms are found close to each other while words with opposite meanings have a large distance between them. You can also apply mathematical operations on the vectors which should produce semantically correct results. A typical example is that the sum of the word embeddings of <em>king</em> and <em>female</em> produces the word embedding of <em>queen</em>.</p>
<h1 id="heading-project-set-up">Project set-up</h1>
<p>Let’s start by importing all packages for this project.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> re
<span class="hljs-keyword">import</span> collections
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt
<span class="hljs-keyword">from</span> pathlib <span class="hljs-keyword">import</span> Path
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> train_test_split
<span class="hljs-keyword">from</span> nltk.corpus <span class="hljs-keyword">import</span> stopwords
<span class="hljs-keyword">from</span> keras.preprocessing.text <span class="hljs-keyword">import</span> Tokenizer
<span class="hljs-keyword">from</span> keras.preprocessing.sequence <span class="hljs-keyword">import</span> pad_sequences
<span class="hljs-keyword">from</span> keras.utils.np_utils <span class="hljs-keyword">import</span> to_categorical
<span class="hljs-keyword">from</span> sklearn.preprocessing <span class="hljs-keyword">import</span> LabelEncoder
<span class="hljs-keyword">from</span> keras <span class="hljs-keyword">import</span> models
<span class="hljs-keyword">from</span> keras <span class="hljs-keyword">import</span> layers
</code></pre>
<p>We define some parameters and paths used throughout the project. Most of them are self-explanatory. But others will be explained further in the code.</p>
<pre><code class="lang-python">NB_WORDS = <span class="hljs-number">10000</span>  <span class="hljs-comment"># Parameter indicating the number of words we'll put in the dictionary</span>
VAL_SIZE = <span class="hljs-number">1000</span>  <span class="hljs-comment"># Size of the validation set</span>
NB_START_EPOCHS = <span class="hljs-number">10</span>  <span class="hljs-comment"># Number of epochs we usually start to train with</span>
BATCH_SIZE = <span class="hljs-number">512</span>  <span class="hljs-comment"># Size of the batches used in the mini-batch gradient descent</span>
MAX_LEN = <span class="hljs-number">24</span>  <span class="hljs-comment"># Maximum number of words in a sequence</span>
GLOVE_DIM = <span class="hljs-number">100</span>  <span class="hljs-comment"># Number of dimensions of the GloVe word embeddings</span>
root = Path(<span class="hljs-string">'../'</span>)
input_path = root / <span class="hljs-string">'input/'</span>
ouput_path = root / <span class="hljs-string">'output/'</span>
source_path = root / <span class="hljs-string">'source/'</span>
</code></pre>
<p>Throughout this code, we will also use some helper functions for data preparation, modeling and visualization. These function definitions are not shown here to keep the blog post clutter free. You can always refer to the <a target="_blank" href="https://github.com/bertcarremans/TwitterUSAirlineSentiment/blob/master/source/Using%20Word%20Embeddings%20for%20Sentiment%20Analysis.ipynb">notebook in Github</a> to look at the code.</p>
<h1 id="heading-data-preparation">Data preparation</h1>
<h2 id="heading-reading-the-data-and-cleaning">Reading the data and cleaning</h2>
<p>We read in the CSV file with the tweets and apply a random shuffle on its indexes. After that, we remove stop words and @ mentions. A test set of 10% is split off to evaluate the model on new data.</p>
<pre><code class="lang-python">df = pd.read_csv(input_path / <span class="hljs-string">'Tweets.csv'</span>)
df = df.reindex(np.random.permutation(df.index))
df = df[[<span class="hljs-string">'text'</span>, <span class="hljs-string">'airline_sentiment'</span>]]
df.text = df.text.apply(remove_stopwords).apply(remove_mentions)
X_train, X_test, y_train, y_test = train_test_split(df.text, df.airline_sentiment, test_size=<span class="hljs-number">0.1</span>, random_state=<span class="hljs-number">37</span>)
</code></pre>
<h2 id="heading-convert-words-into-integers">Convert words into integers</h2>
<p>With the <strong><em>Tokenizer</em></strong> from Keras, we convert the tweets into sequences of integers. We limit the number of words to the <strong>_NB<em>WORDS</em></strong> most frequent words. Additionally, the tweets are cleaned with some filters, set to lowercase and split on spaces.</p>
<pre><code class="lang-python">tk = Tokenizer(num_words=NB_WORDS,
filters=<span class="hljs-string">'!"#$%&amp;()*+,-./:;&lt;=&gt;?@[\]^_`{"}~\t\n'</span>,lower=<span class="hljs-literal">True</span>, split=<span class="hljs-string">" "</span>)
tk.fit_on_texts(X_train)
X_train_seq = tk.texts_to_sequences(X_train)
X_test_seq = tk.texts_to_sequences(X_test)
</code></pre>
<h2 id="heading-equal-length-of-sequences">Equal length of sequences</h2>
<p>Each batch needs to provide sequences of equal length. We achieve this with the <strong>_pad<em>sequences</em></strong> method. By specifying <strong><em>maxlen</em></strong>, the sequences or padded with zeros or truncated.</p>
<pre><code class="lang-python">X_train_seq_trunc = pad_sequences(X_train_seq, maxlen=MAX_LEN)
X_test_seq_trunc = pad_sequences(X_test_seq, maxlen=MAX_LEN)
</code></pre>
<h2 id="heading-encoding-the-target-variable">Encoding the target variable</h2>
<p>The target classes are strings which need to be converted into numeric vectors. This is done with the <strong><em>LabelEncoder</em></strong> from Sklearn and the <strong>_to<em>categorical</em></strong> method from Keras.</p>
<pre><code class="lang-python">le = LabelEncoder()
y_train_le = le.fit_transform(y_train)
y_test_le = le.transform(y_test)
y_train_oh = to_categorical(y_train_le)
y_test_oh = to_categorical(y_test_le)
</code></pre>
<h2 id="heading-splitting-off-the-validation-set">Splitting off the validation set</h2>
<p>From the training data, we split off a validation set of 10% to use during training.</p>
<pre><code class="lang-python">X_train_emb, X_valid_emb, y_train_emb, y_valid_emb = train_test_split(X_train_seq_trunc, y_train_oh, test_size=<span class="hljs-number">0.1</span>, random_state=<span class="hljs-number">37</span>)
</code></pre>
<h1 id="heading-modeling">Modeling</h1>
<h2 id="heading-keras-and-the-embedding-layer">Keras and the Embedding layer</h2>
<p>Keras provides a convenient way to convert each word into a multi-dimensional vector. This can be done with the <strong><em>Embedding</em></strong> layer. It will compute the word embeddings (or use pre-trained embeddings) and look up each word in a dictionary to find its vector representation. Here we will train word embeddings with 8 dimensions.</p>
<pre><code class="lang-python">emb_model = models.Sequential()
emb_model.add(layers.Embedding(NB_WORDS, <span class="hljs-number">8</span>, input_length=MAX_LEN))
emb_model.add(layers.Flatten())
emb_model.add(layers.Dense(<span class="hljs-number">3</span>, activation=<span class="hljs-string">'softmax'</span>))
emb_history = deep_model(emb_model, X_train_emb, y_train_emb, X_valid_emb, y_valid_emb)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/0_-XjJ4DTQ5RQ8jZOF.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We have a validation accuracy of about 74%. The number of words in the tweets is rather low, so this result is quite good. By comparing the training and validation loss, we see that the model starts <strong>overfitting</strong> from epoch 6.</p>
<p>In a previous article, I discussed how we can <a target="_blank" href="https://www.freecodecamp.org/news/handling-overfitting-in-deep-learning-models/">avoid overfitting</a>. You might want to read that if you want to deep dive on that topic.</p>
<p>When we train the model on all data (including the validation data, but excluding the test data) and set the number of epochs to 6, we get a test accuracy of 78%. This test result is OK, but let’s see if we can improve with pre-trained word embeddings.</p>
<pre><code class="lang-python">emb_results = test_model(emb_model, X_train_seq_trunc, y_train_oh, X_test_seq_trunc, y_test_oh, <span class="hljs-number">6</span>)
print(<span class="hljs-string">'/n'</span>)
print(<span class="hljs-string">'Test accuracy of word embeddings model: {0:.2f}%'</span>.format(emb_results[<span class="hljs-number">1</span>]*<span class="hljs-number">100</span>))
</code></pre>
<h2 id="heading-pre-trained-word-embeddings-glove">Pre-trained word embeddings — Glove</h2>
<p>Because the training data is not so large, the model might not be able to learn good embeddings for the sentiment analysis. Alternatively, we can load pre-trained word embeddings built on a much larger training data.</p>
<p>The <a target="_blank" href="https://nlp.stanford.edu/projects/glove/">GloVe database</a> contains multiple pre-trained word embeddings, and more specific <strong><em>embeddings trained on tweets</em></strong>. So this might be useful for the task at hand.</p>
<p>First, we put the word embeddings in a dictionary where the keys are the words and the values the word embeddings.</p>
<pre><code class="lang-python">glove_file = <span class="hljs-string">'glove.twitter.27B.'</span> + str(GLOVE_DIM) + <span class="hljs-string">'d.txt'</span>
emb_dict = {}
glove = open(input_path / glove_file)
<span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> glove:
    values = line.split()
    word = values[<span class="hljs-number">0</span>]
    vector = np.asarray(values[<span class="hljs-number">1</span>:], dtype=<span class="hljs-string">'float32'</span>)
    emb_dict[word] = vector
glove.close()
</code></pre>
<p>With the GloVe embeddings loaded in a dictionary, we can look up the embedding for each word in the corpus of the airline tweets. These will be stored in a matrix with a shape of <strong>_NB<em>WORDS</em></strong> and <strong>_GLOVE<em>DIM</em></strong>. If a word is not found in the GloVe dictionary, the word embedding values for the word are zero.</p>
<pre><code class="lang-python">emb_matrix = np.zeros((NB_WORDS, GLOVE_DIM))
<span class="hljs-keyword">for</span> w, i <span class="hljs-keyword">in</span> tk.word_index.items():
    <span class="hljs-keyword">if</span> i &lt; NB_WORDS:
        vect = emb_dict.get(w)
        <span class="hljs-keyword">if</span> vect <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        emb_matrix[i] = vect
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">break</span>
</code></pre>
<p>Then we specify the model just like we did with the model above.</p>
<pre><code class="lang-python">glove_model = models.Sequential()
glove_model.add(layers.Embedding(NB_WORDS, GLOVE_DIM, input_length=MAX_LEN))
glove_model.add(layers.Flatten())
glove_model.add(layers.Dense(<span class="hljs-number">3</span>, activation=<span class="hljs-string">'softmax'</span>))
</code></pre>
<p>In the Embedding layer (which is layer 0 here) we <strong><em>set the weights</em></strong> for the words to those found in the GloVe word embeddings. By setting <strong><em>trainable</em></strong> to False we make sure that the GloVe word embeddings cannot be changed. After that, we run the model.</p>
<pre><code class="lang-python">glove_model.layers[<span class="hljs-number">0</span>].set_weights([emb_matrix])
glove_model.layers[<span class="hljs-number">0</span>].trainable = <span class="hljs-literal">False</span>
glove_history = deep_model(glove_model, X_train_emb, y_train_emb, X_valid_emb, y_valid_emb)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/0_uhsGcl8UG_JYUycb.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The model overfits fast after 3 epochs. Furthermore, the validation accuracy is lower compared to the embeddings trained on the training data.</p>
<pre><code class="lang-python">glove_results = test_model(glove_model, X_train_seq_trunc, y_train_oh, X_test_seq_trunc, y_test_oh, <span class="hljs-number">3</span>)
print(<span class="hljs-string">'/n'</span>)
print(<span class="hljs-string">'Test accuracy of word glove model: {0:.2f}%'</span>.format(glove_results[<span class="hljs-number">1</span>]*<span class="hljs-number">100</span>))
</code></pre>
<p>As a final exercise, let’s see what results we get when we train the embeddings with the same number of dimensions as the GloVe data.</p>
<h2 id="heading-training-word-embeddings-with-more-dimensions">Training word embeddings with more dimensions</h2>
<p>We will train the word embeddings with the same number of dimensions as the GloVe embeddings (i.e. GLOVE_DIM).</p>
<pre><code class="lang-python">emb_model2 = models.Sequential()
emb_model2.add(layers.Embedding(NB_WORDS, GLOVE_DIM, input_length=MAX_LEN))
emb_model2.add(layers.Flatten())
emb_model2.add(layers.Dense(<span class="hljs-number">3</span>, activation=<span class="hljs-string">'softmax'</span>))
emb_history2 = deep_model(emb_model2, X_train_emb, y_train_emb, X_valid_emb, y_valid_emb)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/0_boJxTu7msbxWzexm.png" alt="Image" width="600" height="400" loading="lazy"></p>
<pre><code class="lang-python">emb_results2 = test_model(emb_model2, X_train_seq_trunc, y_train_oh, X_test_seq_trunc, y_test_oh, <span class="hljs-number">3</span>)
print(<span class="hljs-string">'/n'</span>)
print(<span class="hljs-string">'Test accuracy of word embedding model 2: {0:.2f}%'</span>.format(emb_results2[<span class="hljs-number">1</span>]*<span class="hljs-number">100</span>))
</code></pre>
<p>On the test data we get good results, but we do not outperform the LogisticRegression with the CountVectorizer. So there is still room for improvement.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>The best result is achieved with 100-dimensional word embeddings that are trained on the available data. This even outperforms the use of word embeddings that were trained on a much larger Twitter corpus.</p>
<p>Until now we have just put a Dense layer on the flattened embeddings. By doing this, <strong><em>we do not take into account the relationships between the words</em></strong> in the tweet. This can be achieved with a recurrent neural network or a 1D convolutional network. But that’s something for a future post :)</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Sentiment Analysis with Text Mining ]]>
                </title>
                <description>
                    <![CDATA[ By Bert Carremans In this tutorial, I will explore some text mining techniques for sentiment analysis. We'll look at how to prepare textual data. After that we will try two different classifiers to infer the tweets' sentiment. We will tune the hyperp... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/sentiment-analysis-with-text-mining/</link>
                <guid isPermaLink="false">66d45de18812486a37369c87</guid>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nlp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Sentiment analysis ]]>
                    </category>
                
                    <category>
                        <![CDATA[ text mining ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 13 Jun 2019 21:42:41 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/06/dictionary.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Bert Carremans</p>
<p>In this tutorial, I will explore some text mining techniques for sentiment analysis. We'll look at how to prepare textual data. After that we will try two different classifiers to infer the tweets' sentiment. We will tune the hyperparameters of both classifiers with grid search. Finally, we evaluate the performance on a set of metrics like precision, recall and the F1 score.</p>
<p>For this project, we'll be working with the <a target="_blank" href="https://www.kaggle.com/crowdflower/twitter-airline-sentiment">Twitter US Airline Sentiment data set on Kaggle</a>. It contains the tweet’s text and one variable with three possible sentiment values. Let's start by importing the packages and configuring some settings.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np 
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd 
pd.set_option(<span class="hljs-string">'display.max_colwidth'</span>, <span class="hljs-number">-1</span>)
<span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> re
<span class="hljs-keyword">import</span> string
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> emoji
<span class="hljs-keyword">from</span> pprint <span class="hljs-keyword">import</span> pprint
<span class="hljs-keyword">import</span> collections
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt
<span class="hljs-keyword">import</span> seaborn <span class="hljs-keyword">as</span> sns
sns.set(style=<span class="hljs-string">"darkgrid"</span>)
sns.set(font_scale=<span class="hljs-number">1.3</span>)
<span class="hljs-keyword">from</span> sklearn.base <span class="hljs-keyword">import</span> BaseEstimator, TransformerMixin
<span class="hljs-keyword">from</span> sklearn.feature_extraction.text <span class="hljs-keyword">import</span> CountVectorizer
<span class="hljs-keyword">from</span> sklearn.feature_extraction.text <span class="hljs-keyword">import</span> TfidfVectorizer
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> GridSearchCV
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> train_test_split
<span class="hljs-keyword">from</span> sklearn.pipeline <span class="hljs-keyword">import</span> Pipeline, FeatureUnion
<span class="hljs-keyword">from</span> sklearn.metrics <span class="hljs-keyword">import</span> classification_report
<span class="hljs-keyword">from</span> sklearn.naive_bayes <span class="hljs-keyword">import</span> MultinomialNB
<span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> LogisticRegression
<span class="hljs-keyword">from</span> sklearn.externals <span class="hljs-keyword">import</span> joblib
<span class="hljs-keyword">import</span> gensim
<span class="hljs-keyword">from</span> nltk.corpus <span class="hljs-keyword">import</span> stopwords
<span class="hljs-keyword">from</span> nltk.stem <span class="hljs-keyword">import</span> PorterStemmer
<span class="hljs-keyword">from</span> nltk.tokenize <span class="hljs-keyword">import</span> word_tokenize
<span class="hljs-keyword">import</span> warnings
warnings.filterwarnings(<span class="hljs-string">'ignore'</span>)
np.random.seed(<span class="hljs-number">37</span>)
</code></pre>
<h2 id="heading-loading-the-data">Loading the data</h2>
<p>We read in the comma separated file we downloaded from the Kaggle Datasets. We shuffle the data frame in case the classes are sorted. Applying the <code>reindex</code> method on the <code>permutation</code> of the original indices is good for that. In this notebook, we will work with the <code>text</code> variable and the <code>airline_sentiment</code> variable.</p>
<pre><code class="lang-python">df = pd.read_csv(<span class="hljs-string">'../input/Tweets.csv'</span>)
df = df.reindex(np.random.permutation(df.index))
df = df[[<span class="hljs-string">'text'</span>, <span class="hljs-string">'airline_sentiment'</span>]]
</code></pre>
<h2 id="heading-exploratory-data-analysis">Exploratory Data Analysis</h2>
<h3 id="heading-target-variable">Target variable</h3>
<p>There are three class labels we will predict: negative, neutral or positive.</p>
<p>The class labels are imbalanced as we can see below in the chart. This is something that we should keep in mind during the model training phase. With the <code>factorplot</code> of the seaborn package, we can visualize the distribution of the target variable.</p>
<pre><code class="lang-python">sns.factorplot(x=<span class="hljs-string">"airline_sentiment"</span>, data=df, kind=<span class="hljs-string">"count"</span>, size=<span class="hljs-number">6</span>, aspect=<span class="hljs-number">1.5</span>, palette=<span class="hljs-string">"PuBuGn_d"</span>)
plt.show();
</code></pre>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*v99Gfk4iL4POvy2F.png" alt="Image" width="646" height="412" loading="lazy">
<em>Imbalanced distribution of the target class labels</em></p>
<h3 id="heading-input-variable">Input variable</h3>
<p>To analyze the <code>text</code> variable we create a class <code>TextCounts</code>. In this class we compute some basic statistics on the text variable.</p>
<ul>
<li><code>count_words</code>: number of words in the tweet</li>
<li><code>count_mentions</code>: referrals to other Twitter accounts start with a @</li>
<li><code>count_hashtags</code>: number of tag words, preceded by a #</li>
<li><code>count_capital_words</code>: number of uppercase words are sometimes used to “shout” and express (negative) emotions</li>
<li><code>count_excl_quest_marks</code>: number of question or exclamation marks</li>
<li><code>count_urls</code>: number of links in the tweet, preceded by http(s)</li>
<li><code>count_emojis</code>: number of emoji, which might be a good sign of the sentiment</li>
</ul>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TextCounts</span>(<span class="hljs-params">BaseEstimator, TransformerMixin</span>):</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_regex</span>(<span class="hljs-params">self, pattern, tweet</span>):</span>
        <span class="hljs-keyword">return</span> len(re.findall(pattern, tweet))

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fit</span>(<span class="hljs-params">self, X, y=None, **fit_params</span>):</span>
        <span class="hljs-comment"># fit method is used when specific operations need to be done on the train data, but not on the test data</span>
        <span class="hljs-keyword">return</span> self

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">transform</span>(<span class="hljs-params">self, X, **transform_params</span>):</span>
        count_words = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'\w+'</span>, x)) 
        count_mentions = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'@\w+'</span>, x))
        count_hashtags = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'#\w+'</span>, x))
        count_capital_words = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'\b[A-Z]{2,}\b'</span>, x))
        count_excl_quest_marks = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'!|\?'</span>, x))
        count_urls = X.apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r'http.?://[^\s]+[\s]?'</span>, x))
        <span class="hljs-comment"># We will replace the emoji symbols with a description, which makes using a regex for counting easier</span>
        <span class="hljs-comment"># Moreover, it will result in having more words in the tweet</span>
        count_emojis = X.apply(<span class="hljs-keyword">lambda</span> x: emoji.demojize(x)).apply(<span class="hljs-keyword">lambda</span> x: self.count_regex(<span class="hljs-string">r':[a-z_&amp;]+:'</span>, x))

        df = pd.DataFrame({<span class="hljs-string">'count_words'</span>: count_words
                           , <span class="hljs-string">'count_mentions'</span>: count_mentions
                           , <span class="hljs-string">'count_hashtags'</span>: count_hashtags
                           , <span class="hljs-string">'count_capital_words'</span>: count_capital_words
                           , <span class="hljs-string">'count_excl_quest_marks'</span>: count_excl_quest_marks
                           , <span class="hljs-string">'count_urls'</span>: count_urls
                           , <span class="hljs-string">'count_emojis'</span>: count_emojis
                          })

        <span class="hljs-keyword">return</span> df
tc = TextCounts()
df_eda = tc.fit_transform(df.text)
df_eda[<span class="hljs-string">'airline_sentiment'</span>] = df.airline_sentiment
</code></pre>
<p>It could be interesting to see how the TextStats variables relate to the class variable. So we write a function <code>show_dist</code> that provides descriptive statistics and a plot per target class.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">show_dist</span>(<span class="hljs-params">df, col</span>):</span>
    print(<span class="hljs-string">'Descriptive stats for {}'</span>.format(col))
    print(<span class="hljs-string">'-'</span>*(len(col)+<span class="hljs-number">22</span>))
    print(df.groupby(<span class="hljs-string">'airline_sentiment'</span>)[col].describe())
    bins = np.arange(df[col].min(), df[col].max() + <span class="hljs-number">1</span>)
    g = sns.FacetGrid(df, col=<span class="hljs-string">'airline_sentiment'</span>, size=<span class="hljs-number">5</span>, hue=<span class="hljs-string">'airline_sentiment'</span>, palette=<span class="hljs-string">"PuBuGn_d"</span>)
    g = g.map(sns.distplot, col, kde=<span class="hljs-literal">False</span>, norm_hist=<span class="hljs-literal">True</span>, bins=bins)
    plt.show()
</code></pre>
<p>Below you can find the distribution of the number of words in a tweet per target class. For brevity, we will limit us to only this variable. The charts for all TextCounts variables are in the <a target="_blank" href="https://github.com/bertcarremans/TwitterUSAirlineSentiment">notebook on Github</a>.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*snmvA3GQOb_S9wV8.png" alt="Image" width="1060" height="340" loading="lazy"></p>
<ul>
<li>The number of words used in the tweets is rather low. The largest number of words is 36 and there are even tweets with only 2 words. So we’ll have to be careful during data cleaning not to remove too many words. But the text processing will be faster. Negative tweets contain more words than neutral or positive tweets.</li>
<li>All tweets have at least one mention. This is the result of extracting the tweets based on mentions in the Twitter data. There seems to be no difference in the number of mentions with regard to the sentiment.</li>
<li>Most of the tweets do not contain hash tags. So this variable will not be retained during model training. Again, no difference in the number of hash tags with regard to the sentiment.</li>
<li>Most of the tweets do not contain capitalized words and we do not see a difference in distribution between the sentiments.</li>
<li>The positive tweets seem to be using a bit more exclamation or question marks.</li>
<li>Most tweets do not contain a URL.</li>
<li>Most tweets do not use emojis.</li>
</ul>
<h2 id="heading-text-cleaning">Text Cleaning</h2>
<p>Before we start using the tweets’ text we need to clean it. We’ll do the this in the class <code>CleanText</code><strong>.</strong> With this class we’ll perform the following actions:</p>
<ul>
<li>remove the mentions, as we want to generalize to tweets of other airline companies too.</li>
<li>remove the hash tag sign (#) but not the actual tag as this may contain information</li>
<li>set all words to lowercase</li>
<li>remove all punctuations, including the question and exclamation marks</li>
<li>remove the URLs as they do not contain useful information. We did not notice a difference in the number of URLs used between the sentiment classes</li>
<li>make sure to convert the emojis into one word.</li>
<li>remove digits</li>
<li>remove stopwords</li>
<li>apply the <code>PorterStemmer</code> to keep the stem of the words</li>
</ul>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CleanText</span>(<span class="hljs-params">BaseEstimator, TransformerMixin</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_mentions</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-keyword">return</span> re.sub(<span class="hljs-string">r'@\w+'</span>, <span class="hljs-string">''</span>, input_text)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_urls</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-keyword">return</span> re.sub(<span class="hljs-string">r'http.?://[^\s]+[\s]?'</span>, <span class="hljs-string">''</span>, input_text)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">emoji_oneword</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-comment"># By compressing the underscore, the emoji is kept as one word</span>
        <span class="hljs-keyword">return</span> input_text.replace(<span class="hljs-string">'_'</span>,<span class="hljs-string">''</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_punctuation</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-comment"># Make translation table</span>
        punct = string.punctuation
        trantab = str.maketrans(punct, len(punct)*<span class="hljs-string">' '</span>)  <span class="hljs-comment"># Every punctuation symbol will be replaced by a space</span>
        <span class="hljs-keyword">return</span> input_text.translate(trantab)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_digits</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-keyword">return</span> re.sub(<span class="hljs-string">'\d+'</span>, <span class="hljs-string">''</span>, input_text)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">to_lower</span>(<span class="hljs-params">self, input_text</span>):</span>
        <span class="hljs-keyword">return</span> input_text.lower()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_stopwords</span>(<span class="hljs-params">self, input_text</span>):</span>
        stopwords_list = stopwords.words(<span class="hljs-string">'english'</span>)
        <span class="hljs-comment"># Some words which might indicate a certain sentiment are kept via a whitelist</span>
        whitelist = [<span class="hljs-string">"n't"</span>, <span class="hljs-string">"not"</span>, <span class="hljs-string">"no"</span>]
        words = input_text.split() 
        clean_words = [word <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> words <span class="hljs-keyword">if</span> (word <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> stopwords_list <span class="hljs-keyword">or</span> word <span class="hljs-keyword">in</span> whitelist) <span class="hljs-keyword">and</span> len(word) &gt; <span class="hljs-number">1</span>] 
        <span class="hljs-keyword">return</span> <span class="hljs-string">" "</span>.join(clean_words) 

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stemming</span>(<span class="hljs-params">self, input_text</span>):</span>
        porter = PorterStemmer()
        words = input_text.split() 
        stemmed_words = [porter.stem(word) <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> words]
        <span class="hljs-keyword">return</span> <span class="hljs-string">" "</span>.join(stemmed_words)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fit</span>(<span class="hljs-params">self, X, y=None, **fit_params</span>):</span>
        <span class="hljs-keyword">return</span> self

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">transform</span>(<span class="hljs-params">self, X, **transform_params</span>):</span>
        clean_X = X.apply(self.remove_mentions).apply(self.remove_urls).apply(self.emoji_oneword).apply(self.remove_punctuation).apply(self.remove_digits).apply(self.to_lower).apply(self.remove_stopwords).apply(self.stemming)
        <span class="hljs-keyword">return</span> clean_X
</code></pre>
<p>To show how the cleaned text variable will look like, here’s a sample.</p>
<pre><code class="lang-python">ct = CleanText()
sr_clean = ct.fit_transform(df.text)
sr_clean.sample(<span class="hljs-number">5</span>)
</code></pre>
<blockquote>
<p><em>glad rt bet bird wish flown south winter</em><br><em>point upc code check baggag tell luggag vacat day tri swimsuit</em><br><em>vx jfk la dirti plane not standard</em><br><em>tell mean work need estim time arriv pleas need laptop work thank</em><br><em>sure busi go els airlin travel name kathryn sotelo</em></p>
</blockquote>
<p>One side-effect of text cleaning is that some rows do not have any words left in their text. For the <code>CountVectorizer</code> and <code>TfIdfVectorizer</code> this does not pose a problem. Yet, for the <code>Word2Vec</code> algorithm this causes an error. There are different strategies to deal with these missing values.</p>
<ul>
<li>Remove the complete row, but in a production environment this is not desirable.</li>
<li>Impute the missing value with some placeholder text like <em>[no_text]</em></li>
<li>When applying Word2Vec: use the average of all vectors</li>
</ul>
<p>Here we will impute with placeholder text.</p>
<pre><code class="lang-python">empty_clean = sr_clean == <span class="hljs-string">''</span>
print(<span class="hljs-string">'{} records have no words left after text cleaning'</span>.format(sr_clean[empty_clean].count()))
sr_clean.loc[empty_clean] = <span class="hljs-string">'[no_text]'</span>
</code></pre>
<p>Now that we have the cleaned text of the tweets, we can have a look at what are the most frequent words. Below we’ll show the top 20 words. The most frequent word is “flight”.</p>
<pre><code class="lang-python">cv = CountVectorizer()
bow = cv.fit_transform(sr_clean)
word_freq = dict(zip(cv.get_feature_names(), np.asarray(bow.sum(axis=<span class="hljs-number">0</span>)).ravel()))
word_counter = collections.Counter(word_freq)
word_counter_df = pd.DataFrame(word_counter.most_common(<span class="hljs-number">20</span>), columns = [<span class="hljs-string">'word'</span>, <span class="hljs-string">'freq'</span>])
fig, ax = plt.subplots(figsize=(<span class="hljs-number">12</span>, <span class="hljs-number">10</span>))
sns.barplot(x=<span class="hljs-string">"word"</span>, y=<span class="hljs-string">"freq"</span>, data=word_counter_df, palette=<span class="hljs-string">"PuBuGn_d"</span>, ax=ax)
plt.show();
</code></pre>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*hBvkYfey1Astmd02.png" alt="Image" width="738" height="595" loading="lazy"></p>
<h2 id="heading-creating-test-data">Creating test data</h2>
<p>To check the performance of the models we’ll need a test set. Evaluating on the train data would not be correct. You should not test on the same data used for training the model.</p>
<p>First, we combine the <code>TextCounts</code> variables with the <code>CleanText</code> variable. Initially, I made the mistake to execute TextCounts and CleanText in the <code>GridSearchCV</code>. This took too long as it applies these functions each run of the GridSearch. It suffices to run them only once.</p>
<pre><code class="lang-python">df_model = df_eda
df_model[<span class="hljs-string">'clean_text'</span>] = sr_clean
df_model.columns.tolist()
</code></pre>
<p>So <code>df_model</code> now contains several variables. But our vectorizers (see below) will only need the <code>clean_text</code> variable. The <code>TextCounts</code>variables can be added as such. To select columns, I wrote the class <code>ColumnExtractor</code> below.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ColumnExtractor</span>(<span class="hljs-params">TransformerMixin, BaseEstimator</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, cols</span>):</span>
        self.cols = cols
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">transform</span>(<span class="hljs-params">self, X, **transform_params</span>):</span>
        <span class="hljs-keyword">return</span> X[self.cols]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fit</span>(<span class="hljs-params">self, X, y=None, **fit_params</span>):</span>
        <span class="hljs-keyword">return</span> self
X_train, X_test, y_train, y_test = train_test_split(df_model.drop(<span class="hljs-string">'airline_sentiment'</span>, axis=<span class="hljs-number">1</span>), df_model.airline_sentiment, test_size=<span class="hljs-number">0.1</span>, random_state=<span class="hljs-number">37</span>)
</code></pre>
<h2 id="heading-hyperparameter-tuning-and-cross-validation">Hyperparameter tuning and cross-validation</h2>
<p>As we will see below, the vectorizers and classifiers all have configurable parameters. To choose the best parameters, we need to test on a separate validation set. This validation set was not used during the training. Yet, using only one validation set may not produce reliable validation results. Due to chance, you might have a good model performance on the validation set. If you would split the data otherwise, you might end up with other results. To get a more accurate estimation, we perform cross-validation.</p>
<p>With cross-validation we split the data into a train and validation set many times. The evaluation metric is then averaged over the different folds. Luckily, GridSearchCV applies cross-validation out-of-the-box.</p>
<p>To find the best parameters for both a vectorizer and classifier, we create a <code>Pipeline</code>.</p>
<h2 id="heading-evaluation-metrics">Evaluation metrics</h2>
<p>By default GridSearchCV uses the default scorer to compute the <code>best_score_</code>. For both the <code>MultiNomialNb</code> and <code>LogisticRegression</code> this default scoring metric is accuracy.</p>
<p>In our function <code>grid_vect</code>we additionally generate the <code>classification_report</code> on the test data. This provides some interesting metrics per target class. This might be more appropriate here. These metrics are the precision, recall and F1 score<strong>.</strong></p>
<ul>
<li>Precision<strong>:</strong> Of all rows we predicted to be a certain class, how many did we correctly predict?</li>
<li>Recall<strong>:</strong> Of all rows of a certain class, how many did we correctly predict?</li>
<li>F1 score<strong>:</strong> Harmonic mean of Precision and Recall.</li>
</ul>
<p>With the elements of the <a target="_blank" href="https://en.wikipedia.org/wiki/Confusion_matrix">confusion matrix</a> we can calculate Precision and Recall.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Based on http://scikit-learn.org/stable/auto_examples/model_selection/grid_search_text_feature_extraction.html</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">grid_vect</span>(<span class="hljs-params">clf, parameters_clf, X_train, X_test, parameters_text=None, vect=None, is_w2v=False</span>):</span>

    textcountscols = [<span class="hljs-string">'count_capital_words'</span>,<span class="hljs-string">'count_emojis'</span>,<span class="hljs-string">'count_excl_quest_marks'</span>,<span class="hljs-string">'count_hashtags'</span>
                      ,<span class="hljs-string">'count_mentions'</span>,<span class="hljs-string">'count_urls'</span>,<span class="hljs-string">'count_words'</span>]

    <span class="hljs-keyword">if</span> is_w2v:
        w2vcols = []
        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(SIZE):
            w2vcols.append(i)
        features = FeatureUnion([(<span class="hljs-string">'textcounts'</span>, ColumnExtractor(cols=textcountscols))
                                 , (<span class="hljs-string">'w2v'</span>, ColumnExtractor(cols=w2vcols))]
                                , n_jobs=<span class="hljs-number">-1</span>)
    <span class="hljs-keyword">else</span>:
        features = FeatureUnion([(<span class="hljs-string">'textcounts'</span>, ColumnExtractor(cols=textcountscols))
                                 , (<span class="hljs-string">'pipe'</span>, Pipeline([(<span class="hljs-string">'cleantext'</span>, ColumnExtractor(cols=<span class="hljs-string">'clean_text'</span>)), (<span class="hljs-string">'vect'</span>, vect)]))]
                                , n_jobs=<span class="hljs-number">-1</span>)

    pipeline = Pipeline([
        (<span class="hljs-string">'features'</span>, features)
        , (<span class="hljs-string">'clf'</span>, clf)
    ])

    <span class="hljs-comment"># Join the parameters dictionaries together</span>
    parameters = dict()
    <span class="hljs-keyword">if</span> parameters_text:
        parameters.update(parameters_text)
    parameters.update(parameters_clf)
    <span class="hljs-comment"># Make sure you have scikit-learn version 0.19 or higher to use multiple scoring metrics</span>
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=<span class="hljs-number">-1</span>, verbose=<span class="hljs-number">1</span>, cv=<span class="hljs-number">5</span>)

    print(<span class="hljs-string">"Performing grid search..."</span>)
    print(<span class="hljs-string">"pipeline:"</span>, [name <span class="hljs-keyword">for</span> name, _ <span class="hljs-keyword">in</span> pipeline.steps])
    print(<span class="hljs-string">"parameters:"</span>)
    pprint(parameters)
    t0 = time()
    grid_search.fit(X_train, y_train)
    print(<span class="hljs-string">"done in %0.3fs"</span> % (time() - t0))
    print()
    print(<span class="hljs-string">"Best CV score: %0.3f"</span> % grid_search.best_score_)
    print(<span class="hljs-string">"Best parameters set:"</span>)
    best_parameters = grid_search.best_estimator_.get_params()
    <span class="hljs-keyword">for</span> param_name <span class="hljs-keyword">in</span> sorted(parameters.keys()):
        print(<span class="hljs-string">"\t%s: %r"</span> % (param_name, best_parameters[param_name]))

    print(<span class="hljs-string">"Test score with best_estimator_: %0.3f"</span> % grid_search.best_estimator_.score(X_test, y_test))
    print(<span class="hljs-string">"\n"</span>)
    print(<span class="hljs-string">"Classification Report Test Data"</span>)
    print(classification_report(y_test, grid_search.best_estimator_.predict(X_test)))

    <span class="hljs-keyword">return</span> grid_search
</code></pre>
<h2 id="heading-parameter-grids-for-gridsearchcv">Parameter grids for GridSearchCV</h2>
<p>In the grid search, we will investigate the performance of the classifier. The set of parameters used to test the performance are specified below.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Parameter grid settings for the vectorizers (Count and TFIDF)</span>
parameters_vect = {
    <span class="hljs-string">'features__pipe__vect__max_df'</span>: (<span class="hljs-number">0.25</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">0.75</span>),
    <span class="hljs-string">'features__pipe__vect__ngram_range'</span>: ((<span class="hljs-number">1</span>, <span class="hljs-number">1</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)),
    <span class="hljs-string">'features__pipe__vect__min_df'</span>: (<span class="hljs-number">1</span>,<span class="hljs-number">2</span>)
}

<span class="hljs-comment"># Parameter grid settings for MultinomialNB</span>
parameters_mnb = {
    <span class="hljs-string">'clf__alpha'</span>: (<span class="hljs-number">0.25</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">0.75</span>)
}

<span class="hljs-comment"># Parameter grid settings for LogisticRegression</span>
parameters_logreg = {
    <span class="hljs-string">'clf__C'</span>: (<span class="hljs-number">0.25</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">1.0</span>),
    <span class="hljs-string">'clf__penalty'</span>: (<span class="hljs-string">'l1'</span>, <span class="hljs-string">'l2'</span>)
}
</code></pre>
<h2 id="heading-classifiers">Classifiers</h2>
<p>Here we will compare the performance of a <code>MultinomialNB</code>and <code>LogisticRegression</code>.</p>
<pre><code class="lang-python">mnb = MultinomialNB()
logreg = LogisticRegression()
</code></pre>
<h3 id="heading-countvectorizer">CountVectorizer</h3>
<p>To use words in a classifier, we need to convert the words to numbers. Sklearn’s <code>CountVectorizer</code> takes all words in all tweets, assigns an ID and counts the frequency of the word per tweet. We then use this bag of words as input for a classifier. This bag of words is a sparse data set. This means that each record will have many zeroes for the words not occurring in the tweet.</p>
<pre><code class="lang-python">countvect = CountVectorizer()
<span class="hljs-comment"># MultinomialNB</span>
best_mnb_countvect = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_mnb_countvect, <span class="hljs-string">'../output/best_mnb_countvect.pkl'</span>)
<span class="hljs-comment"># LogisticRegression</span>
best_logreg_countvect = grid_vect(logreg, parameters_logreg, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_logreg_countvect, <span class="hljs-string">'../output/best_logreg_countvect.pkl'</span>)
</code></pre>
<h3 id="heading-tf-idf-vectorizer">TF-IDF Vectorizer</h3>
<p>One issue with CountVectorizer is that there might be words that occur frequently. These words might not have discriminatory information. Thus they can be removed. <a target="_blank" href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">TF-IDF (term frequency — inverse document frequency)</a>can be used to down-weight these frequent words.</p>
<pre><code class="lang-python">tfidfvect = TfidfVectorizer()
<span class="hljs-comment"># MultinomialNB</span>
best_mnb_tfidf = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_mnb_tfidf, <span class="hljs-string">'../output/best_mnb_tfidf.pkl'</span>)
<span class="hljs-comment"># LogisticRegression</span>
best_logreg_tfidf = grid_vect(logreg, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_logreg_tfidf, <span class="hljs-string">'../output/best_logreg_tfidf.pkl'</span>)
</code></pre>
<h3 id="heading-word2vec">Word2Vec</h3>
<p>Another way of converting the words to numerical values is to use <code>Word2Vec</code>. Word2Vec maps each word in a multi-dimensional space. It does this by taking into account the context in which a word appears in the tweets. As a result, words that are similar are also close to each other in the multi-dimensional space.</p>
<p>The Word2Vec algorithm is part of the <a target="_blank" href="https://radimrehurek.com/gensim/models/word2vec.html">gensim</a> package.</p>
<p>The Word2Vec algorithm uses lists of words as input. For that purpose, we use the <code>word_tokenize</code> method of the the <code>nltk</code> package.</p>
<pre><code class="lang-python">SIZE = <span class="hljs-number">50</span>
X_train[<span class="hljs-string">'clean_text_wordlist'</span>] = X_train.clean_text.apply(<span class="hljs-keyword">lambda</span> x : word_tokenize(x))
X_test[<span class="hljs-string">'clean_text_wordlist'</span>] = X_test.clean_text.apply(<span class="hljs-keyword">lambda</span> x : word_tokenize(x))
model = gensim.models.Word2Vec(X_train.clean_text_wordlist
, min_count=<span class="hljs-number">1</span>
, size=SIZE
, window=<span class="hljs-number">5</span>
, workers=<span class="hljs-number">4</span>)
model.most_similar(<span class="hljs-string">'plane'</span>, topn=<span class="hljs-number">3</span>)
</code></pre>
<p>The Word2Vec model provides a vocabulary of the words in all the tweets. For each word you also have its vector values. The number of vector values is equal to the chosen size. These are the dimensions on which each word is mapped in the multi-dimensional space. Words with an occurrence less than <code>min_count</code> are not kept in the vocabulary.</p>
<p>A side effect of the min_count parameter is that some tweets could have no vector values. This is would be the case when the word(s) in the tweet occur in less than min_count tweets. Due to the small corpus of tweets, there is a risk of this happening in our case. Thus we set the min_count value equal to 1.</p>
<p>The tweets can have a different number of vectors, depending on the number of words it contains. To use this output for modeling we will calculate the average of all vectors per tweet. As such we will have the same number (i.e. size) of input variables per tweet.</p>
<p>We do this with the function <code>compute_avg_w2v_vector</code>. In this function we also check whether the words in the tweet occur in the vocabulary of the Word2Vec model. If not, a list filled with 0.0 is returned. Else the average of the word vectors.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compute_avg_w2v_vector</span>(<span class="hljs-params">w2v_dict, tweet</span>):</span>
    list_of_word_vectors = [w2v_dict[w] <span class="hljs-keyword">for</span> w <span class="hljs-keyword">in</span> tweet <span class="hljs-keyword">if</span> w <span class="hljs-keyword">in</span> w2v_dict.vocab.keys()]

    <span class="hljs-keyword">if</span> len(list_of_word_vectors) == <span class="hljs-number">0</span>:
        result = [<span class="hljs-number">0.0</span>]*SIZE
    <span class="hljs-keyword">else</span>:
        result = np.sum(list_of_word_vectors, axis=<span class="hljs-number">0</span>) / len(list_of_word_vectors)

    <span class="hljs-keyword">return</span> result
X_train_w2v = X_train[<span class="hljs-string">'clean_text_wordlist'</span>].apply(<span class="hljs-keyword">lambda</span> x: compute_avg_w2v_vector(model.wv, x))
X_test_w2v = X_test[<span class="hljs-string">'clean_text_wordlist'</span>].apply(<span class="hljs-keyword">lambda</span> x: compute_avg_w2v_vector(model.wv, x))
</code></pre>
<p>This gives us a Series with a vector of dimension equal to <code>SIZE</code>. Now we will split this vector and create a DataFrame with each vector value in a separate column. That way we can concatenate the Word2Vec variables to the other TextCounts variables. We need to reuse the index of <code>X_train</code> and <code>X_test</code>. Otherwise this will give issues (duplicates) in the concatenation later on.</p>
<pre><code class="lang-python">X_train_w2v = pd.DataFrame(X_train_w2v.values.tolist(), index= X_train.index)
X_test_w2v = pd.DataFrame(X_test_w2v.values.tolist(), index= X_test.index)
<span class="hljs-comment"># Concatenate with the TextCounts variables</span>
X_train_w2v = pd.concat([X_train_w2v, X_train.drop([<span class="hljs-string">'clean_text'</span>, <span class="hljs-string">'clean_text_wordlist'</span>], axis=<span class="hljs-number">1</span>)], axis=<span class="hljs-number">1</span>)
X_test_w2v = pd.concat([X_test_w2v, X_test.drop([<span class="hljs-string">'clean_text'</span>, <span class="hljs-string">'clean_text_wordlist'</span>], axis=<span class="hljs-number">1</span>)], axis=<span class="hljs-number">1</span>)
</code></pre>
<p>We only consider LogisticRegression as we have negative values in the Word2Vec vectors. MultinomialNB assumes that the variables have a <a target="_blank" href="https://en.wikipedia.org/wiki/Multinomial_distribution">multinomial distribution</a>. So they cannot contain negative values.</p>
<pre><code class="lang-python">best_logreg_w2v = grid_vect(logreg, parameters_logreg, X_train_w2v, X_test_w2v, is_w2v=<span class="hljs-literal">True</span>)
joblib.dump(best_logreg_w2v, <span class="hljs-string">'../output/best_logreg_w2v.pkl'</span>)
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<ul>
<li>Both classifiers achieve the best results when using the features of the CountVectorizer</li>
<li>Logistic Regression outperforms the Multinomial Naive Bayes classifier</li>
<li>The best performance on the test set comes from the LogisticRegression with features from CountVectorizer.</li>
</ul>
<h3 id="heading-best-parameters">Best parameters</h3>
<ul>
<li>C value of 1</li>
<li>L2 regularization</li>
<li>max_df: 0.5 or maximum document frequency of 50%.</li>
<li>min_df: 1 or the words need to appear in at least 2 tweets</li>
<li>ngram_range: (1, 2), both single words as bi-grams are used</li>
</ul>
<h3 id="heading-evaluation-metrics-1">Evaluation metrics</h3>
<ul>
<li>A test accuracy of 81,3%. This is better than a baseline performance of predicting the majority class (here a negative sentiment) for all observations. The baseline would give 63% accuracy.</li>
<li>The Precision is rather high for all three classes. For instance, of all cases that we predict as negative, 80% is negative.</li>
<li>The Recall for the neutral class is low. Of all neutral cases in our test data, we only predict 48% as being neutral.</li>
</ul>
<h2 id="heading-apply-the-best-model-on-new-tweets">Apply the best model on new tweets</h2>
<p>For the fun, we will use the best model and apply it to some new tweets that contain <em>@VirginAmerica</em>. I selected 3 negative and 3 positive tweets by hand.</p>
<p>Thanks to the GridSearchCV, we now know what are the best hyperparameters. So now we can train the best model on all training data, including the test data that we split off before.</p>
<pre><code class="lang-python">textcountscols = [<span class="hljs-string">'count_capital_words'</span>,<span class="hljs-string">'count_emojis'</span>,<span class="hljs-string">'count_excl_quest_marks'</span>,<span class="hljs-string">'count_hashtags'</span>
,<span class="hljs-string">'count_mentions'</span>,<span class="hljs-string">'count_urls'</span>,<span class="hljs-string">'count_words'</span>]
features = FeatureUnion([(<span class="hljs-string">'textcounts'</span>, ColumnExtractor(cols=textcountscols))
, (<span class="hljs-string">'pipe'</span>, Pipeline([(<span class="hljs-string">'cleantext'</span>, ColumnExtractor(cols=<span class="hljs-string">'clean_text'</span>))
, (<span class="hljs-string">'vect'</span>, CountVectorizer(max_df=<span class="hljs-number">0.5</span>, min_df=<span class="hljs-number">1</span>, ngram_range=(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>)))]))]
, n_jobs=<span class="hljs-number">-1</span>)
pipeline = Pipeline([
(<span class="hljs-string">'features'</span>, features)
, (<span class="hljs-string">'clf'</span>, LogisticRegression(C=<span class="hljs-number">1.0</span>, penalty=<span class="hljs-string">'l2'</span>))
])
best_model = pipeline.fit(df_model.drop(<span class="hljs-string">'airline_sentiment'</span>, axis=<span class="hljs-number">1</span>), df_model.airline_sentiment)
<span class="hljs-comment"># Applying on new positive tweets</span>
new_positive_tweets = pd.Series([<span class="hljs-string">"Thank you @VirginAmerica for you amazing customer support team on Tuesday 11/28 at @EWRairport and returning my lost bag in less than 24h! #efficiencyiskey #virginamerica"</span>
,<span class="hljs-string">"Love flying with you guys ask these years. Sad that this will be the last trip ? @VirginAmerica #LuxuryTravel"</span>
,<span class="hljs-string">"Wow @VirginAmerica main cabin select is the way to fly!! This plane is nice and clean &amp; I have tons of legroom! Wahoo! NYC bound! ✈️"</span>])
df_counts_pos = tc.transform(new_positive_tweets)
df_clean_pos = ct.transform(new_positive_tweets)
df_model_pos = df_counts_pos
df_model_pos[<span class="hljs-string">'clean_text'</span>] = df_clean_pos
best_model.predict(df_model_pos).tolist()
<span class="hljs-comment"># Applying on new negative tweets</span>
new_negative_tweets = pd.Series([<span class="hljs-string">"@VirginAmerica shocked my initially with the service, but then went on to shock me further with no response to what my complaint was. #unacceptable @Delta @richardbranson"</span>
,<span class="hljs-string">"@VirginAmerica this morning I was forced to repack a suitcase w a medical device because it was barely overweight - wasn't even given an option to pay extra. My spouses suitcase then burst at the seam with the added device and had to be taped shut. Awful experience so far!"</span>
,<span class="hljs-string">"Board airplane home. Computer issue. Get off plane, traverse airport to gate on opp side. Get on new plane hour later. Plane too heavy. 8 volunteers get off plane. Ohhh the adventure of travel ✈️ @VirginAmerica"</span>])
df_counts_neg = tc.transform(new_negative_tweets)
df_clean_neg = ct.transform(new_negative_tweets)
df_model_neg = df_counts_neg
df_model_neg[<span class="hljs-string">'clean_text'</span>] = df_clean_neg
best_model.predict(df_model_neg).tolist()
</code></pre>
<p>The model classifies all tweets correctly. A larger test set should be used to assess the model’s performance. But on this small data set it does what we are aiming for. I hope you enjoyed reading this story. If you did, feel free to share it.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to extract keywords from text with TF-IDF and Python’s Scikit-Learn ]]>
                </title>
                <description>
                    <![CDATA[ By Kavita Ganesan Back in 2006, when I had to use TF-IDF for keyword extraction in Java, I ended up writing all of the code from scratch. Neither Data Science nor GitHub were a thing back then and libraries were just limited. The world is much differ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-extract-keywords-from-text-with-tf-idf-and-pythons-scikit-learn-b2a0f3d7e667/</link>
                <guid isPermaLink="false">66c351ec765a634c3485fe16</guid>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ naturallanguageprocessing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ text mining ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 07 Mar 2019 18:00:31 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*PWe5w-WzMaP14YR3lzLIUA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Kavita Ganesan</p>
<p>Back in 2006, when I had to use TF-IDF for keyword extraction in Java, I ended up writing all of the code from scratch. Neither Data Science nor GitHub were a thing back then and libraries were just limited.</p>
<p>The world is much different today. You have several <a target="_blank" href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html#sklearn.feature_extraction.text.TfidfTransformer">libraries</a> and <a target="_blank" href="https://github.com/topics/tf-idf?o=desc&amp;s=forks">open-source code repositories on Github</a> that provide a decent implementation of TF-IDF. If you don’t need a lot of control over how the TF-IDF math is computed, I highly recommend re-using libraries from known packages such as <a target="_blank" href="https://spark.apache.org/docs/2.2.0/mllib-feature-extraction.html">Spark’s MLLib</a> or <a target="_blank" href="http://scikit-learn.org/stable/">Python’s scikit-learn</a>.</p>
<p>The <strong>one problem</strong> that I noticed with these libraries is that they are meant as a pre-step for other tasks like clustering, topic modeling, and text classification. <a target="_blank" href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">TF-IDF</a> can actually be used to extract important keywords from a document to get a sense of what characterizes a document. For example, if you are dealing with Wikipedia articles, you can use tf-idf to extract words that are unique to a given article. These keywords can be used as a very simple summary of a document, and for text-analytics when we look at these keywords in aggregate.</p>
<p><strong>In this article</strong>, I will show you how you can use scikit-learn to extract keywords from documents using TF-IDF. We will specifically do this on a stack overflow dataset. If you want access to the <strong>full Jupyter Notebook</strong>, please <a target="_blank" href="https://github.com/kavgan/data-science-tutorials/tree/master/tf-idf">head over to my repo</a>.</p>
<p><strong>Important note:</strong> I’m assuming that folks following this tutorial are already familiar with the concept of TF-IDF. If you are not, please familiarize yourself with the concept before reading on. There are a couple of <a target="_blank" href="https://www.youtube.com/results?search_query=tf-idf.">videos online</a> that give an intuitive explanation of what it is. For a more academic explanation I would recommend my <a target="_blank" href="https://www.coursera.org/lecture/text-retrieval/lesson-2-2-tf-transformation-W0NZe">Ph.D advisor’s explanation</a>.</p>
<h3 id="heading-dataset">Dataset</h3>
<p>In this example, we will be using a Stack Overflow dataset which is a bit noisy and simulates what you could be dealing with in real life. You can find this dataset in <a target="_blank" href="https://github.com/kavgan/data-science-tutorials/tree/master/tf-idf/data">my tutorial repo</a>.</p>
<p>Notice that there are <strong>two files</strong>. The larger file, <code>stackoverflow-data-idf.json</code> with 20,000 posts, is used to compute the Inverse Document Frequency (IDF). The smaller file, <code>stackoverflow-test.json</code> with 500 posts, would be used as a test set for us to extract keywords from. This dataset is based on the publicly available <a target="_blank" href="https://cloud.google.com/bigquery/public-data/stackoverflow">Stack Overflow dump from Google’s Big Query</a>.</p>
<p>Let’s take a peek at our dataset. The code below reads a one per line json string from <code>data/stackoverflow-data-idf.json</code> into a pandas data frame and prints out its schema and total number of posts.</p>
<p>Here, <code>lines=True</code> simply means we are treating each line in the text file as a separate json string.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/WHKg5Ngu5mwuBeI4y5UIcysbPM2XDdqMimnL" alt="Image" width="800" height="188" loading="lazy">
<em>Read the json file and print out schema and total number of Stack Overflow posts.</em></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/YAXt6ZDQTvw4R5gWJrtC0UhEqxfmEswodALs" alt="Image" width="800" height="498" loading="lazy">
<em>The schema and total number of posts.</em></p>
<p>Notice that this Stack Overflow dataset contains 19 fields including post title, body, tags, dates, and other metadata which we don’t need for this tutorial. For this tutorial, we are mostly interested in the body and title. These will become our source of text for keyword extraction.</p>
<p>We will now create a field that combines both <code>body</code> and <code>title</code> so we have the two in one field. We will also print the second text entry in our new field just to see what the text looks like.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/QTx3-ZVcC-v2478agne5hwk7UIwH-OvmeigO" alt="Image" width="800" height="596" loading="lazy"></p>
<p>Uh oh, this doesn’t look very readable! Well, that’s because of all the cleaning that went on in <code>pre_process(..)</code>. You can do a lot more stuff in <code>pre_process(..)</code>, such as eliminate all code sections, and normalize the words to its root. For simplicity, we will perform only some mild pre-processing.</p>
<h3 id="heading-creating-vocabulary-and-word-counts-for-the-idf">Creating vocabulary and word counts for the IDF</h3>
<p>We now need to create the vocabulary and start the counting process. We can use the <a target="_blank" href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html">CountVectorizer</a> to create a vocabulary from all the text in our <code>df_idf['text']</code> , followed by the counts of words in the vocabulary:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Vz6XrDGC4Zu6h1JK9xcgu4jTJOn7kpyl-wtB" alt="Image" width="800" height="480" loading="lazy"></p>
<p>The result of the last two lines from the code above is a sparse matrix representation of the counts. Each column represents a word in the vocabulary. Each row represents the document in our dataset, where the values are the word counts.</p>
<p><strong>Note</strong> that, with this representation, counts of some words could be 0 if the word did not appear in the corresponding document.</p>
<p>Here we are passing two parameters to CountVectorizer, <code>max_df</code> and <code>stop_words</code>. The first is just to ignore all words that have appeared in 85% of the documents, since those may be unimportant. The later is a custom stop words list. You can also use stop words that are native to sklearn by setting <code>stop_words='english'</code>. The stop word list used for this tutorial can be found <a target="_blank" href="https://github.com/kavgan/data-science-tutorials/tree/master/tf-idf/resources">here</a>.</p>
<p>The resulting shape of <code>word_count_vector</code> is (20000,124901) since we have 20,000 documents in our dataset (the rows) and the vocabulary size is 124,901.</p>
<p>In some text mining applications, such as clustering and text classification, we typically limit the size of the vocabulary. It’s really easy to do this by setting <code>max_features=vocab_size</code> when instantiating CountVectorizer. For this tutorial let’s limit our vocabulary size to 10,000:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/pLhHU6UYk86G5P5KUoJ87sc6cYIKnTmDPiuB" alt="Image" width="800" height="126" loading="lazy"></p>
<p>Now, let’s look at 10 words from our vocabulary:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/SMqynykFACPWIGAlcDN0PQV2vaBw9dAwfA3a" alt="Image" width="800" height="112" loading="lazy"></p>
<pre><code>[<span class="hljs-string">'serializing'</span>, <span class="hljs-string">'private'</span>, <span class="hljs-string">'struct'</span>, <span class="hljs-string">'public'</span>, <span class="hljs-string">'class'</span>, <span class="hljs-string">'contains'</span>, <span class="hljs-string">'properties'</span>, <span class="hljs-string">'string'</span>, <span class="hljs-string">'serialize'</span>, <span class="hljs-string">'attempt'</span>]
</code></pre><p>Sweet, these are mostly programming-related.</p>
<h3 id="heading-tfidftransformer-to-compute-the-idf">TfidfTransformer to compute the IDF</h3>
<p>It’s now time to compute the IDF values.</p>
<p>In the code below, we are essentially taking the sparse matrix from CountVectorizer (<code>word_count_vector</code>) to generate the IDF when you invoke <code>fit(...)</code> :</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/NpfT2hZD8VVfv2wR0hnGrLRfoOjLi0vtX75f" alt="Image" width="800" height="154" loading="lazy"></p>
<p><strong>Extremely important point</strong>: the IDF should always be based on a large corpora, and should be representative of texts you would be using to extract keywords. I’ve seen several articles on the Web that compute the IDF using a handful of documents. You will <strong>defeat the whole purpose</strong> of IDF weighting if its not based on a large corpora as:</p>
<ol>
<li>your vocabulary becomes too small, and</li>
<li>you have limited ability to observe the behavior of words that you do know about.</li>
</ol>
<h3 id="heading-computing-tf-idf-and-extracting-keywords">Computing TF-IDF and extracting keywords</h3>
<p>Once we have our IDF computed, we are ready to compute TF-IDF and then extract top keywords from the TF-IDF vectors.</p>
<p>In this example, we will extract the top keywords for the questions in <code>data/stackoverflow-test.json</code>. This data file has 500 questions with fields identical to that of <code>data/stackoverflow-data-idf.json</code> as we saw above. We will start by reading our test file, extracting the necessary fields — title and body — and getting the texts into a list.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/KnapC5ZcFkMU4dGz3VttCj0gZBPvIVs9nQoO" alt="Image" width="800" height="217" loading="lazy"></p>
<p>The next step is to compute the tf-idf value for a given document in our test set by invoking <code>tfidf_transformer.transform(...)</code>. This generates a vector of tf-idf scores.</p>
<p>Next, we sort the words in the vector in <strong>descending</strong> order of tf-idf values and then iterate over to extract the top-n keywords. In the example below, we are extracting keywords for the first document in our test set.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/88UsQ05S1GAomTM7V03RNGR3MUm9z5l6n-Jx" alt="Image" width="800" height="472" loading="lazy"></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/gfoZfIRRVeyBXoAMNnmgP44BKtEjeVCPTalf" alt="Image" width="800" height="567" loading="lazy"></p>
<p>The <code>sort_coo(...)</code> method essentially sorts the values in the vector while preserving the column index. Once you have the column index then it’s really easy to look-up the corresponding word value as you would see in <code>extract_topn_from_vector(...)</code> where we do <code>feature_vals.append(feature_names[idx])</code>.</p>
<h3 id="heading-some-results">Some results!</h3>
<p>In this section, you will see the stack overflow question followed by the corresponding extracted keywords.</p>
<h4 id="heading-question-about-eclipse-plugin-integration">Question about Eclipse Plugin integration</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/fLsoeShixrFFLm-y5IVDzJgPjZzWTO9f360B" alt="Image" width="800" height="303" loading="lazy">
<em>Actual extracted keywords.</em></p>
<p>From the keywords above, the top keywords actually make sense, it talks about <code>eclipse</code>, <code>maven</code>, <code>integrate</code>, <code>war</code>, and <code>tomcat</code>, which are all unique to this specific question.</p>
<p>There are a couple of keywords that could have been eliminated such as <code>possibility</code> and perhaps even <code>project</code>. You can do this by adding more common words to your stop list. You can even create your own set of stop list, <a target="_blank" href="http://kavita-ganesan.com/tips-for-constructing-custom-stop-word-lists/">very specific to your domain</a>.</p>
<p>Now let’s look at another example.</p>
<h4 id="heading-question-about-sql-import">Question about SQL import</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/uTTNMaq3bVfhUEU2q1Ua4VX83KBpbDzWbBtv" alt="Image" width="800" height="531" loading="lazy">
<em>Actual extracted keywords</em></p>
<p>Even with all the html tags, because of the pre-processing, we are able to extract some pretty nice keywords here. The last word <code>appropriately</code> would qualify as a stop word. You can keep running different examples to get ideas of how to fine-tune the results.</p>
<p>Voilà! Now you can extract important keywords from any type of text!</p>
<h3 id="heading-resources">Resources</h3>
<ul>
<li><a target="_blank" href="https://github.com/kavgan/data-science-tutorials/tree/master/tf-idf/">Full source code and dataset for this tutorial</a></li>
<li>Stack overflow data on <a target="_blank" href="https://cloud.google.com/bigquery/public-data/stackoverflow">Google’s BigQuery</a></li>
</ul>
<p><a target="_blank" href="http://kavita-ganesan.com/subscribe/#.XGs_lpNKigQ">Follow my blog</a> to learn more Text Mining, NLP and Machine Learning from an applied perspective.</p>
<p>This article was originally published at <a target="_blank" href="http://kavita-ganesan.com/extracting-keywords-from-text-with-tf-idf-and-pythons-scikit-learn/">kavita-ganesan.com</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ An easy way to make word clouds for data scientists ]]>
                </title>
                <description>
                    <![CDATA[ By Kavita Ganesan About a year ago, I looked high and low for a Python word cloud library that I could use from within my Jupyter notebook. I needed it to be flexible enough to use counts or tfidf when needed or just accept a set of words and corresp... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/word-cloud-for-data-scientists-76b8a907e04e/</link>
                <guid isPermaLink="false">66c3678663ac6ce6ab8eba3c</guid>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ text mining ]]>
                    </category>
                
                    <category>
                        <![CDATA[ visualization ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 23 Oct 2018 05:24:33 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*HYtC28uzCWtTK2r_cR_3CA.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Kavita Ganesan</p>
<p>About a year ago, I looked high and low for a Python word cloud library that I could use from within my Jupyter notebook. I needed it to be flexible enough to use <code>counts</code> or <code>tfidf</code> when needed or just accept a set of words and corresponding weights.</p>
<p>I was a bit surprised that something like that did not already exist within libraries like <code>plotly</code>. All I wanted to do was to get a quick understanding of my text data and word vectors. I thought that was probably not too much to ask…</p>
<p>Here I am a year later, using my own <a target="_blank" href="https://github.com/kavgan/word_cloud">word_cloud</a> visualization library. Its not the prettiest or the most sophisticated, but it works for most cases. I decided to share it, so that others could use it as well. After <a target="_blank" href="https://github.com/kavgan/word_cloud">installation</a>, here are a few ways you can use it.</p>
<h4 id="heading-generate-word-clouds-with-a-single-text-document">Generate word clouds with a single text document</h4>
<p>This example show examples of how you can generate word clouds with just one document. While the colors can be randomized, in this example, the colors are based on the default color settings.</p>
<p>By default, the words are weighted by word counts unless you explicitly ask for tfidf weighting. Tfidf weighting makes sense only if you have a lot of documents to start with.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*HYtC28uzCWtTK2r_cR_3CA.png" alt="Image" width="800" height="743" loading="lazy">
<em>word cloud based on a single document</em></p>
<h4 id="heading-generate-word-clouds-from-multiple-documents">Generate word clouds from multiple documents</h4>
<p>Let’s say you have 100 documents from one news category, and you just want to see what the common mentions are.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*lKQBi3n4OfjawldVtXrU5g.png" alt="Image" width="800" height="844" loading="lazy">
<em>word cloud based on multiple documents</em></p>
<h4 id="heading-generate-word-clouds-from-existing-weights">Generate word clouds from existing weights</h4>
<p>Let’s say you have a set of words with corresponding weights, and you just want to visualize it. All you need to do is make sure that the weights are normalized between [0 - 1].</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*NyGmBmZ4OOiOPLir9h4doA.png" alt="Image" width="800" height="579" loading="lazy">
<em>word cloud from existing weights</em></p>
<p>Hope you find this useful! Please feel free to propose changes to prettify the output - just open a pull request with your changes.</p>
<h3 id="heading-links">Links</h3>
<ul>
<li><a target="_blank" href="https://colab.research.google.com/drive/1AkdUKEFmaYom77r6KPh18jdQrplIQbKQ">See my Jupyter notebook with code examples</a></li>
<li><a target="_blank" href="https://github.com/kavgan/word_cloud">Start using word_cloud library</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Quick tips for constructing custom stop word lists ]]>
                </title>
                <description>
                    <![CDATA[ By Kavita Ganesan In natural language processing (NLP) and text mining applications, stop words are used to eliminate unimportant words, allowing applications to focus on the important words instead. Stop words are a set of commonly used words in any... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/quick-tips-for-constructing-custom-stop-word-lists-c22b40a25169/</link>
                <guid isPermaLink="false">66c35d31ee410eea7c0987e2</guid>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nlp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ text mining ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 26 Jul 2018 19:54:51 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*eAdqKoWkI9p3NnQdx94yHw.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Kavita Ganesan</p>
<p>In natural language processing (NLP) and text mining applications, stop words are used to eliminate unimportant words, allowing applications to focus on the important words instead.</p>
<p>Stop words are a set of commonly used words in any language. For example, in English, “the”, “is”, and “and” would easily qualify as stop words.</p>
<p>While there are various published stop words that one can use, in many cases these stop words are insufficient as they are not domain-specific. For example, in clinical texts, terms like “mcg” “dr.” and “patient” occur in almost every document that you come across. So, these terms may be regarded as potential stop words for clinical text mining and retrieval.</p>
<p>Similarly, for tweets, terms like “#”, “RT”, “@username” can be potentially regarded as stop words. The common language specific stop word list generally <strong>does not</strong> cover such domain-specific terms.</p>
<p>The good news is that it is actually fairly easy to construct your own domain-specific stop word list. Here are a few ways of doing it assuming you have a large corpus of text from the domain of interest, you can do one or more of the following to curate your stop word list:</p>
<h3 id="heading-1-most-frequent-terms-as-stop-words">1. Most frequent terms as stop words</h3>
<p>Sum the term frequencies of each unique word (<strong>w</strong>) across all documents in your collection. Sort the terms in descending order of raw term frequency. You can take the top <strong>K</strong> terms to be your stop words.</p>
<p>You can also eliminate common English words (using a published stop list) prior to sorting so that you target the domain-specific stop words.</p>
<p>Another option is to treat words occurring in more <strong>X%</strong> of your documents as stop words. I have found eliminating words that appear in <strong>85%</strong> of documents to be effective in several text mining tasks. The benefit of this approach is that it is really easy to implement. The downside, however, is if you have a particularly long document, the raw term frequency from just a few documents can dominate and cause the term to be at the top. One way to resolve this is to normalize the raw term frequency using a normalizer such as the document length — the number of words in a given document.</p>
<h3 id="heading-2-least-frequent-terms-as-stop-words">2. Least frequent terms as stop words</h3>
<p>Just as terms that are extremely frequent could be distracting terms rather than discriminating terms, terms that are extremely infrequent may also not be useful for text mining and retrieval. For example, the username “@username” that occurs only once in a collection of tweets, may not be very useful. Other terms like “yoMateZ!” which could just be made-up terms by people again may not be useful for text mining applications.</p>
<p><strong>Note</strong>: certain terms like “yaaaaayy!!” can often be normalized to standard forms such as “yay”.</p>
<p>However, despite all the normalization, if a term still has a frequency count of one you could remove it. This could significantly reduce your overall feature space.</p>
<h3 id="heading-3-low-idf-terms-as-stop-words">3. Low IDF terms as stop words</h3>
<p><a target="_blank" href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">Inverse document frequency (IDF)</a> refers to the inverse fraction of documents in your collection that contains a specific term (<strong>ti</strong>). Let us say:</p>
<ul>
<li>you have <strong>N</strong> documents</li>
<li>term <strong>ti</strong> occurred in <strong>M</strong> of the <strong>N</strong> documents.</li>
</ul>
<p>The IDF of <strong>ti</strong> is thus computed as:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*PABvj4Re2EypV6zjzLkjIg.png" alt="Image" width="334" height="96" loading="lazy"></p>
<p>So, the more documents <strong>ti</strong> appears in, the lower the <strong>IDF</strong> score. This means terms that appear in every document will have an IDF score of 0.</p>
<p>If you rank each <strong>ti</strong> in your collection by its IDF score in descending order, you can treat the <strong>bottom K</strong> terms with the <strong>lowest IDF</strong> scores to be your stop words.</p>
<p>Again, you can also eliminate common English words using a published stop list prior to sorting so that you target the <strong>domain-specific low IDF words</strong>. This is not necessary if your <strong>K</strong> is large enough that it will prune both general stop words as well as domain-specific stop words. You will find more information about IDFs <a target="_blank" href="http://kavita-ganesan.com/text-mining-cheat-sheet/#.W1olu9hKids">here</a>.</p>
<h3 id="heading-so-would-stop-words-help-my-task">So, would stop words help my task?</h3>
<p>How would you know if removing domain specific stop words would be helpful in your case? Easy — test it on a subset of your data. See if whatever measure of accuracy and performance improves, stays constant, or degrades. If it degrades, needless to say, don’t do it unless the degradation is negligible and you see gains in other forms such as a decrease in size of model, or the ability to process things in memory.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
