\n",
- " \n",
- "### Learning Objectives \n",
- " \n",
- "* Learn how to convert text data into numbers through a Bag-of-Words approach.\n",
- "* Understand the TF-IDF algorithm and how it complements the Bag-of-Words representation.\n",
- "* Implement Bag-of-Words and TF-IDF using the `sklearn` package and understand its parameter settings.\n",
- "* Use the numerical representations of text data to perform sentiment analysis.\n",
- "
\n",
- "\n",
- "### Icons Used in This Notebook\n",
- "🔔 **Question**: A quick question to help you understand what's going on. \n",
- "🥊 **Challenge**: Interactive excersise. We'll work through these in the workshop! \n",
- "🎬 **Demo**: Showing off something more advanced – so you know what Python can be used for! \n",
- "\n",
- "### Sections\n",
- "1. [Exploratory Data Analysis](#section1)\n",
- "2. [Preprocessing](#section2)\n",
- "3. [The Bag-of-Words Representation](#section3)\n",
- "4. [Term Frequency-Inverse Document Frequency](#section4)\n",
- "5. [Sentiment Classification Using the TF-IDF Representation](#section5)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "880e8a36-bd58-4c24-8593-03a0ea70deed",
- "metadata": {},
- "source": [
- "In the previous part, we learned how to perform text preprocessing. However, we didn't move beyond the text data itself. If we're interested in doing any computational analysis on the text data, we still need approaches to convert the text into a **numeric representation**.\n",
- "\n",
- "In Part 2 of our workshop series, we'll explore one of the most straightforward ways to generate a numeric representation from text: the **bag-of-words** (BoW). We will implement the BoW representation to transform our airline tweets data, and then build a classifier to explore what it tells us about the sentiment of the tweets. At the heart of the bag-of-words approach lies the assumption that the frequency of specific tokens is informative about the semantics and sentiment underlying the text.\n",
- "\n",
- "We'll make heavy use of the `scikit-learn` package to do so, as it provides a nice framework for constructing the numeric representation.\n",
- "\n",
- "Let's install `scikit-learn` firstǃ"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9e4a3a0d-66f4-44e5-8dd6-5f441146014d",
- "metadata": {
- "scrolled": true,
- "tags": []
- },
- "outputs": [],
- "source": [
- "# Uncomment to install the package\n",
- "# %pip install scikit-learn"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "21ed437f-9767-43b7-abc5-159aa4339a31",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Uncomment to install the NLP packages introduced in Part 1\n",
- "# %pip install NLTK\n",
- "# %pip install spaCy\n",
- "# !python -m spacy download en_core_web_sm"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "id": "f3862ffd-918f-4184-8c90-8a39a8a2a069",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Import other packages\n",
- "import re\n",
- "import numpy as np\n",
- "import pandas as pd\n",
- "import matplotlib.pyplot as plt\n",
- "import seaborn as sns\n",
- "from string import punctuation\n",
- "%matplotlib inline"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "852ea4a5-7c28-4557-acdd-afe8a97b7235",
- "metadata": {},
- "source": [
- "\n",
- "\n",
- "# Exploratory Data Analysis\n",
- "\n",
- "Before we ever do any preprocessing or modeling, we always should perform exploratory data analysis to familiarize ourselves with the data."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "4190e351-97b7-4c5b-866e-07aa6cbd42c2",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Read in data\n",
- "tweets_path = '../data/airline_tweets.csv'\n",
- "tweets = pd.read_csv(tweets_path, sep=',')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "79acbaf2-6625-4abb-b50f-97ea54ba0d11",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
+ " \n",
+ "### Learning Objectives\n",
+ " \n",
+ "* Learn how to convert text data into numbers through a Bag-of-Words approach.\n",
+ "* Understand the TF-IDF algorithm and how it complements the Bag-of-Words representation.\n",
+ "* Implement Bag-of-Words and TF-IDF using the `sklearn` package and understand its parameter settings.\n",
+ "* Use the numerical representations of text data to perform sentiment analysis.\n",
+ "
\n",
+ "\n",
+ "### Icons Used in This Notebook\n",
+ "🔔 **Question**: A quick question to help you understand what's going on. \n",
+ "🥊 **Challenge**: Interactive excersise. We'll work through these in the workshop! \n",
+ "🎬 **Demo**: Showing off something more advanced – so you know what Python can be used for! \n",
+ "\n",
+ "### Sections\n",
+ "1. [Exploratory Data Analysis](#section1)\n",
+ "2. [Preprocessing](#section2)\n",
+ "3. [The Bag-of-Words Representation](#section3)\n",
+ "4. [Term Frequency-Inverse Document Frequency](#section4)\n",
+ "5. [Sentiment Classification Using the TF-IDF Representation](#section5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "880e8a36-bd58-4c24-8593-03a0ea70deed",
+ "metadata": {
+ "id": "880e8a36-bd58-4c24-8593-03a0ea70deed"
+ },
+ "source": [
+ "In the previous part, we learned how to perform text preprocessing. However, we didn't move beyond the text data itself. If we're interested in doing any computational analysis on the text data, we still need approaches to convert the text into a **numeric representation**.\n",
+ "\n",
+ "In Part 2 of our workshop series, we'll explore one of the most straightforward ways to generate a numeric representation from text: the **bag-of-words** (BoW). We will implement the BoW representation to transform our airline tweets data, and then build a classifier to explore what it tells us about the sentiment of the tweets. At the heart of the bag-of-words approach lies the assumption that the frequency of specific tokens is informative about the semantics and sentiment underlying the text.\n",
+ "\n",
+ "We'll make heavy use of the `scikit-learn` package to do so, as it provides a nice framework for constructing the numeric representation.\n",
+ "\n",
+ "Let's install `scikit-learn` firstǃ"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Esa parte del código es un comentario que indica al usuario que, si elimina el símbolo #, podrá ejecutar el comando mágico %pip install scikit-learn, el cual sirve para instalar la librería de machine learning scikit-learn directamente en el entorno del notebook; sin embargo, en el ejemplo aparece escrito como scikit-lea, lo cual es un error tipográfico que debe corregirse para que la instalación funcione correctamente.*"
],
- "text/plain": [
- " tweet_id airline_sentiment airline_sentiment_confidence \\\n",
- "0 570306133677760513 neutral 1.0000 \n",
- "1 570301130888122368 positive 0.3486 \n",
- "2 570301083672813571 neutral 0.6837 \n",
- "3 570301031407624196 negative 1.0000 \n",
- "4 570300817074462722 negative 1.0000 \n",
- "\n",
- " negativereason negativereason_confidence airline \\\n",
- "0 NaN NaN Virgin America \n",
- "1 NaN 0.0000 Virgin America \n",
- "2 NaN NaN Virgin America \n",
- "3 Bad Flight 0.7033 Virgin America \n",
- "4 Can't Tell 1.0000 Virgin America \n",
- "\n",
- " airline_sentiment_gold name negativereason_gold retweet_count \\\n",
- "0 NaN cairdin NaN 0 \n",
- "1 NaN jnardino NaN 0 \n",
- "2 NaN yvonnalynn NaN 0 \n",
- "3 NaN jnardino NaN 0 \n",
- "4 NaN jnardino NaN 0 \n",
- "\n",
- " text tweet_coord \\\n",
- "0 @VirginAmerica What @dhepburn said. NaN \n",
- "1 @VirginAmerica plus you've added commercials t... NaN \n",
- "2 @VirginAmerica I didn't today... Must mean I n... NaN \n",
- "3 @VirginAmerica it's really aggressive to blast... NaN \n",
- "4 @VirginAmerica and it's a really big bad thing... NaN \n",
- "\n",
- " tweet_created tweet_location user_timezone \n",
- "0 2015-02-24 11:35:52 -0800 NaN Eastern Time (US & Canada) \n",
- "1 2015-02-24 11:15:59 -0800 NaN Pacific Time (US & Canada) \n",
- "2 2015-02-24 11:15:48 -0800 Lets Play Central Time (US & Canada) \n",
- "3 2015-02-24 11:15:36 -0800 NaN Pacific Time (US & Canada) \n",
- "4 2015-02-24 11:14:45 -0800 NaN Pacific Time (US & Canada) "
- ]
- },
- "execution_count": 3,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tweets.head()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "80232c78-ac41-4d74-a581-76c9dac3b8f6",
- "metadata": {},
- "source": [
- "As a refresher, each row in this dataframe correponds to a tweet. The following columns are of main interests to us. There are other columns containing metadata of the tweet, such as the author of the tweet, when it was created, the timezone of the user, and others, which we will set aside for now. \n",
- "\n",
- "- `text` (`str`): the text of the tweet.\n",
- "- `airline_sentiment` (`str`): the sentiment of the tweet, labeled as \"neutral,\" \"positive,\" or \"negative.\" \n",
- "- `airline` (`str`): the airline that is tweeted about.\n",
- "- `retweet count` (`int`): how many times the tweet was retweeted.\n",
- "\n",
- "To prepare us for sentiment classification, we'll partition the dataset to focus on the \"positive\" and \"negative\" tweets for now. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "a1faaf90-8c01-4d25-9468-90c01823f0d5",
- "metadata": {},
- "outputs": [],
- "source": [
- "tweets = tweets[tweets['airline_sentiment'] != 'neutral'].reset_index(drop=True)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "7cb6b039-53e7-4afe-a9e0-b3522c12b2d7",
- "metadata": {},
- "source": [
- "Let's take a look at a few tweets first!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "438830e6-1064-47fe-b578-a1ca693a0ed0",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "@VirginAmerica plus you've added commercials to the experience... tacky.\n",
- "@VirginAmerica it's really aggressive to blast obnoxious \"entertainment\" in your guests' faces & they have little recourse\n",
- "@VirginAmerica and it's a really big bad thing about it\n",
- "@VirginAmerica seriously would pay $30 a flight for seats that didn't have this playing.\n",
- "it's really the only bad thing about flying VA\n",
- "@VirginAmerica yes, nearly every time I fly VX this “ear worm” won’t go away :)\n"
- ]
- }
- ],
- "source": [
- "# Print first five tweets\n",
- "for idx in range(5):\n",
- " print(tweets['text'].iloc[idx])"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "0d6746f8-b29c-40d4-bef6-b4afd4cd6cc1",
- "metadata": {},
- "source": [
- "We can already see that some of these tweets contain negative sentiment—how can we tell this is the case? \n",
- "\n",
- "Next, let's take a look at the distribution of sentiment labels in this dataset. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "id": "01955158-6954-447a-acb6-2989d02a49c3",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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s2aSpJycHC1ZssRtrrGxsaqoqFBeXt5VvRZqFzdVwwiWZdkB9cwzzyg2Nlaffvqp1qxZo7S0NM2aNUtJSUlV3n9CQoJiYmJUWFiorKwsNWrUSPfff78kqaKiQpL06aef6oYbbnB7Hn86APhVw4YN3ZYdDocqKipUUVGhli1bat26dZWe07RpU7fxv2Vd8AXqp59+WseOHdPcuXPVqlUr+fr6Kjo6WuXl5Vc1zzvvvFPt2rXTihUr9OyzzyojI8O+V1D69fM+evRoJScnV3ruTTfddFWvhdpFEKFe2rJlS6XlyMhI3XrrrTp79qy2bt2qXr16SZJ+/vlnfffdd4qKirLHR0REaMyYMRozZoymTp2qRYsWXTSIfHx8dO7cud+dT69evRQREaEPP/xQn332mYYMGSIfHx9J0q233ipfX18dOXLE7cZLAL+va9euKigokLe3t32j84U6dOigbdu2KTEx0V731VdfuY1Zv3693nzzTQ0aNEiSdPToUf30009uYxo2bHhFn/f4+HgtX75cN954oxo0aKAHHnjAbb579uzRzTfffKWHiDqCS2aol44ePaqJEydq//79+uCDD/TGG2/oueeeU2RkpB566CGNHDlSGzZs0Ndff60nnnhCN9xwgx566CFJUkpKilavXq28vDzt2LFDX3zxhVss/Vbr1q1VUlKitWvX6qefftLp06cvOs7hcCg+Pl5vvfWWsrKy9MQTT9jbAgICNGnSJE2YMEFLly7VwYMHtXPnTv3nf/6nli5dWv1vDnAd6d+/v6Kjo/Xwww9r9erVOnTokDZt2qSXXnrJjp6kpCS9/fbbWrp0qQ4cOKBXX31Vu3btcjtrdPPNN2vZsmXat2+ftm7dqoSEBPn5+bm9VuvWrbV27VoVFBSoqKjoknNKSEjQjh079Nprr+mf//mf1ahRI3vblClTtHnzZo0bN065ubk6cOCAVq5ceU1noFE7CCLUS08++aRKS0t15513aty4cUpKStKoUaMk/fotkW7duikuLk7R0dGyLEurVq2yT8mfO3dO48aNU1RUlO677z516NBBb7755kVfp1evXhozZowee+wxtWjRQunp6ZecU0JCgvbu3asbbrhBd999t9u2f/3Xf9Wf/vQnpaWlKSoqSrGxsfrkk0/Upk2banpHgOuTw+HQqlWr1Lt3bw0fPlzt27fX448/rkOHDik0NFTSr5+9qVOnatKkSeratavy8vL09NNPu4XKO++8o6KiInXp0kWJiYlKTk5WSEiI22vNmjVLWVlZioiIUJcuXS45p8jISPXo0UO7du2y7xU87/bbb1d2drYOHDige++9V126dNG0adPUsmXLanxXUBP4pWrUO3369FHnzp350xkALmnAgAEKCwvTsmXLPD0V1BPcQwQAqNdOnz6tt956S7GxsfLy8tIHH3ygzz//XFlZWZ6eGuoRgggAUK+dv6z26quvqqysTB06dNBHH32k/v37e3pqqEe4ZAYAAIzHTdUAAMB4BBEAADAeQQQAAIxHEAEAAOMRRAAAwHgEEYAqO3TokBwOh3Jzcy87bsaMGercubO9/PTTT+vhhx+u0bnVBaYcJ3A94HeIAFRZRESE8vPzFRwcfFXP+4//+I9Kf428Pjt06JDatGmjnTt3uoVfXTpOh8OhjIwMAg24BIIIQJV5eXkpLCzsktsty7roXw93Op01Oa06w5TjBK4HXDIDcFmZmZm655571LRpUwUFBSkuLk4HDx6UVPmS2bp16+RwOLR69Wp1795dvr6+Wr9+faV9XngpqU+fPkpOTtbkyZPVvHlzhYWFacaMGW7PcblcGjVqlEJCQhQYGKi+ffvq66+/vqJj+Prrr/VP//RPCggIUGBgoLp162b/pXRJ2rRpk3r37i0/Pz9FREQoOTlZp06dsre3bt1aqampGj58uAICAnTTTTdp4cKF9vbzf6S3S5cucjgc6tOnzyWPMykpSSkpKWrWrJlCQ0O1cOFCnTp1Sn/84x8VEBCgdu3a6bPPPnOb/969ezVo0CA1adJEoaGhSkxM1E8//XTF71/r1q0lSY888ogcDoe9DOD/I4gAXNapU6c0ceJEbd++XWvXrlWDBg30yCOPqKKi4pLPmTx5stLS0rRv3z7dfvvtV/Q6S5culb+/v7Zu3ar09HS98sor9t+isixLDzzwgAoKCrRq1Srl5OSoa9eu6tevn44fP/67+05ISNCNN96o7du3KycnRy+88IIaNmwoSdq9e7diY2P16KOPateuXfrwww+1YcMGjR8/3m0fs2bNUvfu3bVz506NHTtWzz77rL799ltJ0rZt2yRJn3/+ufLz8/X3v//9sscZHBysbdu2KSkpSc8++6yGDBmiXr16aceOHYqNjVViYqJOnz4tScrPz1dMTIw6d+6sr776SpmZmfrxxx81dOjQK37/tm/fLklavHix8vPz7WUAv2EBwFUoLCy0JFm7d++28vLyLEnWzp07LcuyrC+//NKSZP3Xf/2X23OmT59u3XHHHfbyU089ZT300EP2ckxMjHXPPfe4PadHjx7WlClTLMuyrLVr11qBgYHWL7/84jamXbt21oIFC353zgEBAdaSJUsuui0xMdEaNWqU27r169dbDRo0sEpLSy3LsqxWrVpZTzzxhL29oqLCCgkJsebPn29ZllXpfbjS4zx79qzl7+9vJSYm2uvy8/MtSdbmzZsty7KsadOmWQMHDnTb79GjRy1J1v79+y+6X8tyf/8sy7IkWRkZGRd9DwBYFmeIAFzWwYMHFR8fr7Zt2yowMNC+PHTkyJFLPqd79+5X/ToXnklq2bKlCgsLJUk5OTkqKSlRUFCQmjRpYj/y8vLsy3eXM3HiRD3zzDPq37+//u3f/s3tOTk5OVqyZInbfmNjY1VRUaG8vLyLzs/hcCgsLMyeX1WP08vLS0FBQerUqZO9LjQ0VJLcjv3LL790m98tt9wiSW7Hcbn3D8Dv46ZqAJf14IMPKiIiQosWLVJ4eLgqKirUsWNHlZeXX/I5/v7+V/065y9hnedwOOzLchUVFWrZsqXWrVtX6XlNmzb93X3PmDFD8fHx+vTTT/XZZ59p+vTpWrFihX3pb/To0UpOTq70vJtuuumK5nc1Lraf365zOByS5HbsDz74oGbOnFlpXy1btqz2+QGmIogAXNLPP/+sffv2acGCBbr33nslSRs2bKj1eXTt2lUFBQXy9vau8g3B7du3V/v27TVhwgQNGzZMixcv1iOPPKKuXbtqz549uvnmm6s8Px8fH0m66DfqrlXXrl310UcfqXXr1vL2rvp/shs2bFgj8wOuF1wyA3BJzZo1U1BQkBYuXKj//d//1RdffKGJEyfW+jz69++v6OhoPfzww1q9erUOHTqkTZs26aWXXnL7ttjFlJaWavz48Vq3bp0OHz6sjRs3avv27YqKipIkTZkyRZs3b9a4ceOUm5urAwcOaOXKlUpKSrri+YWEhMjPz8++4dnlcl3T8f7WuHHjdPz4cQ0bNkzbtm3TP/7xD61Zs0bDhw+/qsBp3bq11q5dq4KCAhUVFVXb/IDrBUEE4JIaNGigFStWKCcnRx07dtSECRP05z//udbn4XA4tGrVKvXu3VvDhw9X+/bt9fjjj+vQoUP2PTeX4uXlpZ9//llPPvmk2rdvr6FDh+r+++/Xyy+/LOnXe2+ys7N14MAB3XvvverSpYumTZvmdjnq93h7e+v111/XggULFB4eroceeuiajve3wsPDtXHjRp07d06xsbHq2LGjnnvuOTmdTjVocOX/CZ81a5aysrIUERGhLl26VNv8gOuFw7LqyM+oAgAAeAhniAAAgPEIIgD13m233eb2tfTfPpYvX+7p6QGoB7hkBqDeO3z4sM6cOXPRbaGhoQoICKjlGQGobwgiAABgPC6ZAQAA4xFEAADAeAQRAAAwHkEEAACMRxABAADjEUQAAMB4BBEAADAeQQQAAIz3/wBpBRjTM7nsQgAAAABJRU5ErkJggg==",
- "text/plain": [
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "# Make a bar plot showing the count of tweet sentiments\n",
- "sns.countplot(data=tweets,\n",
- " x='airline_sentiment', \n",
- " color='cornflowerblue',\n",
- " order=['positive', 'negative']);"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "eab45abf-adf4-4f5e-ae09-75f6c4fd50d1",
- "metadata": {},
- "source": [
- "It looks like the majority of the tweets in this dataset are expressing negative sentiment!\n",
- "\n",
- "Let's take a look at what gets more retweeted:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "id": "428ddde7-af73-4eb6-92c9-041a1791ca59",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "airline_sentiment\n",
- "negative 0.093375\n",
- "positive 0.069403\n",
- "Name: retweet_count, dtype: float64"
- ]
- },
- "execution_count": 7,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Get the mean retweet count for each sentiment\n",
- "tweets.groupby('airline_sentiment')['retweet_count'].mean()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "0d31f3bc-257c-48a8-86a0-fd0d7c3e8cb3",
- "metadata": {},
- "source": [
- "Negative tweets are clearly retweeted more often than positive ones!\n",
- "\n",
- "Let's see which airline receives most negative tweets:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "id": "12aa9f2d-d655-494a-bb72-08ad973518f3",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
airline_sentiment
\n",
- "
negative
\n",
- "
positive
\n",
- "
\n",
- "
\n",
- "
airline
\n",
- "
\n",
- "
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
US Airways
\n",
- "
0.893760
\n",
- "
0.106240
\n",
- "
\n",
- "
\n",
- "
American
\n",
- "
0.853659
\n",
- "
0.146341
\n",
- "
\n",
- "
\n",
- "
United
\n",
- "
0.842560
\n",
- "
0.157440
\n",
- "
\n",
- "
\n",
- "
Southwest
\n",
- "
0.675399
\n",
- "
0.324601
\n",
- "
\n",
- "
\n",
- "
Delta
\n",
- "
0.637091
\n",
- "
0.362909
\n",
- "
\n",
- "
\n",
- "
Virgin America
\n",
- "
0.543544
\n",
- "
0.456456
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
+ "metadata": {
+ "id": "IJ2sHVf6bJBh"
+ },
+ "id": "IJ2sHVf6bJBh"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9e4a3a0d-66f4-44e5-8dd6-5f441146014d",
+ "metadata": {
+ "scrolled": true,
+ "tags": [],
+ "id": "9e4a3a0d-66f4-44e5-8dd6-5f441146014d"
+ },
+ "outputs": [],
+ "source": [
+ "# Uncomment to install the package\n",
+ " %pip install scikit-learn"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Ese bloque de código indica al lector que, al quitar el #, se instalarán las librerías de procesamiento de lenguaje natural usadas en la primera parte. En específico, %pip install NLTK instala NLTK, una librería orientada al análisis y manipulación de texto; %pip install spaCy instala spaCy, una herramienta muy eficiente para el procesamiento avanzado de lenguaje; y finalmente !python -m spacy download en_core_web_sm descarga un modelo pequeño en inglés (en_core_web_sm) necesario para que spaCy pueda identificar palabras, oraciones y su estructura gramatical.*"
],
- "text/plain": [
- "airline_sentiment negative positive\n",
- "airline \n",
- "US Airways 0.893760 0.106240\n",
- "American 0.853659 0.146341\n",
- "United 0.842560 0.157440\n",
- "Southwest 0.675399 0.324601\n",
- "Delta 0.637091 0.362909\n",
- "Virgin America 0.543544 0.456456"
- ]
- },
- "execution_count": 8,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Get the proportion of negative tweets by airline\n",
- "proportions = tweets.groupby(['airline', 'airline_sentiment']).size() / tweets.groupby('airline').size()\n",
- "proportions.unstack().sort_values('negative', ascending=False)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "7042419e-9c41-40e7-8dbf-47bd1e2ad45a",
- "metadata": {},
- "source": [
- "It looks like people are most dissatified with US Airways, followed by American Airline, both having over 85\\% negative tweets!\n",
- "\n",
- "A lot of interesting discoveries could be made if you want to explore more about the data. Now let's return to our task of sentiment analysis. Before that, we need to preprocess the text data so that they are in a standard format."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "9e513930-2dc7-489c-bc5a-22eb09add5bf",
- "metadata": {},
- "source": [
- "\n",
- "# Preprocessing\n",
- "\n",
- "We spent much of Part 1 learning how to preprocess data. Let's apply what we learned! Looking at some of the tweets above, we can see that while they are in pretty good shape, we can do some additional processing on them.\n",
- "\n",
- "In our pipeline, we'll omit the tokenization process since we will perform it in a later step. "
- ]
- },
- {
- "cell_type": "markdown",
- "id": "92a83ece-f3b2-4200-9d22-0788fbc07fa4",
- "metadata": {},
- "source": [
- "## 🥊 Challenge 1: Apply a Text Cleaning Pipeline\n",
- "\n",
- "Write a function called `preprocess` that performs the following steps on a text input:\n",
- "\n",
- "* Step 1: Lowercase the text input.\n",
- "* Step 2: Replace the following patterns with placeholders:\n",
- " * URLs → ` URL `\n",
- " * Digits → ` DIGIT `\n",
- " * Hashtags → ` HASHTAG `\n",
- " * Tweet handles → ` USER `\n",
- "* Step 3: Remove extra blankspace.\n",
- "\n",
- "Here are some hints to guide you through this challenge:\n",
- "\n",
- "* For Step 1, recall from Part 1 that a string method called [`.lower()`](https://docs.python.org/3.11/library/stdtypes.html#str.lower) can be usd to convert text to lowercase. \n",
- "* We have integrated Step 2 into a function called `placeholder`. Run the cell below to import it into your notebook, and you can use it just like any other functions.\n",
- "* For Step 3, we have provided the regex pattern for identifying whitespace characters as well as the correct replacement for extract whitespace. \n",
- "\n",
- "Run your `preprocess` function on `example_tweet` (three cells below) to check if it works. If it does, apply it to the entire `text` column in the tweets dataframe."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "id": "21738b02-9ab9-4a61-b41f-ff75888aa747",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "from utils import placeholder"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "03569f0d-34ba-492d-aa1d-1dce9d34f792",
- "metadata": {},
- "outputs": [],
- "source": [
- "blankspace_pattern = r'\\s+'\n",
- "blankspace_repl = ' '\n",
- "\n",
- "def preprocess(text):\n",
- " '''Create a preprocess pipeline that cleans the tweet data.'''\n",
- " \n",
- " # Step 1: Lowercase\n",
- " text = ...\n",
- "\n",
- " # Step 2: Replace patterns with placeholders\n",
- " text = ...\n",
- "\n",
- " # Step 3: Remove extra whitespace characters\n",
- " text = ...\n",
- "\n",
- " return text"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "id": "8990cefd-5d04-46ba-ada2-29978c28cfe8",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "lol @justinbeiber and @BillGates are like soo 2000 #yesterday #amiright saw it on https://twitter.com #yolo\n",
- "==================================================\n",
- "lol USER and USER are like soo DIGIT HASHTAG HASHTAG saw it on URL HASHTAG\n"
- ]
- }
- ],
- "source": [
- "example_tweet = 'lol @justinbeiber and @BillGates are like soo 2000 #yesterday #amiright saw it on https://twitter.com #yolo'\n",
- "\n",
- "# Print the example tweet\n",
- "print(example_tweet)\n",
- "print(f\"{'='*50}\")\n",
- "\n",
- "# Print the preprocessed tweet\n",
- "print(preprocess(example_tweet))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "id": "a5f7bb6a-f064-48cc-b650-12c4ef2fbb88",
- "metadata": {
- "scrolled": true
- },
- "outputs": [
- {
- "data": {
- "text/plain": [
- "0 USER plus you've added commercials to the expe...\n",
- "1 USER it's really aggressive to blast obnoxious...\n",
- "2 USER and it's a really big bad thing about it\n",
- "3 USER seriously would pay $ DIGIT a flight for ...\n",
- "4 USER yes, nearly every time i fly vx this “ear...\n",
- "Name: text_processed, dtype: object"
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Apply the function to the text column and assign the preprocessed tweets to a new column\n",
- "tweets['text_processed'] = tweets['text'].apply(lambda x: preprocess(x))\n",
- "tweets['text_processed'].head()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "1576acc6-b305-492a-8fde-65b343cb779c",
- "metadata": {},
- "source": [
- "Congratualtions! Preprocessing is done. Let's dive into the bag-of-words!"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "53282330-54da-4e1c-bfe5-e77cb8fa3add",
- "metadata": {},
- "source": [
- "\n",
- "# The Bag-of-Words Representation\n",
- "\n",
- "The idea of bag-of-words (BoW), as the name suggests, is quite intuitive: we take a document and toss it in a bag. The action of \"throwing\" the document in a bag disregards the relative position between words, so what is \"in the bag\" is essentially \"an unsorted set of words\" [(Jurafsky & Martin, 2024)](https://web.stanford.edu/~jurafsky/slp3/ed3book.pdf). In return, we have a list of unique words and the frequency of each of them. \n",
- "\n",
- "For example, as shown in the following illustration, the word \"coffee\" appears twice. \n",
- "\n",
- "\n",
- "\n",
- "With a bag-of-words representation, we make heavy use of word frequency but not too much of word order. \n",
- "\n",
- "In the context of sentiment analysis, the sentiment of a tweet is conveyed more strongly by specific words. For example, if a tweet contains the word \"happy,\" it likely conveys positive sentiment, but not always (e.g., \"not happy\" denotes the opposite sentiment). When these words come up more often, they'll probably more strongly convey the sentiment."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "b9d9bdbd-406d-469b-a8f6-41d1b3687c37",
- "metadata": {},
- "source": [
- "## Document Term Matrix\n",
- "\n",
- "Now let's implement the idea of bag-of-words. Before we dive deeper, let's step back for a moment. In practice, text analysis often involves handling many documents; from now on, we use the term **document** to represent a piece of text on which we perform analysis. It could be a phrase, a sentence, a tweet, or any other text—as long as it can be represented by a string, the length dosen't really matter. \n",
- "\n",
- "Imagine we have four documents (i.e., the four phrases shown above), and we toss them all in the bag. Instead of a word-frequency list, we'd expect a document-term matrix (DTM) in return. In a DTM, the word list is the **vocabulary** (V) that holds all unique words occur across the documents. For each **document** (D), we count the number of occurence of each word in the vocabulary, and then plug the number into the matrix. In other words, the DTM we will construct is a $D \\times V$ matrix, where each row corresponds to a document, and each column corresponds to a token (or \"term\").\n",
- "\n",
- "The unique tokens in this set of documents, arranged in alphabetical order, form the columns. For each document, we mark the occurence of each word present in the document. The numerical representation for each document is a row in the matrix. For example, the first document, \"the coffee roaster,\" has the numerical representation $[0, 1, 0, 0, 0, 1, 1, 0]$.\n",
- "\n",
- "Note that the left index column now displays these documents as text, but typically we would just assign an index to each of them. \n",
- "\n",
- "$$\n",
- "\\begin{array}{c|cccccccccccc}\n",
- " & \\text{americano} & \\text{coffee} & \\text{iced} & \\text{light} & \\text{roast} & \\text{roaster} & \\text{the} & \\text{time} \\\\\\hline\n",
- "\\text{the coffee roaster} &0 &1\t&0\t&0\t&0\t&1\t&1\t&0 \\\\ \n",
- "\\text{light roast} &0 &0\t&0\t&1\t&1\t&0\t&0\t&0 \\\\\n",
- "\\text{iced americano} &1 &0\t&1\t&0\t&0\t&0\t&0\t&0 \\\\\n",
- "\\text{coffee time} &0 &1\t&0\t&0\t&0\t&0\t&0\t&1 \\\\\n",
- "\\end{array}\n",
- "$$\n",
- "\n",
- "To create a DTM, we will use `CountVectorizer` from the package `sklearn`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "id": "cd2adf56-ba93-459d-8cfa-16ce8dc9284b",
- "metadata": {},
- "outputs": [],
- "source": [
- "from sklearn.feature_extraction.text import CountVectorizer"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "4989781d-6b40-417a-be70-eeba05cd8a50",
- "metadata": {},
- "source": [
- "The following illustration depicts the three-step workflow of creating a DTM with `CountVectorizr`.\n",
- "\n",
- "\n",
- "\n",
- "Let's walk through these steps with the toy example shown above."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "34174034-46b9-43e2-a511-5972d378cb00",
- "metadata": {},
- "source": [
- "### A Toy Example"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "id": "4da2bd3d-0460-4b5f-9b9e-02940db0d7ca",
- "metadata": {},
- "outputs": [],
- "source": [
- "# A toy example containing four documents\n",
- "test = ['the coffee roaster',\n",
- " 'light roast',\n",
- " 'iced americano',\n",
- " 'coffee time']"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "dff7c1d3-fcee-4e20-b9a7-17306ebd5fc2",
- "metadata": {},
- "source": [
- "The first step is to initialize a `CountVectorizer` object. Within the round paratheses, we can specify parameter settings if desired. Let's take a look at the [documentation](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) and see what options are available. \n",
- "\n",
- "For now we can just leave it blank to use the default settings. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "id": "9de3fe6a-9abf-4e11-aad1-e54c891567bb",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create a CountVectorizer object\n",
- "vectorizer = CountVectorizer()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "1b5a7d0d-0bfc-4fb9-8e5f-e91e39797fb5",
- "metadata": {},
- "source": [
- "The second step is to `fit` this `CountVectorizer` object to the data, which means creating a vocabulary of tokens from the set of documents. Thirdly, we `transform` our data according to the \"fitted\" `CountVectorizer` object, which means taking each of the document and counting the occurrences of tokens according to the vocabulary established during the \"fitting\" step.\n",
- "\n",
- "It may sound a bit complex but steps 2 and 3 can be done in one swoop using a `fit_transform` function."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "id": "da1bbad4-bb1a-4b92-9096-6e17558b4a42",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Fit and transform to create a DTM\n",
- "test_count = vectorizer.fit_transform(test)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "324d3b65-4e98-48bf-87d2-399457f4939c",
- "metadata": {},
- "source": [
- "The return of `fit_transform` is supposed to be the DTM. \n",
- "\n",
- "Let's take a look at it!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "id": "cb044001-8eb2-4489-b025-2d8e2d4bfee2",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "<4x8 sparse matrix of type ''\n",
- "\twith 9 stored elements in Compressed Sparse Row format>"
- ]
- },
- "execution_count": 17,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "test_count"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f9817b09-a806-42c4-9436-822cc27a38b9",
- "metadata": {},
- "source": [
- "Apparently we've got a \"sparse matrix\"—a matrix that contains a lot of zeros. This makes sense. For each document, there are words that don't occur at all, and these are counted as zero in the DTM. This sparse matrix is stored in a \"Compressed Sparse Row\" format, a memory-saving format designed for handling sparse matrices. \n",
- "\n",
- "Let's convert it to a dense matrix, where those zeros are probably represented, as in a numpy array."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "id": "bb03a238-87d8-40c9-b20e-66e7c9b6576b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "matrix([[0, 1, 0, 0, 0, 1, 1, 0],\n",
- " [0, 0, 0, 1, 1, 0, 0, 0],\n",
- " [1, 0, 1, 0, 0, 0, 0, 0],\n",
- " [0, 1, 0, 0, 0, 0, 0, 1]])"
- ]
- },
- "execution_count": 18,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Convert DTM to a dense matrix \n",
- "test_count.todense()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "28b58a63-d7f6-4b9f-aadf-4d4fc7341336",
- "metadata": {},
- "source": [
- "So this is our DTM! The matrix is the same as shown above. To make it more reader-friendly, let's convert it to a dataframe. The column names should be tokens in the vocabulary, which we can access with the `get_feature_names_out` function."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 19,
- "id": "714de5d3-e37d-4a19-9ade-3c6629e38d4e",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "array(['americano', 'coffee', 'iced', 'light', 'roast', 'roaster', 'the',\n",
- " 'time'], dtype=object)"
- ]
- },
- "execution_count": 19,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Retrieve the vocabulary\n",
- "vectorizer.get_feature_names_out()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 20,
- "id": "6a7729a2-ca2e-4de7-8795-74dfedb7a4d5",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create a DTM dataframe\n",
- "test_dtm = pd.DataFrame(data=test_count.todense(),\n",
- " columns=vectorizer.get_feature_names_out())"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "781da407-f394-40f2-9d45-1fac39f02047",
- "metadata": {},
- "source": [
- "Here it is! The DTM of our toy data is now a dataframe. The index of `test_dtm` corresponds to the position of each document in the `test` list. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 21,
- "id": "e41dd243-cd2e-43c3-80f8-5eaab6e64210",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
americano
\n",
- "
coffee
\n",
- "
iced
\n",
- "
light
\n",
- "
roast
\n",
- "
roaster
\n",
- "
the
\n",
- "
time
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
0
\n",
- "
0
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
1
\n",
- "
1
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
1
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
2
\n",
- "
1
\n",
- "
0
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
3
\n",
- "
0
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
1
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
+ "metadata": {
+ "id": "TTSrSpDSb8o7"
+ },
+ "id": "TTSrSpDSb8o7"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21ed437f-9767-43b7-abc5-159aa4339a31",
+ "metadata": {
+ "id": "21ed437f-9767-43b7-abc5-159aa4339a31"
+ },
+ "outputs": [],
+ "source": [
+ "# Uncomment to install the NLP packages introduced in Part 1\n",
+ "%pip install NLTK\n",
+ "%pip install spaCy\n",
+ "!python -m spacy download en_core_web_sm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ " *Ese fragmento de código muestra que, tras el comentario, se están importando varias librerías y módulos necesarios para trabajar con datos y visualización: **re** sirve para manejar expresiones regulares, **numpy** (abreviado como np) para cálculos numéricos y operaciones con arreglos, **pandas** (abreviado como pd) para el manejo de datos en tablas, **matplotlib.pyplot** (abreviado como plt) y **seaborn** (abreviado como sns) para crear gráficos, mientras que punctuation de la librería string proporciona un conjunto de caracteres de puntuación útiles para procesar texto; finalmente, **%matplotlib inline** es un comando mágico de Jupyter que permite mostrar los gráficos directamente dentro del notebook.*"
],
- "text/plain": [
- " americano coffee iced light roast roaster the time\n",
- "0 0 1 0 0 0 1 1 0\n",
- "1 0 0 0 1 1 0 0 0\n",
- "2 1 0 1 0 0 0 0 0\n",
- "3 0 1 0 0 0 0 0 1"
- ]
- },
- "execution_count": 21,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "test_dtm"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "d59a03b4-94fa-4fe7-8f5d-7280e31b9bc4",
- "metadata": {},
- "source": [
- "Hopefully this toy example provides a clear walkthrough of creating a DTM.\n",
- "\n",
- "Now it's time for our tweets data!\n",
- "\n",
- "### DTM for Tweets\n",
- "\n",
- "We'll begin by initializing a `CountVectorizer` object. In the following cell, we have included a few parameters that people often adjust. These parameters are currently set to their default values.\n",
- "\n",
- "When we construct a DTM, the default is to lowercase the input text. If nothing is provided for `stop_words`, the default is to keep them. The next three parameters are used to control the size of the vocabulary, which we'll return to in a minute."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 22,
- "id": "783e44a4-4a22-4290-b222-282b02c080dc",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create a CountVectorizer object\n",
- "vectorizer = CountVectorizer(lowercase=True,\n",
- " stop_words=None,\n",
- " min_df=1,\n",
- " max_df=1.0, \n",
- " max_features=None)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 23,
- "id": "f85e76ea-bc54-4775-bcda-432a03d2c96f",
- "metadata": {
- "scrolled": true
- },
- "outputs": [
- {
- "data": {
- "text/plain": [
- "<11541x8751 sparse matrix of type ''\n",
- "\twith 191139 stored elements in Compressed Sparse Row format>"
- ]
- },
- "execution_count": 23,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Fit and transform to create DTM\n",
- "counts = vectorizer.fit_transform(tweets['text_processed'])\n",
- "counts"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 24,
- "id": "87119057-c78c-4eb2-a9d6-3e9f44e4c22b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "array([[0, 0, 0, ..., 0, 0, 0],\n",
- " [0, 0, 0, ..., 0, 0, 0],\n",
- " [0, 0, 0, ..., 0, 0, 0],\n",
- " ...,\n",
- " [0, 0, 0, ..., 0, 0, 0],\n",
- " [0, 0, 0, ..., 0, 0, 0],\n",
- " [0, 0, 0, ..., 0, 0, 0]])"
- ]
- },
- "execution_count": 24,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Do not run if you have limited memory - this includes DataHub and Binder\n",
- "np.array(counts.todense())"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 25,
- "id": "99322b85-1a15-46a5-bb80-bb5eaa6eeb7b",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Extract tokens\n",
- "tokens = vectorizer.get_feature_names_out()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 26,
- "id": "43620587-3795-4434-8f1f-145c81b93706",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "(11541, 8751)\n"
- ]
- }
- ],
- "source": [
- "# Create DTM\n",
- "first_dtm = pd.DataFrame(data=counts.todense(),\n",
- " index=tweets.index,\n",
- " columns=tokens)\n",
- "\n",
- "# Print the shape of DTM\n",
- "print(first_dtm.shape)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "2dd257d5-4244-436c-afe7-5688232caf8f",
- "metadata": {},
- "source": [
- "If we leave the `CountVectorizer` to the default setting, the vocabulary size of the tweet data is 8751. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 27,
- "id": "bb3604ec-d909-4238-9a3f-67e7d4ae2ac5",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
_exact_
\n",
- "
_wtvd
\n",
- "
aa
\n",
- "
aaaand
\n",
- "
aadv
\n",
- "
aadvantage
\n",
- "
aal
\n",
- "
aaron
\n",
- "
ab
\n",
- "
aback
\n",
- "
...
\n",
- "
zero
\n",
- "
zig
\n",
- "
zip
\n",
- "
zippers
\n",
- "
zone
\n",
- "
zones
\n",
- "
zoom
\n",
- "
zukes
\n",
- "
zurich
\n",
- "
zz
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
2
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
3
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
4
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- " \n",
- "
\n",
- "
5 rows × 8751 columns
\n",
- "
"
+ "metadata": {
+ "id": "1lxnCapVcUCM"
+ },
+ "id": "1lxnCapVcUCM"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f3862ffd-918f-4184-8c90-8a39a8a2a069",
+ "metadata": {
+ "id": "f3862ffd-918f-4184-8c90-8a39a8a2a069"
+ },
+ "outputs": [],
+ "source": [
+ "# Import other packages\n",
+ "import re\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import matplotlib.pyplot as plt\n",
+ "import seaborn as sns\n",
+ "from string import punctuation\n",
+ "%matplotlib inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "852ea4a5-7c28-4557-acdd-afe8a97b7235",
+ "metadata": {
+ "id": "852ea4a5-7c28-4557-acdd-afe8a97b7235"
+ },
+ "source": [
+ "\n",
+ "\n",
+ "# Exploratory Data Analysis\n",
+ "\n",
+ "Before we ever do any preprocessing or modeling, we always should perform exploratory data analysis to familiarize ourselves with the data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Ese bloque de código comienza con un comentario que indica que se van a leer datos. Primero se define la variable tweets_path, que guarda la ruta del archivo llamado airline_tweets.csv ubicado en la carpeta ../data/. Luego, con pd.read_csv(tweets_path, sep=','), se utiliza la librería pandas para cargar ese archivo CSV en un DataFrame llamado tweets, especificando que los valores están separados por comas. De esta forma, los datos del archivo quedan listos para ser analizados dentro del notebook.*"
],
- "text/plain": [
- " _exact_ _wtvd aa aaaand aadv aadvantage aal aaron ab aback ... \\\n",
- "0 0 0 0 0 0 0 0 0 0 0 ... \n",
- "1 0 0 0 0 0 0 0 0 0 0 ... \n",
- "2 0 0 0 0 0 0 0 0 0 0 ... \n",
- "3 0 0 0 0 0 0 0 0 0 0 ... \n",
- "4 0 0 0 0 0 0 0 0 0 0 ... \n",
- "\n",
- " zero zig zip zippers zone zones zoom zukes zurich zz \n",
- "0 0 0 0 0 0 0 0 0 0 0 \n",
- "1 0 0 0 0 0 0 0 0 0 0 \n",
- "2 0 0 0 0 0 0 0 0 0 0 \n",
- "3 0 0 0 0 0 0 0 0 0 0 \n",
- "4 0 0 0 0 0 0 0 0 0 0 \n",
- "\n",
- "[5 rows x 8751 columns]"
- ]
- },
- "execution_count": 27,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "first_dtm.head()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "095d34e2-52f8-4419-b4c7-ed20dbd5df89",
- "metadata": {},
- "source": [
- "Most of the tokens have zero occurences at least in the first five tweets. \n",
- "\n",
- "Let's take a closer look at the DTM!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 28,
- "id": "f432154a-eae0-4723-a797-55f3cfdd71c4",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "user 12882\n",
- "to 6987\n",
- "digit 6927\n",
- "the 5088\n",
- "you 3635\n",
- "for 3386\n",
- "flight 3320\n",
- "and 3276\n",
- "on 3142\n",
- "my 2751\n",
- "dtype: int64"
- ]
- },
- "execution_count": 28,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Most frequent tokens\n",
- "first_dtm.sum().sort_values(ascending=False).head(10)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 29,
- "id": "26c7f1c9-dd66-49f2-b337-01253da551d2",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "_exact_ 1\n",
- "mightmismybrosgraduation 1\n",
- "midterm 1\n",
- "midnite 1\n",
- "midland 1\n",
- "michelle 1\n",
- "michele 1\n",
- "michael 1\n",
- "mhtt 1\n",
- "mgmt 1\n",
- "dtype: int64"
- ]
- },
- "execution_count": 29,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Least frequent tokens\n",
- "first_dtm.sum().sort_values(ascending=True).head(10)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "5d230f79-e752-4e32-93db-4f013287f8e2",
- "metadata": {},
- "source": [
- "It is not surprising to see \"user\" and \"digit\" to be among the most frequent tokens as we replaced each idiosyncratic one with these placeholders. The rest of the most frequent tokens are mostly stop words.\n",
- "\n",
- "Perhaps a more interesting pattern is to look for which token appears most in any given tweet:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 30,
- "id": "efb8f4d8-4c88-4155-a6c5-c72a5b4e8bb8",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
token
\n",
- "
number
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
3127
\n",
- "
lt
\n",
- "
6
\n",
- "
\n",
- "
\n",
- "
918
\n",
- "
worst
\n",
- "
6
\n",
- "
\n",
- "
\n",
- "
10572
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
8148
\n",
- "
the
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
10742
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
152
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
5005
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
10923
\n",
- "
the
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
7750
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- "
\n",
- "
355
\n",
- "
to
\n",
- "
5
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
+ "metadata": {
+ "id": "-Gu0S6LUdNpw"
+ },
+ "id": "-Gu0S6LUdNpw"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4190e351-97b7-4c5b-866e-07aa6cbd42c2",
+ "metadata": {
+ "id": "4190e351-97b7-4c5b-866e-07aa6cbd42c2"
+ },
+ "outputs": [],
+ "source": [
+ "# Read in data\n",
+ "tweets_path = '../data/airline_tweets.csv'\n",
+ "tweets = pd.read_csv(tweets_path, sep=',')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*El comando tweets.head() se utiliza para mostrar las primeras filas del DataFrame llamado tweets. Por defecto enseña las primeras cinco filas, lo que permite al usuario revisar rápidamente cómo están organizados los datos, verificar los nombres de las columnas y observar algunos valores iniciales sin necesidad de mostrar todo el conjunto de datos.*"
],
- "text/plain": [
- " token number\n",
- "3127 lt 6\n",
- "918 worst 6\n",
- "10572 to 5\n",
- "8148 the 5\n",
- "10742 to 5\n",
- "152 to 5\n",
- "5005 to 5\n",
- "10923 the 5\n",
- "7750 to 5\n",
- "355 to 5"
- ]
- },
- "execution_count": 30,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "counts = pd.DataFrame()\n",
- "\n",
- "# Retrieve the index of the tweet where a token appears most frequently\n",
- "counts['token'] = first_dtm.idxmax(axis=1)\n",
- "\n",
- "# Retrieve the number of occurrence \n",
- "counts['number'] = first_dtm.max(axis=1)\n",
- "\n",
- "# Filter out placeholders\n",
- "counts[(counts['token']!='digit')\n",
- " & (counts['token']!='hashtag')\n",
- " & (counts['token']!='user')].sort_values('number', ascending=False).head(10)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "7cdac4ef-6b9d-4aad-9b24-c70f6c2eb8f0",
- "metadata": {},
- "source": [
- "It looks like among all tweets, at most a token appears six times, and it is either the word \"It\" or the word \"worst.\" \n",
- "\n",
- "Let's go back to our tweets dataframe and locate the 918th tweet."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 31,
- "id": "5e7cacd8-1fb3-4f0d-a744-4ee0994a089f",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "\"@united is the worst. Worst reservation policies. Worst costumer service. Worst worst worst. Congrats, @Delta you're not that bad!\""
- ]
- },
- "execution_count": 31,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Retrieve 918th tweet: \"worst\"\n",
- "tweets.iloc[918]['text']"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "3dba8e37-4880-4565-b6fc-7e7c96958f0f",
- "metadata": {},
- "source": [
- "## Customize the `CountVectorizer`\n",
- "\n",
- "So far we've always used the default parameter setting to create our DTMs, but in many cases we may want to customize the `CountVectorizer` object. The purpose of doing so is to further filter out unnecessary tokens. In the example below, we tweak the following parameters:\n",
- "\n",
- "- `stop_words = 'english'`: ignore English stop words \n",
- "- `min_df = 2`: ignore words that don't occur at least twice\n",
- "- `max_df = 0.95`: ignore words if they appear in more than 95\\% of the documents\n",
- "\n",
- "🔔 **Question**: Let's pause for a minute to discuss whether it sounds reasonable to set these parameters! What do you think?\n",
- "\n",
- "Oftentimes, we are not interested in words whose frequencies are either too low or too high, so we use `min_df` and `max_df` to filter them out. Alternatively, we can define our vocabulary size as $N$ by setting `max_features`. In other words, we tell `CountVectorizer` to only consider the top $N$ most frequent tokens when constructing the DTM."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 32,
- "id": "37a0a93e-9dd8-43dc-a82c-06a24bf02bc9",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Customize the parameter setting\n",
- "vectorizer = CountVectorizer(lowercase=True,\n",
- " stop_words='english',\n",
- " min_df=2,\n",
- " max_df=0.95,\n",
- " max_features=None)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 33,
- "id": "b53e5ecf-7be3-4915-9d11-fd3edb913400",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Fit, transform, and get tokens\n",
- "counts = vectorizer.fit_transform(tweets['text_processed'])\n",
- "tokens = vectorizer.get_feature_names_out()\n",
- "\n",
- "# Create the second DTM\n",
- "second_dtm = pd.DataFrame(data=counts.todense(),\n",
- " index=tweets.index,\n",
- " columns=tokens)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6d2e66bc-2eaa-4642-8848-74459948084b",
- "metadata": {},
- "source": [
- "Our second DTM has a substantially smaller vocabulary compared to the first one."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 34,
- "id": "570fb598-fa81-4111-9e36-7172d8034713",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "(11541, 8751)\n",
- "(11541, 4471)\n"
- ]
- }
- ],
- "source": [
- "print(first_dtm.shape)\n",
- "print(second_dtm.shape)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 35,
- "id": "d8deabb2-20eb-4047-b592-48cb1564fd2a",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
"
+ ],
+ "text/plain": [
+ " tweet_id airline_sentiment airline_sentiment_confidence \\\n",
+ "0 570306133677760513 neutral 1.0000 \n",
+ "1 570301130888122368 positive 0.3486 \n",
+ "2 570301083672813571 neutral 0.6837 \n",
+ "3 570301031407624196 negative 1.0000 \n",
+ "4 570300817074462722 negative 1.0000 \n",
+ "\n",
+ " negativereason negativereason_confidence airline \\\n",
+ "0 NaN NaN Virgin America \n",
+ "1 NaN 0.0000 Virgin America \n",
+ "2 NaN NaN Virgin America \n",
+ "3 Bad Flight 0.7033 Virgin America \n",
+ "4 Can't Tell 1.0000 Virgin America \n",
+ "\n",
+ " airline_sentiment_gold name negativereason_gold retweet_count \\\n",
+ "0 NaN cairdin NaN 0 \n",
+ "1 NaN jnardino NaN 0 \n",
+ "2 NaN yvonnalynn NaN 0 \n",
+ "3 NaN jnardino NaN 0 \n",
+ "4 NaN jnardino NaN 0 \n",
+ "\n",
+ " text tweet_coord \\\n",
+ "0 @VirginAmerica What @dhepburn said. NaN \n",
+ "1 @VirginAmerica plus you've added commercials t... NaN \n",
+ "2 @VirginAmerica I didn't today... Must mean I n... NaN \n",
+ "3 @VirginAmerica it's really aggressive to blast... NaN \n",
+ "4 @VirginAmerica and it's a really big bad thing... NaN \n",
+ "\n",
+ " tweet_created tweet_location user_timezone \n",
+ "0 2015-02-24 11:35:52 -0800 NaN Eastern Time (US & Canada) \n",
+ "1 2015-02-24 11:15:59 -0800 NaN Pacific Time (US & Canada) \n",
+ "2 2015-02-24 11:15:48 -0800 Lets Play Central Time (US & Canada) \n",
+ "3 2015-02-24 11:15:36 -0800 NaN Pacific Time (US & Canada) \n",
+ "4 2015-02-24 11:14:45 -0800 NaN Pacific Time (US & Canada) "
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
],
- "text/plain": [
- " aa aadv aadvantage aal abandoned abc ability able aboard abq ... \\\n",
- "0 0 0 0 0 0 0 0 0 0 0 ... \n",
- "1 0 0 0 0 0 0 0 0 0 0 ... \n",
- "2 0 0 0 0 0 0 0 0 0 0 ... \n",
- "3 0 0 0 0 0 0 0 0 0 0 ... \n",
- "4 0 0 0 0 0 0 0 0 0 0 ... \n",
- "\n",
- " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
- "0 0 0 0 0 0 0 0 0 0 0 \n",
- "1 0 0 0 0 0 0 0 0 0 0 \n",
- "2 0 0 0 0 0 0 0 0 0 0 \n",
- "3 0 0 0 0 0 0 0 0 0 0 \n",
- "4 0 0 0 0 0 0 0 0 0 0 \n",
- "\n",
- "[5 rows x 4471 columns]"
- ]
- },
- "execution_count": 35,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "second_dtm.head()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "998fe2c3-ec90-4027-8c7f-417327a33a27",
- "metadata": {},
- "source": [
- "The most frequent token list now includes words that make more sense to us, such as \"cancelled\" and \"service.\" "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 36,
- "id": "ffa7bf4e-640b-49bc-b64b-721140f67f76",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "digit 6927\n",
- "flight 3320\n",
- "hashtag 2633\n",
- "cancelled 956\n",
- "thanks 921\n",
- "service 910\n",
- "just 801\n",
- "customer 726\n",
- "time 695\n",
- "help 687\n",
- "dtype: int64"
- ]
- },
- "execution_count": 36,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "second_dtm.sum().sort_values(ascending=False).head(10)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "3e8b5145-d505-4e36-9a39-a40d25d8ec6f",
- "metadata": {},
- "source": [
- "## 🥊 Challenge 2: Lemmatize the Text Input\n",
- "\n",
- "Recall from Part 1 that we introduced using `spaCy` to perform lemmatization, i.e., to \"recover\" the base form of a word. This process will reduce vocabulary size by keeping word variations minimal—a smaller vocabularly may help improve model performance in sentiment classification. \n",
- "\n",
- "Now let's implement lemmatization on our tweet data and use the lemmatized text to create a third DTM. \n",
- "\n",
- "Complete the function `lemmatize_text`. It requires a text input and returns the lemmas of all tokens. \n",
- "\n",
- "Here are some hints to guide you through this challenge:\n",
- "\n",
- "- Step 1: initialize a list to hold lemmas\n",
- "- Step 2: apply the `nlp` pipeline to the input text\n",
- "- Step 3: iterate over tokens in the processed text and retrieve the lemma of the token\n",
- " - HINT: lemmatization is one of the linguistic annotations that the `nlp` pipeline automatically does for us. We can use `token.lemma_` to access the annotation."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 37,
- "id": "da610560-62c3-48ab-a1b2-25e0b589bc61",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Import spaCy\n",
- "import spacy\n",
- "nlp = spacy.load('en_core_web_sm')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "98ead266-30f3-48ad-bc51-c1685487f000",
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "# Create a function to lemmatize text\n",
- "def lemmatize_text(text):\n",
- " '''Lemmatize the text input with spaCy annotations.'''\n",
- "\n",
- " # Step 1: Initialize an empty list to hold lemmas\n",
- " lemma = ...\n",
- "\n",
- " # Step 2: Apply the nlp pipeline to input text\n",
- " doc = ...\n",
- "\n",
- " # Step 3: Iterate over tokens in the text to get the token lemma\n",
- " for token in doc:\n",
- " lemma.append(...)\n",
- "\n",
- " # Step 4: Join lemmas together into a single string\n",
- " text_lemma = ' '.join(lemma)\n",
- " \n",
- " return text_lemma"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "cf36aab6-35dd-42a2-9b38-b7c432f021c6",
- "metadata": {},
- "source": [
- "Let's apply the function to the following example tweet first!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 39,
- "id": "742e82bb-5c42-4fa8-9101-5a0ea908db25",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "USER wow this just blew my mind\n",
- "==================================================\n",
- "USER wow this just blow my mind\n"
- ]
- }
- ],
- "source": [
- "# Apply the function to an example tweet\n",
- "print(tweets.iloc[33][\"text_processed\"])\n",
- "print(f\"{'='*50}\")\n",
- "print(lemmatize_text(tweets.iloc[33]['text_processed']))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "bbeda987-dc32-4979-b158-c24be7d1a420",
- "metadata": {},
- "source": [
- "And then let's lemmatize the tweet data and save the output to a new column `text_lemmatized`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 40,
- "id": "1ac128d2-1be5-4ef5-bb50-5b8d44ef8ee9",
- "metadata": {},
- "outputs": [],
- "source": [
- "# This may take a while!\n",
- "tweets['text_lemmatized'] = tweets['text_processed'].apply(lambda x: lemmatize_text(x))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "2c02aad6-4e71-4afc-80cf-31d4f39498b2",
- "metadata": {},
- "source": [
- "Now with the `text_lemmatized` column, let's create a third DTM. The parameter setting is the same as the second DTM. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 41,
- "id": "5f49d790-3c9d-4dc1-a5c9-72c306630412",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
aa
\n",
- "
aadv
\n",
- "
aadvantage
\n",
- "
aal
\n",
- "
abandon
\n",
- "
abc
\n",
- "
ability
\n",
- "
able
\n",
- "
aboard
\n",
- "
abq
\n",
- "
...
\n",
- "
yummy
\n",
- "
yup
\n",
- "
yvonne
\n",
- "
yvr
\n",
- "
yyj
\n",
- "
yyz
\n",
- "
zero
\n",
- "
zone
\n",
- "
zoom
\n",
- "
zurich
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
1
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
2
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
3
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- "
\n",
- "
4
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
...
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
0
\n",
- "
\n",
- " \n",
- "
\n",
- "
5 rows × 3553 columns
\n",
- "
"
+ "source": [
+ "tweets.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "80232c78-ac41-4d74-a581-76c9dac3b8f6",
+ "metadata": {
+ "id": "80232c78-ac41-4d74-a581-76c9dac3b8f6"
+ },
+ "source": [
+ "As a refresher, each row in this dataframe correponds to a tweet. The following columns are of main interests to us. There are other columns containing metadata of the tweet, such as the author of the tweet, when it was created, the timezone of the user, and others, which we will set aside for now.\n",
+ "\n",
+ "- `text` (`str`): the text of the tweet.\n",
+ "- `airline_sentiment` (`str`): the sentiment of the tweet, labeled as \"neutral,\" \"positive,\" or \"negative.\"\n",
+ "- `airline` (`str`): the airline that is tweeted about.\n",
+ "- `retweet count` (`int`): how many times the tweet was retweeted.\n",
+ "\n",
+ "To prepare us for sentiment classification, we'll partition the dataset to focus on the \"positive\" and \"negative\" tweets for now."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En esa línea de código se observa que el DataFrame tweets es filtrado para excluir todas las filas donde la columna airline_sentiment tenga el valor \"neutral\", de modo que solo permanecen los registros con sentimientos positivos o negativos. Posteriormente, con .reset_index(drop=True), se reinician los índices de las filas para que queden consecutivos y sin huecos, garantizando que el DataFrame resultante esté limpio y organizado para el análisis.*"
],
- "text/plain": [
- " aa aadv aadvantage aal abandon abc ability able aboard abq ... \\\n",
- "0 0 0 0 0 0 0 0 0 0 0 ... \n",
- "1 0 0 0 0 0 0 0 0 0 0 ... \n",
- "2 0 0 0 0 0 0 0 0 0 0 ... \n",
- "3 0 0 0 0 0 0 0 0 0 0 ... \n",
- "4 0 0 0 0 0 0 0 0 0 0 ... \n",
- "\n",
- " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
- "0 0 0 0 0 0 0 0 0 0 0 \n",
- "1 0 0 0 0 0 0 0 0 0 0 \n",
- "2 0 0 0 0 0 0 0 0 0 0 \n",
- "3 0 0 0 0 0 0 0 0 0 0 \n",
- "4 0 0 0 0 0 0 0 0 0 0 \n",
- "\n",
- "[5 rows x 3553 columns]"
- ]
- },
- "execution_count": 41,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Create the vectorizer (the same param setting as previous)\n",
- "vectorizer = CountVectorizer(lowercase=True,\n",
- " stop_words='english',\n",
- " min_df=2,\n",
- " max_df=0.95,\n",
- " max_features=None)\n",
- "\n",
- "# Fit, transform, and get tokens\n",
- "counts = vectorizer.fit_transform(tweets['text_lemmatized'])\n",
- "tokens = vectorizer.get_feature_names_out()\n",
- "\n",
- "# Create the third DTM\n",
- "third_dtm = pd.DataFrame(data=counts.todense(),\n",
- " index=tweets.index,\n",
- " columns=tokens)\n",
- "third_dtm.head()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 42,
- "id": "9859eb04-dbd2-4fa0-9798-65ed7496c297",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "(11541, 8751)\n",
- "(11541, 4471)\n",
- "(11541, 3553)\n"
- ]
- }
- ],
- "source": [
- "# Print the shapes of three DTMs\n",
- "print(first_dtm.shape)\n",
- "print(second_dtm.shape)\n",
- "print(third_dtm.shape)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fa94c8ac-e4f4-4b76-afdb-1d4af54a3eee",
- "metadata": {},
- "source": [
- "Let's print the top 10 most frequent tokens as usual. These tokens are now lemmas and their counts also change after lemmatization. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 43,
- "id": "5745ca29-97ed-4fe1-81db-7e402c8da674",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "digit 6927\n",
- "flight 4043\n",
- "hashtag 2633\n",
- "thank 1455\n",
- "hour 1134\n",
- "cancel 948\n",
- "delay 937\n",
- "service 937\n",
- "customer 902\n",
- "time 856\n",
- "dtype: int64"
- ]
- },
- "execution_count": 43,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Get the most frequent tokens in the third DTM\n",
- "third_dtm.sum().sort_values(ascending=False).head(10)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 44,
- "id": "16c63e6a-50c3-448a-9a56-a1d193cd6680",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "digit 6927\n",
- "flight 3320\n",
- "hashtag 2633\n",
- "cancelled 956\n",
- "thanks 921\n",
- "service 910\n",
- "just 801\n",
- "customer 726\n",
- "time 695\n",
- "help 687\n",
- "dtype: int64"
- ]
- },
- "execution_count": 44,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Compared to the most frequent tokens in the second DTM\n",
- "second_dtm.sum().sort_values(ascending=False).head(10)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "38363398-fdf5-456b-ae3d-cae9d5294140",
- "metadata": {},
- "source": [
- "\n",
- "\n",
- "# Term Frequency-Inverse Document Frequency \n",
- "\n",
- "So far, we're relying on word frequency to give us information about a document. This assumes if a word appears more often in a document, it's more informative. However, this may not always be the case. For example, we've already removed stop words because they are not informative, despite the fact that they appear many times in a document. We also know the word \"flight\" is among the most frequent words, but it is not that informative, because it appears in many documents. Since we're looking at airline tweets, we shouldn't be surprised to see the word \"flight\"!\n",
- "\n",
- "To remedy this, we use a weighting scheme called **tf-idf (term frequency-inverse document frequency)**. The big idea behind tf-idf is to weight a word not just by its frequency within a document, but also by its frequency in one document relative to the remaining documents. So, when we construct the DTM, we will be assigning each term a **tf-idf score**. Specifically, term $t$ in document $d$ is assigned a tf-idf score as follows:\n",
- "\n",
- "\n",
- "\n",
- "In essence, the tf-idf score of a word in a document is the product of two components: **term frequency (tf)** and **inverse document frequency (idf)**. The idf acts as a scaling factor. If a word occurs in all documents, then idf equals 1. No scaling will happen. But idf is typically greater than 1, which is the weight we assign to the word to make the tf-idf score higher, so as to highlight that the word is informative. In practice, we add 1 to both the denominator and numerator (\"add-1 smooth\") to prevent any issues with zero occurrences.\n",
- "\n",
- "We can also create a tf-idf DTM using `sklearn`. We'll use a `TfidfVectorizer` this time:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 45,
- "id": "f5e32d8a-c42d-475f-aab4-21eca8b1aee8",
- "metadata": {},
- "outputs": [],
- "source": [
- "from sklearn.feature_extraction.text import TfidfVectorizer"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 46,
- "id": "d23916c1-5693-456c-b71d-6d9d78d1e2e4",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create a tfidf vectorizer\n",
- "vectorizer = TfidfVectorizer(lowercase=True,\n",
- " stop_words='english',\n",
- " min_df=2,\n",
- " max_df=0.95,\n",
- " max_features=None)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 47,
- "id": "7af5b342-ab18-4766-9561-e38e50cd1e9b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "<11541x3553 sparse matrix of type ''\n",
- "\twith 88287 stored elements in Compressed Sparse Row format>"
- ]
- },
- "execution_count": 47,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Fit and transform \n",
- "tf_dtm = vectorizer.fit_transform(tweets['text_lemmatized'])\n",
- "tf_dtm"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 48,
- "id": "55e509c8-5402-4be0-9143-0e448fff7066",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
aa
\n",
- "
aadv
\n",
- "
aadvantage
\n",
- "
aal
\n",
- "
abandon
\n",
- "
abc
\n",
- "
ability
\n",
- "
able
\n",
- "
aboard
\n",
- "
abq
\n",
- "
...
\n",
- "
yummy
\n",
- "
yup
\n",
- "
yvonne
\n",
- "
yvr
\n",
- "
yyj
\n",
- "
yyz
\n",
- "
zero
\n",
- "
zone
\n",
- "
zoom
\n",
- "
zurich
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
...
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
\n",
- "
\n",
- "
1
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
...
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
\n",
- "
\n",
- "
2
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
...
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
\n",
- "
\n",
- "
3
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
...
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
\n",
- "
\n",
- "
4
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
...
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
0.0
\n",
- "
\n",
- " \n",
- "
\n",
- "
5 rows × 3553 columns
\n",
- "
"
+ "metadata": {
+ "id": "h1I96r9redwT"
+ },
+ "id": "h1I96r9redwT"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a1faaf90-8c01-4d25-9468-90c01823f0d5",
+ "metadata": {
+ "id": "a1faaf90-8c01-4d25-9468-90c01823f0d5"
+ },
+ "outputs": [],
+ "source": [
+ "tweets = tweets[tweets['airline_sentiment'] != 'neutral'].reset_index(drop=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7cb6b039-53e7-4afe-a9e0-b3522c12b2d7",
+ "metadata": {
+ "id": "7cb6b039-53e7-4afe-a9e0-b3522c12b2d7"
+ },
+ "source": [
+ "Let's take a look at a few tweets first!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, el comentario indica que se van a mostrar los primeros cinco tweets. El bucle for idx in range(5) recorre los índices del 0 al 4, y en cada iteración utiliza tweets['text'].iloc[idx] para acceder al contenido del tweet correspondiente en la columna text del DataFrame, imprimiéndolo en pantalla. De esta manera, se puede visualizar rápidamente cómo lucen los primeros cinco tweets del conjunto de datos.*"
],
- "text/plain": [
- " aa aadv aadvantage aal abandon abc ability able aboard abq ... \\\n",
- "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
- "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
- "2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
- "3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
- "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
- "\n",
- " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
- "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
- "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
- "2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
- "3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
- "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
- "\n",
- "[5 rows x 3553 columns]"
- ]
- },
- "execution_count": 48,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Create a tf-idf dataframe\n",
- "tfidf = pd.DataFrame(tf_dtm.todense(),\n",
- " columns=vectorizer.get_feature_names_out(),\n",
- " index=tweets.index)\n",
- "tfidf.head()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "45ba13ea-c429-4ff1-a9a2-abf27c4d0888",
- "metadata": {},
- "source": [
- "You may have noticed that the vocabulary size is the same as we saw in Challenge 2. This is because we used the same parameter setting when creating the vectorizer. But the values in the matrix are different—they are tf-idf scores instead of raw counts. "
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fa58c360-5c55-4fa0-8c55-1f00e68baa9a",
- "metadata": {},
- "source": [
- "## Interpret TF-IDF Values"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "bdad233d-ebc1-420f-9b67-c227c48f3e60",
- "metadata": {},
- "source": [
- "Let's take a look the document where a term has the highest tf-idf values. We'll use the `.idxmax()` method to find the index."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 49,
- "id": "995b511a-d448-4cfb-a6a0-22a465efd8a8",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "aa 10077\n",
- "aadv 9285\n",
- "aadvantage 9974\n",
- "aal 10630\n",
- "abandon 7859\n",
- " ... \n",
- "yyz 1350\n",
- "zero 2705\n",
- "zone 3177\n",
- "zoom 3920\n",
- "zurich 10622\n",
- "Length: 3553, dtype: int64"
- ]
- },
- "execution_count": 49,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Retrieve the index of the document\n",
- "tfidf.idxmax()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fccc0249-7c68-42ee-8290-ff41715e346b",
- "metadata": {},
- "source": [
- "For example, the term \"worst\" occurs most distinctively in the 918th tweet. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 50,
- "id": "09b222fb-ad8c-4767-a974-dd261370a06e",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "918"
- ]
- },
- "execution_count": 50,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tfidf.idxmax()['worst']"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "955a48bc-dc93-481b-ba49-29876fc577fb",
- "metadata": {},
- "source": [
- "Recall that this is the tweet where the word \"worst\" appears six times!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 51,
- "id": "079ee0e0-476f-4236-ba8a-615ba7a0efe8",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "\"USER is the worst. worst reservation policies. worst costumer service. worst worst worst. congrats, USER you're not that bad!\""
- ]
- },
- "execution_count": 51,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tweets['text_processed'].iloc[918]"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "9dd06bbc-e2fc-49e4-9354-efdaca5cfbd3",
- "metadata": {},
- "source": [
- "How about \"cancel\"? Let's take a look at another example. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 52,
- "id": "f809df1a-1178-4272-a415-42edb20173b2",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "5945"
- ]
- },
- "execution_count": 52,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tfidf.idxmax()['cancel']"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 53,
- "id": "8093b6a7-54ca-468a-9376-b3c0be0b6f9b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "'USER cancelled flighted 😢'"
- ]
- },
- "execution_count": 53,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tweets['text_processed'].iloc[5945]"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "163dcecd-dc8c-43a9-952d-5bc84a307b07",
- "metadata": {},
- "source": [
- "## 🥊 Challenge 3: Words with Highest Mean TF-IDF scores\n",
- "\n",
- "We have obtained tf-idf values for each term in each document. But what do these values tell us about the sentiments of tweets? Are there any words that are particularly informative for positive/negative tweets? \n",
- "\n",
- "To explore this, let's gather the indices of all positive/negative tweets and calculate the mean tf-idf scores of words appear in each category. \n",
- "\n",
- "We've provided the following starter code to guide you:\n",
- "- Subset the `tweets` dataframe according to the `airline_sentiment` label and retrieve the index of each subset (`.index`). Assign the index to `positive_index` or `negative_index`.\n",
- "- For each subset:\n",
- " - Retrieve the td-idf representation \n",
- " - Take the mean tf-idf values across the subset using `.mean()`\n",
- " - Sort the mean values in the descending order using `.sort_values()`\n",
- " - Get the top 10 terms using `.head()`\n",
- "\n",
- "Next, run `pos.plot` and `neg.plot` to plot the words with the highest mean tf-idf scores for each subset. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2bfbf838-9ff6-48b8-ad5d-5e75304fe060",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Complete the boolean masks \n",
- "positive_index = tweets[...].index\n",
- "negative_index = tweets[...].index"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8c67ea1f-de9e-49a9-94f2-a3351446e364",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Complete the following two lines\n",
- "pos = tfidf.loc[...].mean().sort_values(...).head(...)\n",
- "neg = tfidf.loc[...].mean().sort_values(...).head(...)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "f1e29043-8c78-4e41-81d2-b4552030b457",
- "metadata": {},
- "outputs": [],
- "source": [
- "pos.plot(kind='barh', \n",
- " xlim=(0, 0.18),\n",
- " color='cornflowerblue',\n",
- " title='Top 10 terms with the highest mean tf-idf values for positive tweets');"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "e8b25940-2372-4755-818e-f75e4d23daf9",
- "metadata": {},
- "outputs": [],
- "source": [
- "neg.plot(kind='barh', \n",
- " xlim=(0, 0.18),\n",
- " color='darksalmon',\n",
- " title='Top 10 terms with the highest mean tf-idf values for negative tweets');"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "77bca876-9649-46f3-bd4f-f9f68fea649a",
- "metadata": {},
- "source": [
- "🔔 **Question**: How would you interpret these results? Share your thoughts in the chat!"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "da410cb3-a452-441b-a94d-8f751d59d7a6",
- "metadata": {},
- "source": [
- "\n",
- "\n",
- "## 🎬 **Demo**: Sentiment Classification Using the TF-IDF Representation\n",
- "\n",
- "Now that we have a tf-idf representation of the text, we are ready to do sentiment analysis!\n",
- "\n",
- "In this demo, we will use a logistic regression model to perform the classification task. Here we briefly step through how logistic regression works as one of the supervised Machine Learning methods, but feel free to explore our workshop on [Python Machine Learning Fundamentals](https://github.com/dlab-berkeley/Python-Machine-Learning) if you want to learn more about it.\n",
- "\n",
- "Logistic regression is a linear model, with which we use to predict the label of a tweet, based on a set of features ($x_1, x_2, x_3, ..., x_i$), as shown below:\n",
- "\n",
- "$$\n",
- "L = \\beta_1 x_1 + \\beta_2 x_2 + \\cdots + \\beta_T x_T\n",
- "$$\n",
- "\n",
- "The list of features we'll pass to the model is the vocabulary of the DTM. We also feed the model with a portion of the data, known as the training set, along with other model specification, to learn the coeffient ($\\beta_1, \\beta_2, \\beta_3, ..., \\beta_i$) of each feature. The coefficients tell us whether a feature contributes positively or negatively to the predicted value. The predicted value corresponds to adding all features (multiplied by their coefficients) up, and the predicted value gets passed to a [sigmoid function](https://en.wikipedia.org/wiki/Sigmoid_function) to be converted into the probability space, which tells us whether the predicted label is positive (when $p>0.5$) or negative (when $p<0.5$). \n",
- "\n",
- "The remaining portion of the data, known as the test set, is used to test whether the learned coefficients could be generalized to unseen data. \n",
- "\n",
- "Now that we already have the tf-idf dataframe, the feature set is ready. Let's dive into model specification!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 55,
- "id": "33413d63-87eb-489f-b374-3cfeaa51cf3c",
- "metadata": {},
- "outputs": [],
- "source": [
- "from sklearn.linear_model import LogisticRegressionCV\n",
- "from sklearn.model_selection import train_test_split"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "ee87ff74-3fbb-472a-b795-6f4d18fab215",
- "metadata": {},
- "source": [
- "We'll use the `train_test_split` function from `sklearn` to separate our data into two sets:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 56,
- "id": "64cec8b9-14d9-4897-9c02-cc89fcf7b3c6",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Train-test split\n",
- "X = tfidf\n",
- "y = tweets['airline_sentiment']\n",
- "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "066771d8-2f31-4646-9a1b-6d2b1b9b208c",
- "metadata": {},
- "source": [
- "The `fit_logistic_regression` function is written below to streamline the training process."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 57,
- "id": "d46de0b2-af00-4a1d-b4cd-31b96ce545d1",
- "metadata": {},
- "outputs": [],
- "source": [
- "def fit_logistic_regression(X, y):\n",
- " '''Fits a logistic regression model to provided data.'''\n",
- " model = LogisticRegressionCV(Cs=10,\n",
- " penalty='l1',\n",
- " cv=5,\n",
- " solver='liblinear',\n",
- " class_weight='balanced',\n",
- " random_state=42,\n",
- " refit=True).fit(X, y)\n",
- " return model"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "124aa7ea-1bc1-43e2-beeb-0ba2da9b2df9",
- "metadata": {},
- "source": [
- "We'll fit the model and compute the training and test accuracy."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 58,
- "id": "773963bd-6603-4fad-884b-09ce60afab18",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Fit the logistic regression model\n",
- "model = fit_logistic_regression(X_train, y_train)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 59,
- "id": "e10d06c1-d884-45d4-a03d-dd5d40bf70aa",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Training accuracy: 0.9455601998164951\n",
- "Test accuracy: 0.894919168591224\n"
- ]
- }
- ],
- "source": [
- "# Get the training and test accuracy\n",
- "print(f\"Training accuracy: {model.score(X_train, y_train)}\")\n",
- "print(f\"Test accuracy: {model.score(X_test, y_test)}\")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "d4e186c5-1719-4deb-bdb4-614a9980f058",
- "metadata": {},
- "source": [
- "The model achieved ~94% accuracy on the training set and ~89% on the test set—that's pretty good! The model generalizes reasonably well to the test data."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "310dac39-4753-4ae8-8dfa-e65e5824cccb",
- "metadata": {},
- "source": [
- "Next, let's also take a look at the fitted coefficients to see if what we see makes sense. \n",
- "\n",
- "We can access them using `coef_`, and we can match each coefficient to the tokens from the vectorizer:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 60,
- "id": "6dcb6ef1-13b3-437e-813c-7118911847a4",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Get coefs of all features\n",
- "coefs = model.coef_.ravel()\n",
- "\n",
- "# Get all tokens\n",
- "tokens = vectorizer.get_feature_names_out()\n",
- "\n",
- "# Create a token-coef dataframe\n",
- "importance = pd.DataFrame()\n",
- "importance['token'] = tokens\n",
- "importance['coefs'] = coefs"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 61,
- "id": "3e63814e-9c0d-4f7a-a5e0-72cca2758d71",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
token
\n",
- "
coefs
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
2724
\n",
- "
rude
\n",
- "
-11.138668
\n",
- "
\n",
- "
\n",
- "
2784
\n",
- "
screw
\n",
- "
-9.962456
\n",
- "
\n",
- "
\n",
- "
2727
\n",
- "
ruin
\n",
- "
-9.849836
\n",
- "
\n",
- "
\n",
- "
1505
\n",
- "
hour
\n",
- "
-9.282416
\n",
- "
\n",
- "
\n",
- "
389
\n",
- "
break
\n",
- "
-7.949487
\n",
- "
\n",
- "
\n",
- "
2280
\n",
- "
pay
\n",
- "
-7.823908
\n",
- "
\n",
- "
\n",
- "
458
\n",
- "
cancel
\n",
- "
-7.534084
\n",
- "
\n",
- "
\n",
- "
264
\n",
- "
bad
\n",
- "
-7.357206
\n",
- "
\n",
- "
\n",
- "
1872
\n",
- "
luggage
\n",
- "
-7.093317
\n",
- "
\n",
- "
\n",
- "
3034
\n",
- "
strand
\n",
- "
-7.046890
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
+ "metadata": {
+ "id": "nSGYf82xexRJ"
+ },
+ "id": "nSGYf82xexRJ"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "438830e6-1064-47fe-b578-a1ca693a0ed0",
+ "metadata": {
+ "id": "438830e6-1064-47fe-b578-a1ca693a0ed0",
+ "outputId": "ae408e8f-86fe-4502-c316-b3129c5d76ed"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "@VirginAmerica plus you've added commercials to the experience... tacky.\n",
+ "@VirginAmerica it's really aggressive to blast obnoxious \"entertainment\" in your guests' faces & they have little recourse\n",
+ "@VirginAmerica and it's a really big bad thing about it\n",
+ "@VirginAmerica seriously would pay $30 a flight for seats that didn't have this playing.\n",
+ "it's really the only bad thing about flying VA\n",
+ "@VirginAmerica yes, nearly every time I fly VX this “ear worm” won’t go away :)\n"
+ ]
+ }
],
- "text/plain": [
- " token coefs\n",
- "2724 rude -11.138668\n",
- "2784 screw -9.962456\n",
- "2727 ruin -9.849836\n",
- "1505 hour -9.282416\n",
- "389 break -7.949487\n",
- "2280 pay -7.823908\n",
- "458 cancel -7.534084\n",
- "264 bad -7.357206\n",
- "1872 luggage -7.093317\n",
- "3034 strand -7.046890"
- ]
- },
- "execution_count": 61,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Get the top 10 tokens with lowest coefs\n",
- "neg_coef = importance.sort_values('coefs').head(10)\n",
- "neg_coef"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 62,
- "id": "0d596bf7-753c-40cd-ac52-4a37163650ae",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
token
\n",
- "
coefs
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
3165
\n",
- "
thankful
\n",
- "
8.002975
\n",
- "
\n",
- "
\n",
- "
1091
\n",
- "
exceptional
\n",
- "
8.136278
\n",
- "
\n",
- "
\n",
- "
1563
\n",
- "
impressed
\n",
- "
8.501364
\n",
- "
\n",
- "
\n",
- "
648
\n",
- "
compliment
\n",
- "
8.981360
\n",
- "
\n",
- "
\n",
- "
1373
\n",
- "
great
\n",
- "
9.080558
\n",
- "
\n",
- "
\n",
- "
3498
\n",
- "
wonderful
\n",
- "
9.401606
\n",
- "
\n",
- "
\n",
- "
1089
\n",
- "
excellent
\n",
- "
10.147230
\n",
- "
\n",
- "
\n",
- "
250
\n",
- "
awesome
\n",
- "
10.315909
\n",
- "
\n",
- "
\n",
- "
1746
\n",
- "
kudo
\n",
- "
11.623828
\n",
- "
\n",
- "
\n",
- "
3164
\n",
- "
thank
\n",
- "
16.027534
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
+ "source": [
+ "# Print first five tweets\n",
+ "for idx in range(5):\n",
+ " print(tweets['text'].iloc[idx])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0d6746f8-b29c-40d4-bef6-b4afd4cd6cc1",
+ "metadata": {
+ "id": "0d6746f8-b29c-40d4-bef6-b4afd4cd6cc1"
+ },
+ "source": [
+ "We can already see that some of these tweets contain negative sentiment—how can we tell this is the case?\n",
+ "\n",
+ "Next, let's take a look at the distribution of sentiment labels in this dataset."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, el comentario indica que se va a crear un gráfico de barras que muestre la cantidad de tweets según su sentimiento. La función sns.countplot de seaborn se utiliza para contar cuántos tweets hay de cada tipo en la columna airline_sentiment. Se especifica que el eje x será esa columna, que las barras tendrán el color cornflowerblue y que el orden de las categorías será primero \"positive\" y luego \"negative\". Esto permite visualizar de manera clara cuántos tweets positivos y negativos hay en el conjunto de datos.*"
],
- "text/plain": [
- " token coefs\n",
- "3165 thankful 8.002975\n",
- "1091 exceptional 8.136278\n",
- "1563 impressed 8.501364\n",
- "648 compliment 8.981360\n",
- "1373 great 9.080558\n",
- "3498 wonderful 9.401606\n",
- "1089 excellent 10.147230\n",
- "250 awesome 10.315909\n",
- "1746 kudo 11.623828\n",
- "3164 thank 16.027534"
- ]
- },
- "execution_count": 62,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# Get the top 10 tokens with highest coefs\n",
- "pos_coef = importance.sort_values('coefs').tail(10)\n",
- "pos_coef "
- ]
- },
- {
- "cell_type": "markdown",
- "id": "7b3b7893-caa0-4281-98f0-92c9e7b31953",
- "metadata": {},
- "source": [
- "Let's plot the top 10 tokens with the highest/lowest coefficients. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 63,
- "id": "17b1223b-e5c1-4992-bb7e-0a99651c3729",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnwAAAGxCAYAAAD4c2uhAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABh3ElEQVR4nO3deVxN+f8H8Ndpv+3aC5UtyVKaMJZRGCNlGca+hjFj35fxtWXsxjKGYTCjGGMdxmDsVPYthZFtkm1ERiqFUvfz+8Oj83OVlOLq9Ho+HufxcM/yOe/P6dZ9+ZzlSkIIASIiIiJSLB1tF0BERERE7xYDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDH5U4kiTlawoPD3/ntaxevRqdOnVC5cqVoaOjA1dX19eum5qaimHDhsHJyQlGRkbw8vLC+vXr87WfnTt3Ijg4uFC1urq6okWLFoVq40MXHh6e42ef17GTJAmDBg0q1L5+//33N64bFBSU53sjL8HBwZAkCf/9999bbf+2ZsyYga1bt77Xfb6NRYsWoWLFijAwMIAkSUhKSgIATJgwAc7OztDT04OlpSUAwM/PD35+fu+0npiYGAQHB+PGjRvvdD+vk9vvACmDnrYLIHrfjh8/rvF66tSpCAsLw8GDBzXme3h4vPNafv31V9y7dw+1a9eGWq3G8+fPX7tu27Ztcfr0acyaNQtubm5Yu3YtOnfuDLVajS5duuS5n507d+LHH38sdOhTOm9vbxw/flzjZ/8hHLuJEydi6NChWtv/25gxYwbatWuHzz//XNulvFZ0dDSGDBmCL7/8Ej179oSenh7MzMzw559/Yvr06Rg/fjyaN28OQ0NDAMCSJUveeU0xMTGYMmUK/Pz83jrkE+WGgY9KnI8//ljjta2tLXR0dHLMfx/27NkDHZ0XA+0tWrTA33//net6O3fuxL59++SQBwCNGjXCzZs3MXr0aHTs2BG6urrvrW6lMjc318r74E0qVKig7RIU6eLFiwCAvn37onbt2vL87N/DIUOGwM7OTp7/Pv4TSPSu8JQuUS4SExMxYMAAlC5dGgYGBihfvjzGjx+P9PR0jfWyT+ktW7YMbm5uMDQ0hIeHR75PtWaHvTf5448/YGpqivbt22vM79WrF+7evYuTJ0++dtugoCD8+OOPcr3ZU/Ypo2fPnmHcuHEoV64cDAwMULp0aQwcOFA+tZWXJUuWQE9PD5MnT5bn7d+/H02aNIG5uTmMjY1Rv359HDhwQGO77NOMFy9eROfOnWFhYQF7e3v07t0bycnJGutu2rQJderUgYWFBYyNjVG+fHn07t07z7rat2+PqlWrasxr2bIlJEnCpk2b5Hlnz56FJEnYvn07gJyns9507LL9+uuvqFKlCoyNjeHp6YkdO3a88dhle/78OcaPHw8nJyeYm5vj008/xZUrVzTWye2UblJSEvr06QMrKyuYmpoiMDAQ169fhyRJuY5G3r9//43HWgiBJUuWwMvLCyqVCqVKlUK7du1w/fp1jfWioqLQokUL2NnZwdDQEE5OTggMDMSdO3fkY5WWloZVq1bJx+xNp0LT09Px7bffokqVKjAyMoK1tTUaNWqEY8eOyesU5L26YcMG1K1bFyYmJjA1NUWzZs0QFRUlL/fz80O3bt0AAHXq1IEkSfJxnjBhAgDA3t5e43jmdko3IyMD06ZNg7u7OwwNDWFra4tevXrhwYMHGutlXxKxe/dueHt7Q6VSwd3dHStXrpTXCQ0NlX/HGzVqJB+70NDQXI/Z1q1bIUlSjt8vAFi6dCkkScL58+cBAGfOnEGnTp3g6uoKlUoFV1dXdO7cGTdv3sy17Ze97lR2bu/L/B6PgwcPws/PD9bW1lCpVHB2dsYXX3yBJ0+evLEeekuCqITr2bOnMDExkV8/ffpU1KhRQ5iYmIi5c+eKvXv3iokTJwo9PT0REBCgsS0AUbZsWeHh4SHWrVsntm3bJvz9/QUAsWnTpgLVERgYKFxcXHJd9vHHH4tatWrlmP/3338LAGLZsmWvbfeff/4R7dq1EwDE8ePH5enZs2dCrVaLZs2aCT09PTFx4kSxd+9eMXfuXGFiYiJq1qwpnj17Jrfj4uIiAgMDhRBCqNVqMXLkSKGvry9CQkLkdX799VchSZL4/PPPxZYtW8T27dtFixYthK6urti/f7+83uTJkwUAUblyZTFp0iSxb98+MX/+fGFoaCh69eolr3fs2DEhSZLo1KmT2Llzpzh48KAICQkR3bt3z/NY/vTTTwKAuHv3rhBCiOfPnwszMzOhUqlE37595fVmz54t9PT0REpKihBCiLCwMAFAhIWFvfHYCfHi5+/q6ipq164tNm7cKHbu3Cn8/PyEnp6eiI2NzbPG7H25urqKrl27ir/++kusW7dOODs7i0qVKonMzEx53Z49e2q8N7KyskSDBg2EkZGRmDVrlti7d6+YMmWKqFSpkgAgJk+eXOBjLYQQffv2Ffr6+mLkyJFi9+7dYu3atcLd3V3Y29uLe/fuCSGESE1NFdbW1sLHx0ds3LhRREREiA0bNoh+/fqJmJgYIYQQx48fFyqVSgQEBMjH7OLFi689Fs+fPxeNGjUSenp6YtSoUWLnzp1i27Zt4n//+59Yt26dEEIU6L06ffp0IUmS6N27t9ixY4fYsmWLqFu3rjAxMZHruHjxopgwYYIAIEJCQsTx48fFP//8I86ePSv69OkjAIjdu3eL48ePi9u3bwshhPD19RW+vr4aPwd/f39hYmIipkyZIvbt2yd+/vlnUbp0aeHh4SGePHkir+vi4iLKlCkjPDw8xOrVq8WePXtE+/btBQAREREhhBAiISFBzJgxQwAQP/74o3zsEhISXnvc7OzsRNeuXXMsq127tvD29pZfb9q0SUyaNEn88ccfIiIiQqxfv174+voKW1tb8eDBA3m9V38Hcut3ttzel/k5HnFxccLIyEg0bdpUbN26VYSHh4vffvtNdO/eXTx69CjXvlLhMfBRifdq4MsOCxs3btRYb/bs2QKA2Lt3rzwPgFCpVPKHoRBCZGZmCnd3d1GxYsUC1ZFX4KtUqZJo1qxZjvl3794VAMSMGTPybHvgwIEit//f7d69WwAQc+bM0Zi/YcMGAUAsX75cnpcd+J48eSK++OILYWFhoRHi0tLShJWVlWjZsqVGW1lZWcLT01PUrl1bnpcdQl7d74ABA4SRkZFQq9VCCCHmzp0rAIikpKQ8+/eqf/75RwAQq1evFkIIceTIEQFAjBkzRpQrV05er2nTpqJevXry69w+7F537IR48fO3t7eXA6MQQty7d0/o6OiImTNn5llj9r5e/U/Exo0b5YCZ7dUP1r/++ksAEEuXLtXYdubMma8NfG861sePHxcAxLx58zTWu337tlCpVGLMmDFCCCHOnDkjAIitW7fm2T8TExPRs2fPPNfJtnr1agFArFix4rXr5Pe9euvWLaGnpycGDx6ssd7jx4+Fg4OD6NChgzwvJCREABCnT5/WWDf7mL0chITIGXzWrVsnAIjNmzdrrHf69GkBQCxZskSe5+LiIoyMjMTNmzfleU+fPhVWVlbi66+/ludt2rQpx3swLyNGjBAqlUrjdyQmJkYAEIsWLXrtdpmZmSI1NVWYmJiIhQsXyvMLE/jyezx+//13AUBER0fnq49UNHhKl+gVBw8ehImJCdq1a6cxPygoCABynD5p0qQJ7O3t5de6urro2LEj/vnnH/kUV1GQJOmtluUl+0aV7L5la9++PUxMTHL09eHDh2jcuDFOnTqFI0eOoEmTJvKyY8eOITExET179kRmZqY8qdVq+Pv74/Tp00hLS9Nor1WrVhqva9SogWfPniEhIQEAUKtWLQBAhw4dsHHjRvz777/56leFChXg6uqK/fv3AwD27duH6tWro1u3boiLi0NsbCzS09Nx5MgRfPrpp/lq83UaNWoEMzMz+bW9vT3s7OzydaoMyP0YAMhz+4iICAAvjsvLsq/vzO9+Xj7WO3bsgCRJ6Natm8bPz8HBAZ6envJp7ooVK6JUqVIYO3YsfvrpJ8TExOSrn3nZtWsXjIyM8jxVn9/36p49e5CZmYkePXpo9MPIyAi+vr5Fevfpjh07YGlpiZYtW2rsy8vLCw4ODjn25eXlBWdnZ/m1kZER3Nzc8v1eyU3v3r3x9OlTbNiwQZ4XEhICQ0NDjZu5UlNTMXbsWFSsWBF6enrQ09ODqakp0tLScOnSpbfe/8vyezy8vLxgYGCAr776CqtWrcpxyQC9Gwx8RK94+PAhHBwccoQoOzs76Onp4eHDhxrzHRwccrSRPe/Vdd+WtbV1rm0lJiYCAKysrN6q3YcPH0JPTw+2trYa8yVJgoODQ459Xr16FSdPnkTz5s1RrVo1jWX3798HALRr1w76+voa0+zZsyGEkOt9uV8vy74b8unTpwCAhg0bYuvWrfIHeJkyZVCtWjWsW7fujX1r0qSJHAL279+Ppk2bonr16rC3t8f+/ftx9OhRPH36tNCB79U+ZPcjuw8F3f7VY5Cb7J/bqz/3l//jUdD93L9/H0II2Nvb5/j5nThxQn6si4WFBSIiIuDl5YX//e9/qFq1KpycnDB58uQ87zLPy4MHD+Dk5JTnNa35fa9mvw9r1aqVox8bNmwo0sfT3L9/H0lJSTAwMMixr3v37uXYV2HfK7mpWrUqatWqhZCQEABAVlYW1qxZg9atW2u8P7p06YLFixfjyy+/xJ49e3Dq1CmcPn0atra2hdr/y/J7PCpUqID9+/fDzs4OAwcORIUKFVChQgUsXLiwSOqg3PEuXaJXWFtb4+TJkxBCaIS+hIQEZGZmwsbGRmP9e/fu5Wgje15uf+DfRvXq1bFu3TpkZmZCT+//f20vXLgAADnCV35ZW1sjMzMTDx480PggFULg3r178ghbtrp166J9+/bo06cPgBcXhmd/SGcfl0WLFr32Tte8AsnrtG7dGq1bt0Z6ejpOnDiBmTNnokuXLnB1dUXdunVfu12TJk3wyy+/4NSpUzh58qR8IX7jxo2xb98+3Lx5E6amph/kXblvkv1zS0xM1PhQz+29mF82NjaQJAmHDx+Ww+DLXp5XvXp1rF+/HkIInD9/HqGhofj222+hUqnwzTffFHjftra2OHLkCNRq9WtDX37fq9nvw99//x0uLi4FrqUgbGxsYG1tjd27d+e6/OWR33epV69eGDBgAC5duoTr168jPj4evXr1kpcnJydjx44dmDx5ssbPJz09Pcd/wnJjZGSU4wYfADkCbUGOxyeffIJPPvkEWVlZOHPmDBYtWoRhw4bB3t4enTp1emNNVHAc4SN6RZMmTZCamprjobGrV6+Wl7/swIED8qgC8OJ/2Bs2bECFChVQpkyZIqmpTZs2SE1NxebNmzXmr1q1Ck5OTqhTp06e279u1Ci7L2vWrNGYv3nzZqSlpeXoKwD07NkT69evR0hICHr06IGsrCwAQP369WFpaYmYmBj4+PjkOhkYGBSs46/0wdfXF7NnzwYAjTsuc9OkSRNIkoSJEydCR0cHDRs2BAB8+umnCAsLw759+9CwYUPo6+u/cb9A3iNu75uvry8AaJzGA5Dvu8Nz06JFCwgh8O+//+b6s6tevXqObSRJgqenJxYsWABLS0ucPXtWXlaQkavmzZvj2bNnr70bFcj/e7VZs2bQ09NDbGzsa9+HRaVFixZ4+PAhsrKyct1P5cqVC9zm27zfOnfuDCMjI4SGhiI0NBSlS5fGZ599Ji+XJAlCiBxB/ueff5Z/f/Pi6uqKq1evajyl4OHDhxp3UANvdzx0dXVRp04d+W74l99DVLQ4wkf0ih49euDHH39Ez549cePGDVSvXh1HjhzBjBkzEBAQkOMUoI2NDRo3boyJEyfCxMQES5YsweXLl/P14RsTEyNfA3Xv3j08efJE/uYFDw8P+blfzZs3R9OmTdG/f3+kpKSgYsWKWLduHXbv3o01a9a88Rl82R/Ws2fPRvPmzaGrq4saNWqgadOmaNasGcaOHYuUlBTUr18f58+fx+TJk1GzZk1079491/batWsHY2NjtGvXDk+fPsW6detgamqKRYsWoWfPnkhMTES7du1gZ2eHBw8e4Ny5c3jw4AGWLl36xmPyskmTJuHOnTto0qQJypQpg6SkJCxcuBD6+vpy6HkdOzs7VKtWDXv37kWjRo1gbGwM4EXgS0xMRGJiIubPn//GGl537AoTXgvL398f9evXx8iRI5GSkoKPPvoIx48fl/9Tkt/H/bysfv36+Oqrr9CrVy+cOXMGDRs2hImJCeLj43HkyBFUr14d/fv3x44dO7BkyRJ8/vnnKF++PIQQ2LJlC5KSktC0aVO5verVqyM8PBzbt2+Ho6MjzMzMXhuAOnfujJCQEPTr1w9XrlxBo0aNoFarcfLkSVSpUgWdOnXK93vV1dUV3377LcaPH4/r16/D398fpUqVwv3793Hq1CmYmJhgypQpb3HUc+rUqRN+++03BAQEYOjQoahduzb09fVx584dhIWFoXXr1mjTpk2B2swerV++fDnMzMxgZGSEcuXK5Xm2wNLSEm3atEFoaCiSkpIwatQojfeAubk5GjZsiO+++w42NjZwdXVFREQEfvnlF/lbRPLSvXt3LFu2DN26dUPfvn3x8OFDzJkzB+bm5m91PH766SccPHgQgYGBcHZ2xrNnz+TH0xT2EgvKg9ZuFyH6QLx6l64QQjx8+FD069dPODo6Cj09PeHi4iLGjRun8egHIV7cpTlw4ECxZMkSUaFCBaGvry/c3d3Fb7/9lq99Z98NmNv08p2WQry4y3DIkCHCwcFBGBgYiBo1asiPrHiT9PR08eWXXwpbW1shSZIAIOLi4oQQL+4UHDt2rHBxcRH6+vrC0dFR9O/fP8fjEV5+LEu2sLAwYWpqKvz9/eVHLkRERIjAwEBhZWUl9PX1RenSpUVgYKDGY2pedxdk9l2T2bXt2LFDNG/eXJQuXVoYGBgIOzs7ERAQIA4fPpyvfg8fPlwAENOnT9eYn/34kvPnz+foD165QzGvY5f983+Vi4vLG+9Qzd7Xq4/viYuLkx8Vku3VuyGFECIxMVH06tVLWFpaCmNjY9G0aVNx4sQJAUDjrsv8HutsK1euFHXq1BEmJiZCpVKJChUqiB49eogzZ84IIYS4fPmy6Ny5s6hQoYJQqVTCwsJC1K5dW4SGhmq0Ex0dLerXry+MjY0FgFzv8nzZ06dPxaRJk0SlSpWEgYGBsLa2Fo0bNxbHjh3TWCc/71UhhNi6dato1KiRMDc3F4aGhsLFxUW0a9dO487ywt6lK8SLR6PMnTtXeHp6CiMjI2Fqairc3d3F119/La5duyavl9vvz+va/P7770W5cuWErq5ujvfC6+zdu1f+23H16tUcy+/cuSO++OILUapUKWFmZib8/f3F33//neO9mtvvgBBCrFq1SlSpUkUYGRkJDw8PsWHDhlzfl/k5HsePHxdt2rQRLi4uwtDQUFhbWwtfX1+xbdu2N/aT3p4khBDvKVsSKY4kSRg4cCAWL16s7VKIsHbtWnTt2hVHjx5FvXr1tF0OEX1AeEqXiKgYWrduHf79919Ur14dOjo6OHHiBL777js0bNiQYY+IcmDgIyIqhszMzLB+/XpMmzYNaWlpcHR0RFBQEKZNm6bt0ojoA8RTukREREQKx8eyEBERESkcAx8RERGRwjHwERERESkcb9ooAdRqNe7evQszM7Mc3w9LREREHyYhBB4/fvzG75rODwa+EuDu3bsoW7astssgIiKit3D79u1Cf1UnA18JkP2F1bdv387xVThERET0YUpJSUHZsmXlz/HCYOArAbJP45qbmzPwERERFTNFcTkWb9ogIiIiUjgGPiIiIiKFY+AjIiIiUjhew0dERET5IoRAZmYmsrKytF2KIujq6kJPT++9PDKNgY+IiIjeKCMjA/Hx8Xjy5Im2S1EUY2NjODo6wsDA4J3uh4GPiIiI8qRWqxEXFwddXV04OTnBwMCAD/IvJCEEMjIy8ODBA8TFxaFSpUqFfrhyXhj4SpDBKxJhoMrUdhlEirdigJW2SyAqUhkZGVCr1ShbtiyMjY21XY5iqFQq6Ovr4+bNm8jIyICRkdE72xdv2iAiIqJ8eZcjUCXV+zqm/MkRERERKRwDHxEREZHClbhr+MLDw9GoUSM8evQIlpaW72w/N27cQLly5RAVFQUvL69c17l37x66d++OY8eOQV9fH0lJSW9s933VT0RElB99lyS+t319iNfHXr58GUFBQYiOjoa7uzuio6O1XVKuFB/4/Pz84OXlhe+//17bpeSwYMECxMfHIzo6GhYWFtouh4iIiApo8uTJMDExwZUrV2Bqaqrtcl5L8YHvQxYbG4uPPvoIlSpV0nYpRERE9BZiY2MRGBgIFxcXbZeSJ0VfwxcUFISIiAgsXLgQkiRBkiTcuHEDABAZGQkfHx8YGxujXr16uHLlirxdbGwsWrduDXt7e5iamqJWrVrYv3+/Rtuurq6YMWMGevfuDTMzMzg7O2P58uWvrUWtVqNv375wc3PDzZs34erqis2bN2P16tWQJAlBQUG4ceMGJEnSGA5OSkqCJEkIDw8vykNDRERUIqjVasyePRsVK1aEoaEhnJ2dMX36dADAhQsX0LhxY6hUKlhbW+Orr75CamqqxvYhISGoUqUKjIyM4O7ujiVLlsjLJElCZGQkvv32W0iShODgYGRkZGDQoEFwdHSEkZERXF1dMXPmzPfa59woOvAtXLgQdevWRd++fREfH4/4+HiULVsWADB+/HjMmzcPZ86cgZ6eHnr37i1vl5qaioCAAOzfvx9RUVFo1qwZWrZsiVu3bmm0P2/ePPj4+CAqKgoDBgxA//79cfny5Rx1ZGRkoEOHDjhz5gyOHDkCFxcXnD59Gv7+/ujQoQPi4+OxcOHCIut3eno6UlJSNCYiIqKSaNy4cZg9ezYmTpyImJgYrF27Fvb29njy5An8/f1RqlQpnD59Gps2bcL+/fsxaNAgedsVK1Zg/PjxmD59Oi5duoQZM2Zg4sSJWLVqFQAgPj4eVatWxciRIxEfH49Ro0bhhx9+wLZt27Bx40ZcuXIFa9asgaurq5Z6//8UfUrXwsICBgYGMDY2hoODAwDIgWz69Onw9fUFAHzzzTcIDAzEs2fPYGRkBE9PT3h6esrtTJs2DX/88Qe2bdum8UYICAjAgAEDAABjx47FggULEB4eDnd3d3md1NRUBAYG4unTpwgPD5ev1bO1tYWhoSFUKpVc26NHj4qk3zNnzsSUKVOKpC0iIqLi6vHjx1i4cCEWL16Mnj17AgAqVKiABg0aYMWKFXj69ClWr14NExMTAMDixYvRsmVLzJ49G/b29pg6dSrmzZuHtm3bAgDKlSuHmJgYLFu2DD179oSDgwP09PRgamoqf5bfunULlSpVQoMGDSBJ0gdzqlfRI3x5qVGjhvxvR0dHAEBCQgIAIC0tDWPGjIGHhwcsLS1hamqKy5cv5xjhe7kNSZLg4OAgt5Gtc+fOSE1Nxd69e9/bjRnjxo1DcnKyPN2+ffu97JeIiOhDcunSJaSnp6NJkya5LvP09JTDHgDUr18farUaV65cwYMHD3D79m306dMHpqam8jRt2jTExsa+dp/Zd+xWrlwZQ4YMwd69e99J3wpK0SN8edHX15f/nf19gGq1GgAwevRo7NmzB3PnzkXFihWhUqnQrl07ZGRkvLaN7Hay28gWEBCANWvW4MSJE2jcuHGeNWU/bVsIIc97/vx5AXsGGBoawtDQsMDbERERKYlKpXrtMiHEa78P+OXP8xUrVqBOnToay3V1dV/brre3N+Li4rBr1y7s378fHTp0wKefforff//9LXpQdBQ/wmdgYICsrKwCbXP48GEEBQWhTZs2qF69OhwcHOSbPQqqf//+mDVrFlq1aoWIiIg817W1tQXw4pqAbB/q83yIiIg+dJUqVYJKpcKBAwdyLPPw8EB0dDTS0tLkeUePHoWOjg7c3Nxgb2+P0qVL4/r166hYsaLGVK5cuTz3a25ujo4dO2LFihXYsGEDNm/ejMTE9/e8wtwofoTP1dUVJ0+exI0bN2BqappjBC43FStWxJYtW9CyZUtIkoSJEyfma7vXGTx4MLKystCiRQvs2rULDRo0yHU9lUqFjz/+GLNmzYKrqyv+++8/TJgw4a33S0REVJIZGRlh7NixGDNmDAwMDFC/fn08ePAAFy9eRNeuXTF58mT07NkTwcHBePDgAQYPHozu3bvD3t4eABAcHIwhQ4bA3NwczZs3R3p6Os6cOYNHjx5hxIgRue5zwYIFcHR0hJeXF3R0dLBp0yY4ODho/csSFB/4Ro0ahZ49e8LDwwNPnz5FSEjIG7dZsGABevfujXr16sHGxgZjx44t9J2uw4YNg1qtRkBAAHbv3o169erlut7KlSvRu3dv+Pj4oHLlypgzZw4+++yzQu2biIjoXfkQv/3iZRMnToSenh4mTZqEu3fvwtHREf369YOxsTH27NmDoUOHolatWjA2NsYXX3yB+fPny9t++eWXMDY2xnfffYcxY8bAxMQE1atXx7Bhw167P1NTU8yePRvXrl2Drq4uatWqhZ07d8qXbWmLJF6+YIwUKSUlBRYWFugxNw4GKnNtl0OkeB/6ByBRQT179gxxcXEoV64cjIyMtF2OouR1bLM/v5OTk2FuXrjPb8Vfw0dERERU0jHwERERESmc4q/ho/+3qK9VoYeEiYiIqPjhCB8RERGRwjHwERERUb7wPs+i976OKQMfERER5Sn7m6WePHmi5UqUJ/uYvvrtXUWN1/ARERFRnnR1dWFpaSl/X7yxsfFrv5aM8kcIgSdPniAhIQGWlpZ5fl1bUWDgIyIiojdycHAAADn0UdGwtLSUj+27xMBHREREbyRJEhwdHWFnZ4fnz59ruxxF0NfXf+cje9kY+IiIiCjfdHV131tIoaLDmzaIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI7fpVuCDF6RCANVprbLICpxVgyw0nYJRFTCcYSPiIiISOEY+IiIiIgUjoGPiIiISOEY+ApBkiRs3br1ve4zPDwckiQhKSnpve6XiIiIii8GvnwIDg6Gl5dXjvnx8fFo3rz5+y+IiIiIqAB4l24hODg4aLsEIiIiojf6IEb4hBCYM2cOypcvD5VKBU9PT/z+++8QQuDTTz+Fv78/hBAAgKSkJDg7O2P8+PHy9tu2bYOPjw+MjIxgY2ODtm3byssyMjIwZswYlC5dGiYmJqhTpw7Cw8Pl5aGhobC0tMTWrVvh5uYGIyMjNG3aFLdv35aXT5kyBefOnYMkSZAkCaGhoQByntK9cOECGjduDJVKBWtra3z11VdITU2VlwcFBeHzzz/H3Llz4ejoCGtrawwcOBDPnz+X11mzZg18fHxgZmYGBwcHdOnSBQkJCUV5uImIiKiE+SAC34QJExASEoKlS5fi4sWLGD58OLp164ZDhw5h1apVOHXqFH744QcAQL9+/WBvb4/g4GAAwF9//YW2bdsiMDAQUVFROHDgAHx8fOS2e/XqhaNHj2L9+vU4f/482rdvD39/f1y7dk1e58mTJ5g+fTpWrVqFo0ePIiUlBZ06dQIAdOzYESNHjkTVqlURHx+P+Ph4dOzYMUcfnjx5An9/f5QqVQqnT5/Gpk2bsH//fgwaNEhjvbCwMMTGxiIsLAyrVq1CaGioHCCBFwF16tSpOHfuHLZu3Yq4uDgEBQUV6Himp6cjJSVFYyIiIqKSS+undNPS0jB//nwcPHgQdevWBQCUL18eR44cwbJly7B27VosW7YM3bt3x/3797F9+3ZERUVBX18fADB9+nR06tQJU6ZMkdv09PQEAMTGxmLdunW4c+cOnJycAACjRo3C7t27ERISghkzZgAAnj9/jsWLF6NOnToAgFWrVqFKlSo4deoUateuDVNTU+jp6eV5Cve3337D06dPsXr1apiYmAAAFi9ejJYtW2L27Nmwt7cHAJQqVQqLFy+Grq4u3N3dERgYiAMHDqBv374AgN69e8ttli9fHj/88ANq166N1NRUmJqa5uuYzpw5U+N4EBERUcmm9RG+mJgYPHv2DE2bNoWpqak8rV69GrGxsQCA9u3bo23btpg5cybmzZsHNzc3efvo6Gg0adIk17bPnj0LIQTc3Nw02o6IiJDbBgA9PT2NUUF3d3dYWlri0qVL+e7HpUuX4OnpKYc9AKhfvz7UajWuXLkiz6tatSp0dXXl146OjhqnbKOiotC6dWu4uLjAzMwMfn5+AIBbt27lu5Zx48YhOTlZnrJPTxMREVHJpPURPrVaDeDFqdnSpUtrLDM0NATw4nRpZGQkdHV1NU7FAoBKpcqzbV1dXXnbl706WiZJUo7tc5v3OkKI167/8vzskcmXl2Ufg7S0NHz22Wf47LPPsGbNGtja2uLWrVto1qwZMjIy8l2LoaGhfOyIiIiItB74PDw8YGhoiFu3bsHX1zfXdUaOHAkdHR3s2rULAQEBCAwMROPGjQEANWrUwIEDB9CrV68c29WsWRNZWVlISEjAJ5988toaMjMzcebMGdSuXRsAcOXKFSQlJcHd3R0AYGBggKysrDf2Y9WqVUhLS5NH+Y4ePQodHR2NEcm8XL58Gf/99x9mzZqFsmXLAgDOnDmTr22JiIiIXkfrgc/MzAyjRo3C8OHDoVar0aBBA6SkpODYsWMwNTWFjY0NVq5ciePHj8Pb2xvffPMNevbsifPnz6NUqVKYPHkymjRpggoVKqBTp07IzMzErl27MGbMGLi5uaFr167o0aMH5s2bh5o1a+K///7DwYMHUb16dQQEBAB4Meo2ePBg/PDDD9DX18egQYPw8ccfywHQ1dUVcXFxiI6ORpkyZWBmZpZjBK1r166YPHkyevbsieDgYDx48ACDBw9G9+7d5ev33sTZ2RkGBgZYtGgR+vXrh7///htTp04t2gNOREREJY7Wr+EDgKlTp2LSpEmYOXMmqlSpgmbNmmH79u1wdXVFnz59EBwcDG9vbwDA5MmT4eTkhH79+gEA/Pz8sGnTJmzbtg1eXl5o3LgxTp48KbcdEhKCHj16YOTIkahcuTJatWqFkydPyiNoAGBsbIyxY8eiS5cuqFu3LlQqFdavXy8v/+KLL+Dv749GjRrB1tYW69aty9EHY2Nj7NmzB4mJiahVqxbatWuHJk2aYPHixfk+Dra2tggNDcWmTZvg4eGBWbNmYe7cuQU+nkREREQvk0T2A+5KqNDQUAwbNkzRX1WWkpICCwsL9JgbBwOVubbLISpxVgyw0nYJRFQMZX9+Jycnw9y8cJ/fH8QIHxERERG9Owx8RERERApX4k/plgRFOSRMRERE7wdP6RIRERFRvjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESmcnrYLoPdn8IpEGKgytV0GEb3GigFW2i6BiBSKI3xERERECsfAR0RERKRwDHxEREREClcsAp+fnx+GDRum7TI+CDwWREREVFDF4qaNLVu2QF9fX9tlEBERERVLxSLwWVm9/zvXnj9/zpBJREREilDsTum6urpi2rRp6NGjB0xNTeHi4oI///wTDx48QOvWrWFqaorq1avjzJkz8vahoaGwtLTE1q1b4ebmBiMjIzRt2hS3b9+W1wkODoaXlxdWrlyJ8uXLw9DQEEIIJCcn46uvvoKdnR3Mzc3RuHFjnDt3Tt7u3LlzaNSoEczMzGBubo6PPvpI3vfNmzfRsmVLlCpVCiYmJqhatSp27twpbxsTE4OAgACYmprC3t4e3bt3x3///ScvT0tLk/vp6OiIefPmvatDTERERApWLALfqxYsWID69esjKioKgYGB6N69O3r06IFu3brh7NmzqFixInr06AEhhLzNkydPMH36dKxatQpHjx5FSkoKOnXqpNHuP//8g40bN2Lz5s2Ijo4GAAQGBuLevXvYuXMnIiMj4e3tjSZNmiAxMREA0LVrV5QpUwanT59GZGQkvvnmG3lkcODAgUhPT8ehQ4dw4cIFzJ49G6ampgCA+Ph4+Pr6wsvLC2fOnMHu3btx//59dOjQQa5n9OjRCAsLwx9//IG9e/ciPDwckZGRbzw+6enpSElJ0ZiIiIio5CoWp3RfFRAQgK+/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SUmBhYUFesyNg4FKe6fjiUj7Vgyw0nYJRJRP2Z/fycnJhb6crliO8BERERFR/jHwERERESlcvh/LQsXfor5WWr3DmoiIiLSDI3xERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwetougN6fwSsSYaDK1HYZRKQgKwZYabsEIsoHjvARERERKRwDHxEREZHCMfARERERKRwDXxEJDQ2FpaWl/Do4OBheXl5aq4eIiIgoGwNfMfJqqCQiIiLKDwY+IiIiIoVTVOATQmDOnDkoX748VCoVPD098fvvv0MIgU8//RT+/v4QQgAAkpKS4OzsjPHjx8vbb9u2DT4+PjAyMoKNjQ3atm0rL8vIyMCYMWNQunRpmJiYoE6dOggPDy9QfSEhIahSpQqMjIzg7u6OJUuWyMtu3LgBSZKwZcsWNGrUCMbGxvD09MTx48cBAOHh4ejVqxeSk5MhSRIkSUJwcPDbHywiIiIqMRQV+CZMmICQkBAsXboUFy9exPDhw9GtWzccOnQIq1atwqlTp/DDDz8AAPr16wd7e3s5NP31119o27YtAgMDERUVhQMHDsDHx0duu1evXjh69CjWr1+P8+fPo3379vD398e1a9fyVduKFSswfvx4TJ8+HZcuXcKMGTMwceJErFq1SmO98ePHY9SoUYiOjoabmxs6d+6MzMxM1KtXD99//z3Mzc0RHx+P+Ph4jBo1Ktd9paenIyUlRWMiIiKikksxD15OS0vD/PnzcfDgQdStWxcAUL58eRw5cgTLli3D2rVrsWzZMnTv3h3379/H9u3bERUVBX19fQDA9OnT0alTJ0yZMkVu09PTEwAQGxuLdevW4c6dO3BycgIAjBo1Crt370ZISAhmzJjxxvqmTp2KefPmyaOG5cqVQ0xMDJYtW4aePXvK640aNQqBgYEAgClTpqBq1ar4559/4O7uDgsLC0iSBAcHhzz3NXPmTI1+EBERUcmmmMAXExODZ8+eoWnTphrzMzIyULNmTQBA+/bt8ccff2DmzJlYunQp3Nzc5PWio6PRt2/fXNs+e/YshBAa6wMv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WhWGhoZwdXXFvHnz3n1HiIiISFEY+IrI+vXr0aFDB6xevRo9evR44/q3b99G27ZtERAQgOjoaHz55Zf45ptvNNaJjIxEhw4d0KlTJ1y4cAHBwcGYOHEiQkND82w7PT0dKSkpGhMRERGVXAx8RWDJkiXo168f/vzzT7Ru3Tpf2yxduhTly5fHggULULlyZXTt2hVBQUEa68yfPx9NmjTBxIkT4ebmhqCgIAwaNAjfffddnm3PnDkTFhYW8lS2bNm37RoREREpAANfIW3evBnDhg3D3r170ahRo3xvd+nSJXz88ceQJEmeV7du3Rzr1K9fX2Ne/fr1ce3aNWRlZb227XHjxiE5OVmebt++ne+6iIiISHkY+ArJy8sLtra2CAkJgRACAKCj8+KwZr8GgOfPn2ts9/Ky1xFCaATC/G5naGgIc3NzjYmIiIhKLga+QqpQoQLCwsLw559/YvDgwQAAW1tbAEB8fLy83ss3cACAh4cHTpw4oTHv1dceHh44cuSIxrxjx47Bzc0Nurq6RdUFIiIiUjgGviLg5uaGsLAw+fSuSqXCxx9/jFmzZiEmJgaHDh3ChAkTNLbp168fYmNjMWLECFy5cgVr167NcTPGyJEjceDAAUydOhVXr17FqlWrsHjxYowaNeo99o6IiIiKOwa+IlK5cmUcPHgQ69atw8iRI7Fy5Uo8f/4cPj4+GDp0KKZNm6axvrOzMzZv3ozt27fD09MTP/30E2bMmKGxjre3NzZu3Ij169ejWrVqmDRpEr799tscN3cQERER5UUS+bkojIq1lJQUWFhYoMfcOBioeD0fEZUsKwZYabsEoreS/fmdnJxc6OvxOcJHREREpHAMfEREREQKp6ftAuj9WdTXio9oISIiKoE4wkdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERAqnp+0C6P0ZvCIRBqpMbZdBRERUpFYMsNJ2CR88jvARERERKRwDHxEREZHCMfARERERKRwD31sKDw+HJElISkp6p/u5ceMGJElCdHT0O90PERERKRcDXz75+flh2LBh2i6DiIiI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",
- "text/plain": [
- "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Make a bar plot showing the count of tweet sentiments\n",
+ "sns.countplot(data=tweets,\n",
+ " x='airline_sentiment',\n",
+ " color='cornflowerblue',\n",
+ " order=['positive', 'negative']);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eab45abf-adf4-4f5e-ae09-75f6c4fd50d1",
+ "metadata": {
+ "id": "eab45abf-adf4-4f5e-ae09-75f6c4fd50d1"
+ },
+ "source": [
+ "It looks like the majority of the tweets in this dataset are expressing negative sentiment!\n",
+ "\n",
+ "Let's take a look at what gets more retweeted:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, el comentario indica que se quiere calcular el promedio de retweets para cada tipo de sentimiento. Primero, tweets.groupby('airline_sentiment') agrupa los datos según la columna airline_sentiment, separando los tweets positivos de los negativos. Luego, ['retweet_count'].mean() toma la columna retweet_count de cada grupo y calcula su valor promedio. De esta manera, se obtiene el número medio de retweets para los tweets positivos y negativos, lo que ayuda a entender qué tipo de sentimiento tiende a generar más interacción.*"
+ ],
+ "metadata": {
+ "id": "tN7Gp0RwfJS-"
+ },
+ "id": "tN7Gp0RwfJS-"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "428ddde7-af73-4eb6-92c9-041a1791ca59",
+ "metadata": {
+ "id": "428ddde7-af73-4eb6-92c9-041a1791ca59",
+ "outputId": "6bcfd9f3-38cb-4234-c76b-3b36e8633a32"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "airline_sentiment\n",
+ "negative 0.093375\n",
+ "positive 0.069403\n",
+ "Name: retweet_count, dtype: float64"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Get the mean retweet count for each sentiment\n",
+ "tweets.groupby('airline_sentiment')['retweet_count'].mean()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0d31f3bc-257c-48a8-86a0-fd0d7c3e8cb3",
+ "metadata": {
+ "id": "0d31f3bc-257c-48a8-86a0-fd0d7c3e8cb3"
+ },
+ "source": [
+ "Negative tweets are clearly retweeted more often than positive ones!\n",
+ "\n",
+ "Let's see which airline receives most negative tweets:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, el comentario indica que se quiere calcular la proporción de tweets negativos por aerolínea. Primero, tweets.groupby(['airline', 'airline_sentiment']).size() cuenta cuántos tweets hay de cada sentimiento para cada aerolínea, y se divide entre tweets.groupby('airline').size(), que da el total de tweets por aerolínea; así se obtiene la proporción de cada tipo de sentimiento. Luego, proportions.unstack() reorganiza los datos para que cada sentimiento sea una columna, y sort_values('negative', ascending=False) ordena las aerolíneas de mayor a menor según la proporción de tweets negativos, mostrando cuáles tienen más críticas.*"
+ ],
+ "metadata": {
+ "id": "q2aUAry5fgUP"
+ },
+ "id": "q2aUAry5fgUP"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12aa9f2d-d655-494a-bb72-08ad973518f3",
+ "metadata": {
+ "id": "12aa9f2d-d655-494a-bb72-08ad973518f3",
+ "outputId": "613c5a79-5468-441e-d9a9-1367bfe0bf5e"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
airline_sentiment
\n",
+ "
negative
\n",
+ "
positive
\n",
+ "
\n",
+ "
\n",
+ "
airline
\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
US Airways
\n",
+ "
0.893760
\n",
+ "
0.106240
\n",
+ "
\n",
+ "
\n",
+ "
American
\n",
+ "
0.853659
\n",
+ "
0.146341
\n",
+ "
\n",
+ "
\n",
+ "
United
\n",
+ "
0.842560
\n",
+ "
0.157440
\n",
+ "
\n",
+ "
\n",
+ "
Southwest
\n",
+ "
0.675399
\n",
+ "
0.324601
\n",
+ "
\n",
+ "
\n",
+ "
Delta
\n",
+ "
0.637091
\n",
+ "
0.362909
\n",
+ "
\n",
+ "
\n",
+ "
Virgin America
\n",
+ "
0.543544
\n",
+ "
0.456456
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ "airline_sentiment negative positive\n",
+ "airline \n",
+ "US Airways 0.893760 0.106240\n",
+ "American 0.853659 0.146341\n",
+ "United 0.842560 0.157440\n",
+ "Southwest 0.675399 0.324601\n",
+ "Delta 0.637091 0.362909\n",
+ "Virgin America 0.543544 0.456456"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Get the proportion of negative tweets by airline\n",
+ "proportions = tweets.groupby(['airline', 'airline_sentiment']).size() / tweets.groupby('airline').size()\n",
+ "proportions.unstack().sort_values('negative', ascending=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7042419e-9c41-40e7-8dbf-47bd1e2ad45a",
+ "metadata": {
+ "id": "7042419e-9c41-40e7-8dbf-47bd1e2ad45a"
+ },
+ "source": [
+ "It looks like people are most dissatified with US Airways, followed by American Airline, both having over 85\\% negative tweets!\n",
+ "\n",
+ "A lot of interesting discoveries could be made if you want to explore more about the data. Now let's return to our task of sentiment analysis. Before that, we need to preprocess the text data so that they are in a standard format."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9e513930-2dc7-489c-bc5a-22eb09add5bf",
+ "metadata": {
+ "id": "9e513930-2dc7-489c-bc5a-22eb09add5bf"
+ },
+ "source": [
+ "\n",
+ "# Preprocessing\n",
+ "\n",
+ "We spent much of Part 1 learning how to preprocess data. Let's apply what we learned! Looking at some of the tweets above, we can see that while they are in pretty good shape, we can do some additional processing on them.\n",
+ "\n",
+ "In our pipeline, we'll omit the tokenization process since we will perform it in a later step."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "92a83ece-f3b2-4200-9d22-0788fbc07fa4",
+ "metadata": {
+ "id": "92a83ece-f3b2-4200-9d22-0788fbc07fa4"
+ },
+ "source": [
+ "## 🥊 Challenge 1: Apply a Text Cleaning Pipeline\n",
+ "\n",
+ "Write a function called `preprocess` that performs the following steps on a text input:\n",
+ "\n",
+ "* Step 1: Lowercase the text input.\n",
+ "* Step 2: Replace the following patterns with placeholders:\n",
+ " * URLs → ` URL `\n",
+ " * Digits → ` DIGIT `\n",
+ " * Hashtags → ` HASHTAG `\n",
+ " * Tweet handles → ` USER `\n",
+ "* Step 3: Remove extra blankspace.\n",
+ "\n",
+ "Here are some hints to guide you through this challenge:\n",
+ "\n",
+ "* For Step 1, recall from Part 1 that a string method called [`.lower()`](https://docs.python.org/3.11/library/stdtypes.html#str.lower) can be usd to convert text to lowercase.\n",
+ "* We have integrated Step 2 into a function called `placeholder`. Run the cell below to import it into your notebook, and you can use it just like any other functions.\n",
+ "* For Step 3, we have provided the regex pattern for identifying whitespace characters as well as the correct replacement for extract whitespace.\n",
+ "\n",
+ "Run your `preprocess` function on `example_tweet` (three cells below) to check if it works. If it does, apply it to the entire `text` column in the tweets dataframe."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En esta línea de código se está importando algo llamado placeholder desde un módulo llamado utils. Esto significa que dentro del archivo o paquete utils.py existe una función, clase o variable llamada placeholder, y se quiere usar en el código actual. Básicamente, permite acceder a esa funcionalidad definida en otro archivo sin tener que repetir su implementación.*"
+ ],
+ "metadata": {
+ "id": "a0AM2TmKf38o"
+ },
+ "id": "a0AM2TmKf38o"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21738b02-9ab9-4a61-b41f-ff75888aa747",
+ "metadata": {
+ "tags": [],
+ "id": "21738b02-9ab9-4a61-b41f-ff75888aa747"
+ },
+ "outputs": [],
+ "source": [
+ "from utils import placeholder"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, se observa que primero se definen dos variables: blankspace_pattern = r'\\s+', que es una expresión regular que representa uno o más espacios en blanco, y blankspace_repl = ' ', que indica que esos espacios serán reemplazados por un solo espacio. Luego se define la función preprocess(text), cuyo propósito es crear un pipeline de preprocesamiento para limpiar los datos de los tweets. Dentro de la función, los comentarios indican los pasos: primero convertir todo el texto a minúsculas, luego reemplazar ciertos patrones por placeholders y finalmente eliminar espacios en blanco extra. Al final, la función devuelve el texto limpio y estandarizado listo para análisis o procesamiento adicional.*"
+ ],
+ "metadata": {
+ "id": "CQqYp2MEf6S4"
+ },
+ "id": "CQqYp2MEf6S4"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "03569f0d-34ba-492d-aa1d-1dce9d34f792",
+ "metadata": {
+ "id": "03569f0d-34ba-492d-aa1d-1dce9d34f792"
+ },
+ "outputs": [],
+ "source": [
+ "blankspace_pattern = r'\\s+'\n",
+ "blankspace_repl = ' '\n",
+ "\n",
+ "def preprocess(text):\n",
+ " '''Create a preprocess pipeline that cleans the tweet data.'''\n",
+ "\n",
+ " # Step 1: Lowercase\n",
+ " text = ...\n",
+ "\n",
+ " # Step 2: Replace patterns with placeholders\n",
+ " text = ...\n",
+ "\n",
+ " # Step 3: Remove extra whitespace characters\n",
+ " text = ...\n",
+ "\n",
+ " return text"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8990cefd-5d04-46ba-ada2-29978c28cfe8",
+ "metadata": {
+ "id": "8990cefd-5d04-46ba-ada2-29978c28cfe8",
+ "outputId": "61e6c6b8-05df-4acc-db84-b4b3151615ef"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "lol @justinbeiber and @BillGates are like soo 2000 #yesterday #amiright saw it on https://twitter.com #yolo\n",
+ "==================================================\n",
+ "lol USER and USER are like soo DIGIT HASHTAG HASHTAG saw it on URL HASHTAG\n"
+ ]
+ }
+ ],
+ "source": [
+ "example_tweet = 'lol @justinbeiber and @BillGates are like soo 2000 #yesterday #amiright saw it on https://twitter.com #yolo'\n",
+ "\n",
+ "# Print the example tweet\n",
+ "print(example_tweet)\n",
+ "print(f\"{'='*50}\")\n",
+ "\n",
+ "# Print the preprocessed tweet\n",
+ "print(preprocess(example_tweet))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En este bloque de código, se aplica la función de limpieza preprocess a cada tweet de la columna text usando .apply(), y el resultado se guarda en una nueva columna llamada text_processed. Esto significa que cada tweet se transforma según el pipeline de preprocesamiento definido antes (por ejemplo, pasando a minúsculas, reemplazando patrones y eliminando espacios extras). Finalmente, tweets['text_processed'].head() muestra los primeros cinco tweets ya procesados, permitiendo revisar rápidamente cómo quedó el texto limpio.*"
+ ],
+ "metadata": {
+ "id": "Hr0Bxs3JgPqi"
+ },
+ "id": "Hr0Bxs3JgPqi"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a5f7bb6a-f064-48cc-b650-12c4ef2fbb88",
+ "metadata": {
+ "scrolled": true,
+ "id": "a5f7bb6a-f064-48cc-b650-12c4ef2fbb88",
+ "outputId": "ca00ca39-d1b8-4570-bb16-cbe8c16cae2c"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0 USER plus you've added commercials to the expe...\n",
+ "1 USER it's really aggressive to blast obnoxious...\n",
+ "2 USER and it's a really big bad thing about it\n",
+ "3 USER seriously would pay $ DIGIT a flight for ...\n",
+ "4 USER yes, nearly every time i fly vx this “ear...\n",
+ "Name: text_processed, dtype: object"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Apply the function to the text column and assign the preprocessed tweets to a new column\n",
+ "tweets['text_processed'] = tweets['text'].apply(lambda x: preprocess(x))\n",
+ "tweets['text_processed'].head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1576acc6-b305-492a-8fde-65b343cb779c",
+ "metadata": {
+ "id": "1576acc6-b305-492a-8fde-65b343cb779c"
+ },
+ "source": [
+ "Congratualtions! Preprocessing is done. Let's dive into the bag-of-words!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "53282330-54da-4e1c-bfe5-e77cb8fa3add",
+ "metadata": {
+ "id": "53282330-54da-4e1c-bfe5-e77cb8fa3add"
+ },
+ "source": [
+ "\n",
+ "# The Bag-of-Words Representation\n",
+ "\n",
+ "The idea of bag-of-words (BoW), as the name suggests, is quite intuitive: we take a document and toss it in a bag. The action of \"throwing\" the document in a bag disregards the relative position between words, so what is \"in the bag\" is essentially \"an unsorted set of words\" [(Jurafsky & Martin, 2024)](https://web.stanford.edu/~jurafsky/slp3/ed3book.pdf). In return, we have a list of unique words and the frequency of each of them.\n",
+ "\n",
+ "For example, as shown in the following illustration, the word \"coffee\" appears twice.\n",
+ "\n",
+ "\n",
+ "\n",
+ "With a bag-of-words representation, we make heavy use of word frequency but not too much of word order.\n",
+ "\n",
+ "In the context of sentiment analysis, the sentiment of a tweet is conveyed more strongly by specific words. For example, if a tweet contains the word \"happy,\" it likely conveys positive sentiment, but not always (e.g., \"not happy\" denotes the opposite sentiment). When these words come up more often, they'll probably more strongly convey the sentiment."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b9d9bdbd-406d-469b-a8f6-41d1b3687c37",
+ "metadata": {
+ "id": "b9d9bdbd-406d-469b-a8f6-41d1b3687c37"
+ },
+ "source": [
+ "## Document Term Matrix\n",
+ "\n",
+ "Now let's implement the idea of bag-of-words. Before we dive deeper, let's step back for a moment. In practice, text analysis often involves handling many documents; from now on, we use the term **document** to represent a piece of text on which we perform analysis. It could be a phrase, a sentence, a tweet, or any other text—as long as it can be represented by a string, the length dosen't really matter.\n",
+ "\n",
+ "Imagine we have four documents (i.e., the four phrases shown above), and we toss them all in the bag. Instead of a word-frequency list, we'd expect a document-term matrix (DTM) in return. In a DTM, the word list is the **vocabulary** (V) that holds all unique words occur across the documents. For each **document** (D), we count the number of occurence of each word in the vocabulary, and then plug the number into the matrix. In other words, the DTM we will construct is a $D \\times V$ matrix, where each row corresponds to a document, and each column corresponds to a token (or \"term\").\n",
+ "\n",
+ "The unique tokens in this set of documents, arranged in alphabetical order, form the columns. For each document, we mark the occurence of each word present in the document. The numerical representation for each document is a row in the matrix. For example, the first document, \"the coffee roaster,\" has the numerical representation $[0, 1, 0, 0, 0, 1, 1, 0]$.\n",
+ "\n",
+ "Note that the left index column now displays these documents as text, but typically we would just assign an index to each of them.\n",
+ "\n",
+ "$$\n",
+ "\\begin{array}{c|cccccccccccc}\n",
+ " & \\text{americano} & \\text{coffee} & \\text{iced} & \\text{light} & \\text{roast} & \\text{roaster} & \\text{the} & \\text{time} \\\\\\hline\n",
+ "\\text{the coffee roaster} &0 &1\t&0\t&0\t&0\t&1\t&1\t&0 \\\\\n",
+ "\\text{light roast} &0 &0\t&0\t&1\t&1\t&0\t&0\t&0 \\\\\n",
+ "\\text{iced americano} &1 &0\t&1\t&0\t&0\t&0\t&0\t&0 \\\\\n",
+ "\\text{coffee time} &0 &1\t&0\t&0\t&0\t&0\t&0\t&1 \\\\\n",
+ "\\end{array}\n",
+ "$$\n",
+ "\n",
+ "To create a DTM, we will use `CountVectorizer` from the package `sklearn`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*En esta línea de código se está importando CountVectorizer desde el módulo sklearn.feature_extraction.text de la librería scikit-learn. CountVectorizer es una herramienta que convierte texto en números, creando una matriz donde cada fila representa un documento (por ejemplo, un tweet) y cada columna representa una palabra, con valores que indican cuántas veces aparece cada palabra. Esto permite que los datos de texto puedan ser usados por modelos de machine learning.*"
+ ],
+ "metadata": {
+ "id": "A1t6-BDPgfmH"
+ },
+ "id": "A1t6-BDPgfmH"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cd2adf56-ba93-459d-8cfa-16ce8dc9284b",
+ "metadata": {
+ "id": "cd2adf56-ba93-459d-8cfa-16ce8dc9284b"
+ },
+ "outputs": [],
+ "source": [
+ "from sklearn.feature_extraction.text import CountVectorizer"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4989781d-6b40-417a-be70-eeba05cd8a50",
+ "metadata": {
+ "id": "4989781d-6b40-417a-be70-eeba05cd8a50"
+ },
+ "source": [
+ "The following illustration depicts the three-step workflow of creating a DTM with `CountVectorizr`.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Let's walk through these steps with the toy example shown above."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "34174034-46b9-43e2-a511-5972d378cb00",
+ "metadata": {
+ "id": "34174034-46b9-43e2-a511-5972d378cb00"
+ },
+ "source": [
+ "### A Toy Example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4da2bd3d-0460-4b5f-9b9e-02940db0d7ca",
+ "metadata": {
+ "id": "4da2bd3d-0460-4b5f-9b9e-02940db0d7ca"
+ },
+ "outputs": [],
+ "source": [
+ "# A toy example containing four documents\n",
+ "test = ['the coffee roaster',\n",
+ " 'light roast',\n",
+ " 'iced americano',\n",
+ " 'coffee time']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dff7c1d3-fcee-4e20-b9a7-17306ebd5fc2",
+ "metadata": {
+ "id": "dff7c1d3-fcee-4e20-b9a7-17306ebd5fc2"
+ },
+ "source": [
+ "The first step is to initialize a `CountVectorizer` object. Within the round paratheses, we can specify parameter settings if desired. Let's take a look at the [documentation](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) and see what options are available. \n",
+ "\n",
+ "For now we can just leave it blank to use the default settings."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9de3fe6a-9abf-4e11-aad1-e54c891567bb",
+ "metadata": {
+ "id": "9de3fe6a-9abf-4e11-aad1-e54c891567bb"
+ },
+ "outputs": [],
+ "source": [
+ "# Create a CountVectorizer object\n",
+ "vectorizer = CountVectorizer()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1b5a7d0d-0bfc-4fb9-8e5f-e91e39797fb5",
+ "metadata": {
+ "id": "1b5a7d0d-0bfc-4fb9-8e5f-e91e39797fb5"
+ },
+ "source": [
+ "The second step is to `fit` this `CountVectorizer` object to the data, which means creating a vocabulary of tokens from the set of documents. Thirdly, we `transform` our data according to the \"fitted\" `CountVectorizer` object, which means taking each of the document and counting the occurrences of tokens according to the vocabulary established during the \"fitting\" step.\n",
+ "\n",
+ "It may sound a bit complex but steps 2 and 3 can be done in one swoop using a `fit_transform` function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "da1bbad4-bb1a-4b92-9096-6e17558b4a42",
+ "metadata": {
+ "id": "da1bbad4-bb1a-4b92-9096-6e17558b4a42"
+ },
+ "outputs": [],
+ "source": [
+ "# Fit and transform to create a DTM\n",
+ "test_count = vectorizer.fit_transform(test)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "324d3b65-4e98-48bf-87d2-399457f4939c",
+ "metadata": {
+ "id": "324d3b65-4e98-48bf-87d2-399457f4939c"
+ },
+ "source": [
+ "The return of `fit_transform` is supposed to be the DTM.\n",
+ "\n",
+ "Let's take a look at it!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cb044001-8eb2-4489-b025-2d8e2d4bfee2",
+ "metadata": {
+ "id": "cb044001-8eb2-4489-b025-2d8e2d4bfee2",
+ "outputId": "db26bb61-3db0-4a87-c0bf-dec1a19391f6"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "<4x8 sparse matrix of type ''\n",
+ "\twith 9 stored elements in Compressed Sparse Row format>"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "test_count"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f9817b09-a806-42c4-9436-822cc27a38b9",
+ "metadata": {
+ "id": "f9817b09-a806-42c4-9436-822cc27a38b9"
+ },
+ "source": [
+ "Apparently we've got a \"sparse matrix\"—a matrix that contains a lot of zeros. This makes sense. For each document, there are words that don't occur at all, and these are counted as zero in the DTM. This sparse matrix is stored in a \"Compressed Sparse Row\" format, a memory-saving format designed for handling sparse matrices.\n",
+ "\n",
+ "Let's convert it to a dense matrix, where those zeros are probably represented, as in a numpy array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bb03a238-87d8-40c9-b20e-66e7c9b6576b",
+ "metadata": {
+ "id": "bb03a238-87d8-40c9-b20e-66e7c9b6576b",
+ "outputId": "97a84c1a-a0c4-4237-845f-a48ddf5d4181"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "matrix([[0, 1, 0, 0, 0, 1, 1, 0],\n",
+ " [0, 0, 0, 1, 1, 0, 0, 0],\n",
+ " [1, 0, 1, 0, 0, 0, 0, 0],\n",
+ " [0, 1, 0, 0, 0, 0, 0, 1]])"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Convert DTM to a dense matrix\n",
+ "test_count.todense()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "28b58a63-d7f6-4b9f-aadf-4d4fc7341336",
+ "metadata": {
+ "id": "28b58a63-d7f6-4b9f-aadf-4d4fc7341336"
+ },
+ "source": [
+ "So this is our DTM! The matrix is the same as shown above. To make it more reader-friendly, let's convert it to a dataframe. The column names should be tokens in the vocabulary, which we can access with the `get_feature_names_out` function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "714de5d3-e37d-4a19-9ade-3c6629e38d4e",
+ "metadata": {
+ "id": "714de5d3-e37d-4a19-9ade-3c6629e38d4e",
+ "outputId": "fd976dd8-e521-4be1-d543-192e13845b1d"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array(['americano', 'coffee', 'iced', 'light', 'roast', 'roaster', 'the',\n",
+ " 'time'], dtype=object)"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Retrieve the vocabulary\n",
+ "vectorizer.get_feature_names_out()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6a7729a2-ca2e-4de7-8795-74dfedb7a4d5",
+ "metadata": {
+ "id": "6a7729a2-ca2e-4de7-8795-74dfedb7a4d5"
+ },
+ "outputs": [],
+ "source": [
+ "# Create a DTM dataframe\n",
+ "test_dtm = pd.DataFrame(data=test_count.todense(),\n",
+ " columns=vectorizer.get_feature_names_out())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "781da407-f394-40f2-9d45-1fac39f02047",
+ "metadata": {
+ "id": "781da407-f394-40f2-9d45-1fac39f02047"
+ },
+ "source": [
+ "Here it is! The DTM of our toy data is now a dataframe. The index of `test_dtm` corresponds to the position of each document in the `test` list."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e41dd243-cd2e-43c3-80f8-5eaab6e64210",
+ "metadata": {
+ "id": "e41dd243-cd2e-43c3-80f8-5eaab6e64210",
+ "outputId": "ddb94d16-441b-4ebe-dd45-2db08cf39ac3"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
americano
\n",
+ "
coffee
\n",
+ "
iced
\n",
+ "
light
\n",
+ "
roast
\n",
+ "
roaster
\n",
+ "
the
\n",
+ "
time
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
0
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
1
\n",
+ "
1
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
1
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
1
\n",
+ "
0
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
0
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
1
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " americano coffee iced light roast roaster the time\n",
+ "0 0 1 0 0 0 1 1 0\n",
+ "1 0 0 0 1 1 0 0 0\n",
+ "2 1 0 1 0 0 0 0 0\n",
+ "3 0 1 0 0 0 0 0 1"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "test_dtm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d59a03b4-94fa-4fe7-8f5d-7280e31b9bc4",
+ "metadata": {
+ "id": "d59a03b4-94fa-4fe7-8f5d-7280e31b9bc4"
+ },
+ "source": [
+ "Hopefully this toy example provides a clear walkthrough of creating a DTM.\n",
+ "\n",
+ "Now it's time for our tweets data!\n",
+ "\n",
+ "### DTM for Tweets\n",
+ "\n",
+ "We'll begin by initializing a `CountVectorizer` object. In the following cell, we have included a few parameters that people often adjust. These parameters are currently set to their default values.\n",
+ "\n",
+ "When we construct a DTM, the default is to lowercase the input text. If nothing is provided for `stop_words`, the default is to keep them. The next three parameters are used to control the size of the vocabulary, which we'll return to in a minute."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ " *La variable vectorizer almacena este objeto, que se configura para convertir texto en una matriz de conteos de palabras. Los parámetros especificados significan lo siguiente: lowercase=True convierte todo el texto a minúsculas, stop_words=None no elimina palabras comunes, min_df=1 incluye palabras que aparecen al menos en un documento, max_df=1.0 permite incluir palabras que aparecen en todos los documentos, y max_features=None indica que no hay un límite en el número de palabras a considerar. Este objeto servirá para transformar los tweets en datos numéricos que los modelos de machine learning puedan procesar.*"
+ ],
+ "metadata": {
+ "id": "J-byaxyLhCtS"
+ },
+ "id": "J-byaxyLhCtS"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "783e44a4-4a22-4290-b222-282b02c080dc",
+ "metadata": {
+ "id": "783e44a4-4a22-4290-b222-282b02c080dc"
+ },
+ "outputs": [],
+ "source": [
+ "# Create a CountVectorizer object\n",
+ "vectorizer = CountVectorizer(lowercase=True,\n",
+ " stop_words=None,\n",
+ " min_df=1,\n",
+ " max_df=1.0,\n",
+ " max_features=None)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*La línea counts = vectorizer.fit_transform(tweets['text_processed']) hace dos cosas: primero, fit aprende el vocabulario de los tweets procesados, y luego transform convierte cada tweet en un vector de conteos de palabras según ese vocabulario.*"
+ ],
+ "metadata": {
+ "id": "BcEdpBovhST8"
+ },
+ "id": "BcEdpBovhST8"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f85e76ea-bc54-4775-bcda-432a03d2c96f",
+ "metadata": {
+ "scrolled": true,
+ "id": "f85e76ea-bc54-4775-bcda-432a03d2c96f",
+ "outputId": "761cbe0a-1936-4e19-a90f-7ba9ed8c8ec2"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "<11541x8751 sparse matrix of type ''\n",
+ "\twith 191139 stored elements in Compressed Sparse Row format>"
+ ]
+ },
+ "execution_count": 23,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Fit and transform to create DTM\n",
+ "counts = vectorizer.fit_transform(tweets['text_processed'])\n",
+ "counts"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Esta línea de código convierte la matriz dispersa counts en una matriz densa de tipo numpy.ndarray. Al usar counts.todense(), se transforma la representación comprimida de los conteos de palabras en una matriz completa con todos los ceros y valores explícitos, y luego np.array(...) la convierte en un arreglo de NumPy. Esto permite manipular los datos de manera más directa para análisis o procesamiento adicional.*"
+ ],
+ "metadata": {
+ "id": "r3b5VJBvhiNA"
+ },
+ "id": "r3b5VJBvhiNA"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "87119057-c78c-4eb2-a9d6-3e9f44e4c22b",
+ "metadata": {
+ "id": "87119057-c78c-4eb2-a9d6-3e9f44e4c22b",
+ "outputId": "d313cfd4-66c0-4e96-d955-083f73b78567"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[0, 0, 0, ..., 0, 0, 0],\n",
+ " [0, 0, 0, ..., 0, 0, 0],\n",
+ " [0, 0, 0, ..., 0, 0, 0],\n",
+ " ...,\n",
+ " [0, 0, 0, ..., 0, 0, 0],\n",
+ " [0, 0, 0, ..., 0, 0, 0],\n",
+ " [0, 0, 0, ..., 0, 0, 0]])"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Do not run if you have limited memory - this includes DataHub and Binder\n",
+ "np.array(counts.todense())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "99322b85-1a15-46a5-bb80-bb5eaa6eeb7b",
+ "metadata": {
+ "id": "99322b85-1a15-46a5-bb80-bb5eaa6eeb7b"
+ },
+ "outputs": [],
+ "source": [
+ "# Extract tokens\n",
+ "tokens = vectorizer.get_feature_names_out()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "43620587-3795-4434-8f1f-145c81b93706",
+ "metadata": {
+ "id": "43620587-3795-4434-8f1f-145c81b93706",
+ "outputId": "a55de36b-27e8-45a3-e52b-941b008b0ca8"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(11541, 8751)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Create DTM\n",
+ "first_dtm = pd.DataFrame(data=counts.todense(),\n",
+ " index=tweets.index,\n",
+ " columns=tokens)\n",
+ "\n",
+ "# Print the shape of DTM\n",
+ "print(first_dtm.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2dd257d5-4244-436c-afe7-5688232caf8f",
+ "metadata": {
+ "id": "2dd257d5-4244-436c-afe7-5688232caf8f"
+ },
+ "source": [
+ "If we leave the `CountVectorizer` to the default setting, the vocabulary size of the tweet data is 8751."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bb3604ec-d909-4238-9a3f-67e7d4ae2ac5",
+ "metadata": {
+ "id": "bb3604ec-d909-4238-9a3f-67e7d4ae2ac5",
+ "outputId": "71611a7b-3fb7-4019-977a-b0c8464143fc"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ " token number\n",
+ "3127 lt 6\n",
+ "918 worst 6\n",
+ "10572 to 5\n",
+ "8148 the 5\n",
+ "10742 to 5\n",
+ "152 to 5\n",
+ "5005 to 5\n",
+ "10923 the 5\n",
+ "7750 to 5\n",
+ "355 to 5"
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "counts = pd.DataFrame()\n",
+ "\n",
+ "# Retrieve the index of the tweet where a token appears most frequently\n",
+ "counts['token'] = first_dtm.idxmax(axis=1)\n",
+ "\n",
+ "# Retrieve the number of occurrence\n",
+ "counts['number'] = first_dtm.max(axis=1)\n",
+ "\n",
+ "# Filter out placeholders\n",
+ "counts[(counts['token']!='digit')\n",
+ " & (counts['token']!='hashtag')\n",
+ " & (counts['token']!='user')].sort_values('number', ascending=False).head(10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7cdac4ef-6b9d-4aad-9b24-c70f6c2eb8f0",
+ "metadata": {
+ "id": "7cdac4ef-6b9d-4aad-9b24-c70f6c2eb8f0"
+ },
+ "source": [
+ "It looks like among all tweets, at most a token appears six times, and it is either the word \"It\" or the word \"worst.\"\n",
+ "\n",
+ "Let's go back to our tweets dataframe and locate the 918th tweet."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5e7cacd8-1fb3-4f0d-a744-4ee0994a089f",
+ "metadata": {
+ "id": "5e7cacd8-1fb3-4f0d-a744-4ee0994a089f",
+ "outputId": "bc9ca1e9-8f52-4958-a3e9-3cb3e992201c"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\"@united is the worst. Worst reservation policies. Worst costumer service. Worst worst worst. Congrats, @Delta you're not that bad!\""
+ ]
+ },
+ "execution_count": 31,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Retrieve 918th tweet: \"worst\"\n",
+ "tweets.iloc[918]['text']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3dba8e37-4880-4565-b6fc-7e7c96958f0f",
+ "metadata": {
+ "id": "3dba8e37-4880-4565-b6fc-7e7c96958f0f"
+ },
+ "source": [
+ "## Customize the `CountVectorizer`\n",
+ "\n",
+ "So far we've always used the default parameter setting to create our DTMs, but in many cases we may want to customize the `CountVectorizer` object. The purpose of doing so is to further filter out unnecessary tokens. In the example below, we tweak the following parameters:\n",
+ "\n",
+ "- `stop_words = 'english'`: ignore English stop words\n",
+ "- `min_df = 2`: ignore words that don't occur at least twice\n",
+ "- `max_df = 0.95`: ignore words if they appear in more than 95\\% of the documents\n",
+ "\n",
+ "🔔 **Question**: Let's pause for a minute to discuss whether it sounds reasonable to set these parameters! What do you think?\n",
+ "\n",
+ "Oftentimes, we are not interested in words whose frequencies are either too low or too high, so we use `min_df` and `max_df` to filter them out. Alternatively, we can define our vocabulary size as $N$ by setting `max_features`. In other words, we tell `CountVectorizer` to only consider the top $N$ most frequent tokens when constructing the DTM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "37a0a93e-9dd8-43dc-a82c-06a24bf02bc9",
+ "metadata": {
+ "id": "37a0a93e-9dd8-43dc-a82c-06a24bf02bc9"
+ },
+ "outputs": [],
+ "source": [
+ "# Customize the parameter setting\n",
+ "vectorizer = CountVectorizer(lowercase=True,\n",
+ " stop_words='english',\n",
+ " min_df=2,\n",
+ " max_df=0.95,\n",
+ " max_features=None)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b53e5ecf-7be3-4915-9d11-fd3edb913400",
+ "metadata": {
+ "id": "b53e5ecf-7be3-4915-9d11-fd3edb913400"
+ },
+ "outputs": [],
+ "source": [
+ "# Fit, transform, and get tokens\n",
+ "counts = vectorizer.fit_transform(tweets['text_processed'])\n",
+ "tokens = vectorizer.get_feature_names_out()\n",
+ "\n",
+ "# Create the second DTM\n",
+ "second_dtm = pd.DataFrame(data=counts.todense(),\n",
+ " index=tweets.index,\n",
+ " columns=tokens)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6d2e66bc-2eaa-4642-8848-74459948084b",
+ "metadata": {
+ "id": "6d2e66bc-2eaa-4642-8848-74459948084b"
+ },
+ "source": [
+ "Our second DTM has a substantially smaller vocabulary compared to the first one."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "570fb598-fa81-4111-9e36-7172d8034713",
+ "metadata": {
+ "id": "570fb598-fa81-4111-9e36-7172d8034713",
+ "outputId": "4194f1fd-f2d4-4037-8e74-d0e9cc0a12da"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(11541, 8751)\n",
+ "(11541, 4471)\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(first_dtm.shape)\n",
+ "print(second_dtm.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d8deabb2-20eb-4047-b592-48cb1564fd2a",
+ "metadata": {
+ "id": "d8deabb2-20eb-4047-b592-48cb1564fd2a",
+ "outputId": "da080b87-ec96-4e49-9a47-d1c63a3e7635"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
aa
\n",
+ "
aadv
\n",
+ "
aadvantage
\n",
+ "
aal
\n",
+ "
abandoned
\n",
+ "
abc
\n",
+ "
ability
\n",
+ "
able
\n",
+ "
aboard
\n",
+ "
abq
\n",
+ "
...
\n",
+ "
yummy
\n",
+ "
yup
\n",
+ "
yvonne
\n",
+ "
yvr
\n",
+ "
yyj
\n",
+ "
yyz
\n",
+ "
zero
\n",
+ "
zone
\n",
+ "
zoom
\n",
+ "
zurich
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5 rows × 4471 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " aa aadv aadvantage aal abandoned abc ability able aboard abq ... \\\n",
+ "0 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "1 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "2 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "3 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "4 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "\n",
+ " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
+ "0 0 0 0 0 0 0 0 0 0 0 \n",
+ "1 0 0 0 0 0 0 0 0 0 0 \n",
+ "2 0 0 0 0 0 0 0 0 0 0 \n",
+ "3 0 0 0 0 0 0 0 0 0 0 \n",
+ "4 0 0 0 0 0 0 0 0 0 0 \n",
+ "\n",
+ "[5 rows x 4471 columns]"
+ ]
+ },
+ "execution_count": 35,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "second_dtm.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "998fe2c3-ec90-4027-8c7f-417327a33a27",
+ "metadata": {
+ "id": "998fe2c3-ec90-4027-8c7f-417327a33a27"
+ },
+ "source": [
+ "The most frequent token list now includes words that make more sense to us, such as \"cancelled\" and \"service.\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ffa7bf4e-640b-49bc-b64b-721140f67f76",
+ "metadata": {
+ "id": "ffa7bf4e-640b-49bc-b64b-721140f67f76",
+ "outputId": "99601ba3-8894-465d-bd35-4d73897933bd"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "digit 6927\n",
+ "flight 3320\n",
+ "hashtag 2633\n",
+ "cancelled 956\n",
+ "thanks 921\n",
+ "service 910\n",
+ "just 801\n",
+ "customer 726\n",
+ "time 695\n",
+ "help 687\n",
+ "dtype: int64"
+ ]
+ },
+ "execution_count": 36,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "second_dtm.sum().sort_values(ascending=False).head(10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3e8b5145-d505-4e36-9a39-a40d25d8ec6f",
+ "metadata": {
+ "id": "3e8b5145-d505-4e36-9a39-a40d25d8ec6f"
+ },
+ "source": [
+ "## 🥊 Challenge 2: Lemmatize the Text Input\n",
+ "\n",
+ "Recall from Part 1 that we introduced using `spaCy` to perform lemmatization, i.e., to \"recover\" the base form of a word. This process will reduce vocabulary size by keeping word variations minimal—a smaller vocabularly may help improve model performance in sentiment classification.\n",
+ "\n",
+ "Now let's implement lemmatization on our tweet data and use the lemmatized text to create a third DTM.\n",
+ "\n",
+ "Complete the function `lemmatize_text`. It requires a text input and returns the lemmas of all tokens.\n",
+ "\n",
+ "Here are some hints to guide you through this challenge:\n",
+ "\n",
+ "- Step 1: initialize a list to hold lemmas\n",
+ "- Step 2: apply the `nlp` pipeline to the input text\n",
+ "- Step 3: iterate over tokens in the processed text and retrieve the lemma of the token\n",
+ " - HINT: lemmatization is one of the linguistic annotations that the `nlp` pipeline automatically does for us. We can use `token.lemma_` to access the annotation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*nlp = spacy.load('en_core_web_sm') carga un modelo de idioma en inglés pequeño llamado en_core_web_sm, que permite a spaCy reconocer palabras, oraciones, partes de la oración y otras características del texto. El objeto nlp resultante se puede usar para analizar y procesar los tweets de manera avanzada.*"
+ ],
+ "metadata": {
+ "id": "oc1FLlG7h2Oi"
+ },
+ "id": "oc1FLlG7h2Oi"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "da610560-62c3-48ab-a1b2-25e0b589bc61",
+ "metadata": {
+ "id": "da610560-62c3-48ab-a1b2-25e0b589bc61"
+ },
+ "outputs": [],
+ "source": [
+ "# Import spaCy\n",
+ "import spacy\n",
+ "nlp = spacy.load('en_core_web_sm')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*La función lemmatize_text(text) tiene como objetivo transformar cada palabra en su forma base o lema. Primero, se inicializa una lista vacía llamada lemma para guardar los lemas de cada palabra. Luego, se aplica el pipeline de spaCy al texto de entrada (doc = nlp(text)) para analizarlo. Después, se recorre cada token en doc y se extrae su lema, agregándolo a la lista. Finalmente, se unen todos los lemas en un solo string con ' '.join(lemma) y se devuelve ese texto lematizado listo para análisis posterior.*"
+ ],
+ "metadata": {
+ "id": "nYmmg3bYiGan"
+ },
+ "id": "nYmmg3bYiGan"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "98ead266-30f3-48ad-bc51-c1685487f000",
+ "metadata": {
+ "scrolled": true,
+ "id": "98ead266-30f3-48ad-bc51-c1685487f000"
+ },
+ "outputs": [],
+ "source": [
+ "# Create a function to lemmatize text\n",
+ "def lemmatize_text(text):\n",
+ " '''Lemmatize the text input with spaCy annotations.'''\n",
+ "\n",
+ " # Step 1: Initialize an empty list to hold lemmas\n",
+ " lemma = ...\n",
+ "\n",
+ " # Step 2: Apply the nlp pipeline to input text\n",
+ " doc = ...\n",
+ "\n",
+ " # Step 3: Iterate over tokens in the text to get the token lemma\n",
+ " for token in doc:\n",
+ " lemma.append(...)\n",
+ "\n",
+ " # Step 4: Join lemmas together into a single string\n",
+ " text_lemma = ' '.join(lemma)\n",
+ "\n",
+ " return text_lemma"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cf36aab6-35dd-42a2-9b38-b7c432f021c6",
+ "metadata": {
+ "id": "cf36aab6-35dd-42a2-9b38-b7c432f021c6"
+ },
+ "source": [
+ "Let's apply the function to the following example tweet first!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Este bloque de código sirve para mostrar cómo queda un tweet después del preprocesamiento y la lematización. Primero se imprime el texto limpio del tweet, luego se coloca una línea de separación para que la salida sea más clara, y finalmente se muestra el mismo texto tras aplicarle la función de lematización, que convierte cada palabra a su forma base. Esto permite comparar de manera sencilla el texto antes y después de la lematización.*"
+ ],
+ "metadata": {
+ "id": "JyMbnArsiY0a"
+ },
+ "id": "JyMbnArsiY0a"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "742e82bb-5c42-4fa8-9101-5a0ea908db25",
+ "metadata": {
+ "id": "742e82bb-5c42-4fa8-9101-5a0ea908db25",
+ "outputId": "25800351-d172-47ba-9ed7-dbd8491ac726"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "USER wow this just blew my mind\n",
+ "==================================================\n",
+ "USER wow this just blow my mind\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Apply the function to an example tweet\n",
+ "print(tweets.iloc[33][\"text_processed\"])\n",
+ "print(f\"{'='*50}\")\n",
+ "print(lemmatize_text(tweets.iloc[33]['text_processed']))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bbeda987-dc32-4979-b158-c24be7d1a420",
+ "metadata": {
+ "id": "bbeda987-dc32-4979-b158-c24be7d1a420"
+ },
+ "source": [
+ "And then let's lemmatize the tweet data and save the output to a new column `text_lemmatized`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1ac128d2-1be5-4ef5-bb50-5b8d44ef8ee9",
+ "metadata": {
+ "id": "1ac128d2-1be5-4ef5-bb50-5b8d44ef8ee9"
+ },
+ "outputs": [],
+ "source": [
+ "# This may take a while!\n",
+ "tweets['text_lemmatized'] = tweets['text_processed'].apply(lambda x: lemmatize_text(x))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2c02aad6-4e71-4afc-80cf-31d4f39498b2",
+ "metadata": {
+ "id": "2c02aad6-4e71-4afc-80cf-31d4f39498b2"
+ },
+ "source": [
+ "Now with the `text_lemmatized` column, let's create a third DTM. The parameter setting is the same as the second DTM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5f49d790-3c9d-4dc1-a5c9-72c306630412",
+ "metadata": {
+ "id": "5f49d790-3c9d-4dc1-a5c9-72c306630412",
+ "outputId": "af1310d9-518d-4815-d4c9-b2f775786583"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
aa
\n",
+ "
aadv
\n",
+ "
aadvantage
\n",
+ "
aal
\n",
+ "
abandon
\n",
+ "
abc
\n",
+ "
ability
\n",
+ "
able
\n",
+ "
aboard
\n",
+ "
abq
\n",
+ "
...
\n",
+ "
yummy
\n",
+ "
yup
\n",
+ "
yvonne
\n",
+ "
yvr
\n",
+ "
yyj
\n",
+ "
yyz
\n",
+ "
zero
\n",
+ "
zone
\n",
+ "
zoom
\n",
+ "
zurich
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
...
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
0
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5 rows × 3553 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " aa aadv aadvantage aal abandon abc ability able aboard abq ... \\\n",
+ "0 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "1 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "2 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "3 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "4 0 0 0 0 0 0 0 0 0 0 ... \n",
+ "\n",
+ " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
+ "0 0 0 0 0 0 0 0 0 0 0 \n",
+ "1 0 0 0 0 0 0 0 0 0 0 \n",
+ "2 0 0 0 0 0 0 0 0 0 0 \n",
+ "3 0 0 0 0 0 0 0 0 0 0 \n",
+ "4 0 0 0 0 0 0 0 0 0 0 \n",
+ "\n",
+ "[5 rows x 3553 columns]"
+ ]
+ },
+ "execution_count": 41,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Create the vectorizer (the same param setting as previous)\n",
+ "vectorizer = CountVectorizer(lowercase=True,\n",
+ " stop_words='english',\n",
+ " min_df=2,\n",
+ " max_df=0.95,\n",
+ " max_features=None)\n",
+ "\n",
+ "# Fit, transform, and get tokens\n",
+ "counts = vectorizer.fit_transform(tweets['text_lemmatized'])\n",
+ "tokens = vectorizer.get_feature_names_out()\n",
+ "\n",
+ "# Create the third DTM\n",
+ "third_dtm = pd.DataFrame(data=counts.todense(),\n",
+ " index=tweets.index,\n",
+ " columns=tokens)\n",
+ "third_dtm.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9859eb04-dbd2-4fa0-9798-65ed7496c297",
+ "metadata": {
+ "id": "9859eb04-dbd2-4fa0-9798-65ed7496c297",
+ "outputId": "02b8a80e-93ed-4159-8f02-d20f3357f614"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(11541, 8751)\n",
+ "(11541, 4471)\n",
+ "(11541, 3553)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Print the shapes of three DTMs\n",
+ "print(first_dtm.shape)\n",
+ "print(second_dtm.shape)\n",
+ "print(third_dtm.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fa94c8ac-e4f4-4b76-afdb-1d4af54a3eee",
+ "metadata": {
+ "id": "fa94c8ac-e4f4-4b76-afdb-1d4af54a3eee"
+ },
+ "source": [
+ "Let's print the top 10 most frequent tokens as usual. These tokens are now lemmas and their counts also change after lemmatization."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5745ca29-97ed-4fe1-81db-7e402c8da674",
+ "metadata": {
+ "id": "5745ca29-97ed-4fe1-81db-7e402c8da674",
+ "outputId": "2a9f8f6e-4543-4bcb-ab88-bc509010acf0"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "digit 6927\n",
+ "flight 4043\n",
+ "hashtag 2633\n",
+ "thank 1455\n",
+ "hour 1134\n",
+ "cancel 948\n",
+ "delay 937\n",
+ "service 937\n",
+ "customer 902\n",
+ "time 856\n",
+ "dtype: int64"
+ ]
+ },
+ "execution_count": 43,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Get the most frequent tokens in the third DTM\n",
+ "third_dtm.sum().sort_values(ascending=False).head(10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "16c63e6a-50c3-448a-9a56-a1d193cd6680",
+ "metadata": {
+ "id": "16c63e6a-50c3-448a-9a56-a1d193cd6680",
+ "outputId": "cb11c807-409c-475f-f4d2-d848329ffce7"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "digit 6927\n",
+ "flight 3320\n",
+ "hashtag 2633\n",
+ "cancelled 956\n",
+ "thanks 921\n",
+ "service 910\n",
+ "just 801\n",
+ "customer 726\n",
+ "time 695\n",
+ "help 687\n",
+ "dtype: int64"
+ ]
+ },
+ "execution_count": 44,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Compared to the most frequent tokens in the second DTM\n",
+ "second_dtm.sum().sort_values(ascending=False).head(10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "38363398-fdf5-456b-ae3d-cae9d5294140",
+ "metadata": {
+ "id": "38363398-fdf5-456b-ae3d-cae9d5294140"
+ },
+ "source": [
+ "\n",
+ "\n",
+ "# Term Frequency-Inverse Document Frequency\n",
+ "\n",
+ "So far, we're relying on word frequency to give us information about a document. This assumes if a word appears more often in a document, it's more informative. However, this may not always be the case. For example, we've already removed stop words because they are not informative, despite the fact that they appear many times in a document. We also know the word \"flight\" is among the most frequent words, but it is not that informative, because it appears in many documents. Since we're looking at airline tweets, we shouldn't be surprised to see the word \"flight\"!\n",
+ "\n",
+ "To remedy this, we use a weighting scheme called **tf-idf (term frequency-inverse document frequency)**. The big idea behind tf-idf is to weight a word not just by its frequency within a document, but also by its frequency in one document relative to the remaining documents. So, when we construct the DTM, we will be assigning each term a **tf-idf score**. Specifically, term $t$ in document $d$ is assigned a tf-idf score as follows:\n",
+ "\n",
+ "\n",
+ "\n",
+ "In essence, the tf-idf score of a word in a document is the product of two components: **term frequency (tf)** and **inverse document frequency (idf)**. The idf acts as a scaling factor. If a word occurs in all documents, then idf equals 1. No scaling will happen. But idf is typically greater than 1, which is the weight we assign to the word to make the tf-idf score higher, so as to highlight that the word is informative. In practice, we add 1 to both the denominator and numerator (\"add-1 smooth\") to prevent any issues with zero occurrences.\n",
+ "\n",
+ "We can also create a tf-idf DTM using `sklearn`. We'll use a `TfidfVectorizer` this time:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Esta línea de código importa TfidfVectorizer, una herramienta que convierte textos en números usando la técnica TF-IDF, que permite medir la importancia de cada palabra en un conjunto de documentos. Esto facilita que los modelos de machine learning puedan procesar y analizar el texto de manera efectiva.*"
+ ],
+ "metadata": {
+ "id": "RCYzWtd6isU9"
+ },
+ "id": "RCYzWtd6isU9"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f5e32d8a-c42d-475f-aab4-21eca8b1aee8",
+ "metadata": {
+ "id": "f5e32d8a-c42d-475f-aab4-21eca8b1aee8"
+ },
+ "outputs": [],
+ "source": [
+ "from sklearn.feature_extraction.text import TfidfVectorizer"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d23916c1-5693-456c-b71d-6d9d78d1e2e4",
+ "metadata": {
+ "id": "d23916c1-5693-456c-b71d-6d9d78d1e2e4"
+ },
+ "outputs": [],
+ "source": [
+ "# Create a tfidf vectorizer\n",
+ "vectorizer = TfidfVectorizer(lowercase=True,\n",
+ " stop_words='english',\n",
+ " min_df=2,\n",
+ " max_df=0.95,\n",
+ " max_features=None)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7af5b342-ab18-4766-9561-e38e50cd1e9b",
+ "metadata": {
+ "id": "7af5b342-ab18-4766-9561-e38e50cd1e9b",
+ "outputId": "be9232f3-cb80-4d0f-fd62-7cfd7c59efbe"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "<11541x3553 sparse matrix of type ''\n",
+ "\twith 88287 stored elements in Compressed Sparse Row format>"
+ ]
+ },
+ "execution_count": 47,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Fit and transform\n",
+ "tf_dtm = vectorizer.fit_transform(tweets['text_lemmatized'])\n",
+ "tf_dtm"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "55e509c8-5402-4be0-9143-0e448fff7066",
+ "metadata": {
+ "id": "55e509c8-5402-4be0-9143-0e448fff7066",
+ "outputId": "0277c92c-66ca-4e99-dbdc-a2addb2a7f7c"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
aa
\n",
+ "
aadv
\n",
+ "
aadvantage
\n",
+ "
aal
\n",
+ "
abandon
\n",
+ "
abc
\n",
+ "
ability
\n",
+ "
able
\n",
+ "
aboard
\n",
+ "
abq
\n",
+ "
...
\n",
+ "
yummy
\n",
+ "
yup
\n",
+ "
yvonne
\n",
+ "
yvr
\n",
+ "
yyj
\n",
+ "
yyz
\n",
+ "
zero
\n",
+ "
zone
\n",
+ "
zoom
\n",
+ "
zurich
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
...
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
...
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
...
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
...
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
...
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
0.0
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5 rows × 3553 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " aa aadv aadvantage aal abandon abc ability able aboard abq ... \\\n",
+ "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
+ "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
+ "2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
+ "3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
+ "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... \n",
+ "\n",
+ " yummy yup yvonne yvr yyj yyz zero zone zoom zurich \n",
+ "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
+ "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
+ "2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
+ "3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
+ "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
+ "\n",
+ "[5 rows x 3553 columns]"
+ ]
+ },
+ "execution_count": 48,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Create a tf-idf dataframe\n",
+ "tfidf = pd.DataFrame(tf_dtm.todense(),\n",
+ " columns=vectorizer.get_feature_names_out(),\n",
+ " index=tweets.index)\n",
+ "tfidf.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "45ba13ea-c429-4ff1-a9a2-abf27c4d0888",
+ "metadata": {
+ "id": "45ba13ea-c429-4ff1-a9a2-abf27c4d0888"
+ },
+ "source": [
+ "You may have noticed that the vocabulary size is the same as we saw in Challenge 2. This is because we used the same parameter setting when creating the vectorizer. But the values in the matrix are different—they are tf-idf scores instead of raw counts."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fa58c360-5c55-4fa0-8c55-1f00e68baa9a",
+ "metadata": {
+ "id": "fa58c360-5c55-4fa0-8c55-1f00e68baa9a"
+ },
+ "source": [
+ "## Interpret TF-IDF Values"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bdad233d-ebc1-420f-9b67-c227c48f3e60",
+ "metadata": {
+ "id": "bdad233d-ebc1-420f-9b67-c227c48f3e60"
+ },
+ "source": [
+ "Let's take a look the document where a term has the highest tf-idf values. We'll use the `.idxmax()` method to find the index."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "995b511a-d448-4cfb-a6a0-22a465efd8a8",
+ "metadata": {
+ "id": "995b511a-d448-4cfb-a6a0-22a465efd8a8",
+ "outputId": "cca8c541-474f-47f4-ad72-cc2041cff708"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "aa 10077\n",
+ "aadv 9285\n",
+ "aadvantage 9974\n",
+ "aal 10630\n",
+ "abandon 7859\n",
+ " ... \n",
+ "yyz 1350\n",
+ "zero 2705\n",
+ "zone 3177\n",
+ "zoom 3920\n",
+ "zurich 10622\n",
+ "Length: 3553, dtype: int64"
+ ]
+ },
+ "execution_count": 49,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Retrieve the index of the document\n",
+ "tfidf.idxmax()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fccc0249-7c68-42ee-8290-ff41715e346b",
+ "metadata": {
+ "id": "fccc0249-7c68-42ee-8290-ff41715e346b"
+ },
+ "source": [
+ "For example, the term \"worst\" occurs most distinctively in the 918th tweet."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "09b222fb-ad8c-4767-a974-dd261370a06e",
+ "metadata": {
+ "id": "09b222fb-ad8c-4767-a974-dd261370a06e",
+ "outputId": "7becaa64-4264-4bb5-cb23-dd1e613925c4"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "918"
+ ]
+ },
+ "execution_count": 50,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "tfidf.idxmax()['worst']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "955a48bc-dc93-481b-ba49-29876fc577fb",
+ "metadata": {
+ "id": "955a48bc-dc93-481b-ba49-29876fc577fb"
+ },
+ "source": [
+ "Recall that this is the tweet where the word \"worst\" appears six times!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "079ee0e0-476f-4236-ba8a-615ba7a0efe8",
+ "metadata": {
+ "id": "079ee0e0-476f-4236-ba8a-615ba7a0efe8",
+ "outputId": "05bf355c-82be-4b01-f7d6-07536e6ae1c6"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\"USER is the worst. worst reservation policies. worst costumer service. worst worst worst. congrats, USER you're not that bad!\""
+ ]
+ },
+ "execution_count": 51,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "tweets['text_processed'].iloc[918]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9dd06bbc-e2fc-49e4-9354-efdaca5cfbd3",
+ "metadata": {
+ "id": "9dd06bbc-e2fc-49e4-9354-efdaca5cfbd3"
+ },
+ "source": [
+ "How about \"cancel\"? Let's take a look at another example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f809df1a-1178-4272-a415-42edb20173b2",
+ "metadata": {
+ "id": "f809df1a-1178-4272-a415-42edb20173b2",
+ "outputId": "3c7739f9-df94-429a-e62e-f4a1f550d019"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "5945"
+ ]
+ },
+ "execution_count": 52,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "tfidf.idxmax()['cancel']"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8093b6a7-54ca-468a-9376-b3c0be0b6f9b",
+ "metadata": {
+ "id": "8093b6a7-54ca-468a-9376-b3c0be0b6f9b",
+ "outputId": "d82f97e5-52cb-417b-f3e9-23b23cf7ccab"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'USER cancelled flighted 😢'"
+ ]
+ },
+ "execution_count": 53,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "tweets['text_processed'].iloc[5945]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "163dcecd-dc8c-43a9-952d-5bc84a307b07",
+ "metadata": {
+ "id": "163dcecd-dc8c-43a9-952d-5bc84a307b07"
+ },
+ "source": [
+ "## 🥊 Challenge 3: Words with Highest Mean TF-IDF scores\n",
+ "\n",
+ "We have obtained tf-idf values for each term in each document. But what do these values tell us about the sentiments of tweets? Are there any words that are particularly informative for positive/negative tweets?\n",
+ "\n",
+ "To explore this, let's gather the indices of all positive/negative tweets and calculate the mean tf-idf scores of words appear in each category.\n",
+ "\n",
+ "We've provided the following starter code to guide you:\n",
+ "- Subset the `tweets` dataframe according to the `airline_sentiment` label and retrieve the index of each subset (`.index`). Assign the index to `positive_index` or `negative_index`.\n",
+ "- For each subset:\n",
+ " - Retrieve the td-idf representation\n",
+ " - Take the mean tf-idf values across the subset using `.mean()`\n",
+ " - Sort the mean values in the descending order using `.sort_values()`\n",
+ " - Get the top 10 terms using `.head()`\n",
+ "\n",
+ "Next, run `pos.plot` and `neg.plot` to plot the words with the highest mean tf-idf scores for each subset."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2bfbf838-9ff6-48b8-ad5d-5e75304fe060",
+ "metadata": {
+ "id": "2bfbf838-9ff6-48b8-ad5d-5e75304fe060"
+ },
+ "outputs": [],
+ "source": [
+ "# Complete the boolean masks\n",
+ "positive_index = tweets[...].index\n",
+ "negative_index = tweets[...].index"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c67ea1f-de9e-49a9-94f2-a3351446e364",
+ "metadata": {
+ "id": "8c67ea1f-de9e-49a9-94f2-a3351446e364"
+ },
+ "outputs": [],
+ "source": [
+ "# Complete the following two lines\n",
+ "pos = tfidf.loc[...].mean().sort_values(...).head(...)\n",
+ "neg = tfidf.loc[...].mean().sort_values(...).head(...)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f1e29043-8c78-4e41-81d2-b4552030b457",
+ "metadata": {
+ "id": "f1e29043-8c78-4e41-81d2-b4552030b457"
+ },
+ "outputs": [],
+ "source": [
+ "pos.plot(kind='barh',\n",
+ " xlim=(0, 0.18),\n",
+ " color='cornflowerblue',\n",
+ " title='Top 10 terms with the highest mean tf-idf values for positive tweets');"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e8b25940-2372-4755-818e-f75e4d23daf9",
+ "metadata": {
+ "id": "e8b25940-2372-4755-818e-f75e4d23daf9"
+ },
+ "outputs": [],
+ "source": [
+ "neg.plot(kind='barh',\n",
+ " xlim=(0, 0.18),\n",
+ " color='darksalmon',\n",
+ " title='Top 10 terms with the highest mean tf-idf values for negative tweets');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "77bca876-9649-46f3-bd4f-f9f68fea649a",
+ "metadata": {
+ "id": "77bca876-9649-46f3-bd4f-f9f68fea649a"
+ },
+ "source": [
+ "🔔 **Question**: How would you interpret these results? Share your thoughts in the chat!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da410cb3-a452-441b-a94d-8f751d59d7a6",
+ "metadata": {
+ "id": "da410cb3-a452-441b-a94d-8f751d59d7a6"
+ },
+ "source": [
+ "\n",
+ "\n",
+ "## 🎬 **Demo**: Sentiment Classification Using the TF-IDF Representation\n",
+ "\n",
+ "Now that we have a tf-idf representation of the text, we are ready to do sentiment analysis!\n",
+ "\n",
+ "In this demo, we will use a logistic regression model to perform the classification task. Here we briefly step through how logistic regression works as one of the supervised Machine Learning methods, but feel free to explore our workshop on [Python Machine Learning Fundamentals](https://github.com/dlab-berkeley/Python-Machine-Learning) if you want to learn more about it.\n",
+ "\n",
+ "Logistic regression is a linear model, with which we use to predict the label of a tweet, based on a set of features ($x_1, x_2, x_3, ..., x_i$), as shown below:\n",
+ "\n",
+ "$$\n",
+ "L = \\beta_1 x_1 + \\beta_2 x_2 + \\cdots + \\beta_T x_T\n",
+ "$$\n",
+ "\n",
+ "The list of features we'll pass to the model is the vocabulary of the DTM. We also feed the model with a portion of the data, known as the training set, along with other model specification, to learn the coeffient ($\\beta_1, \\beta_2, \\beta_3, ..., \\beta_i$) of each feature. The coefficients tell us whether a feature contributes positively or negatively to the predicted value. The predicted value corresponds to adding all features (multiplied by their coefficients) up, and the predicted value gets passed to a [sigmoid function](https://en.wikipedia.org/wiki/Sigmoid_function) to be converted into the probability space, which tells us whether the predicted label is positive (when $p>0.5$) or negative (when $p<0.5$).\n",
+ "\n",
+ "The remaining portion of the data, known as the test set, is used to test whether the learned coefficients could be generalized to unseen data.\n",
+ "\n",
+ "Now that we already have the tf-idf dataframe, the feature set is ready. Let's dive into model specification!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Estas líneas de código importan dos herramientas de scikit-learn: una para crear un modelo de regresión logística con validación cruzada (LogisticRegressionCV) y otra para dividir los datos en conjuntos de entrenamiento y prueba (train_test_split), lo que permite entrenar y evaluar el modelo de manera adecuada.*"
+ ],
+ "metadata": {
+ "id": "All0kUqFjKkY"
+ },
+ "id": "All0kUqFjKkY"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "33413d63-87eb-489f-b374-3cfeaa51cf3c",
+ "metadata": {
+ "id": "33413d63-87eb-489f-b374-3cfeaa51cf3c"
+ },
+ "outputs": [],
+ "source": [
+ "from sklearn.linear_model import LogisticRegressionCV\n",
+ "from sklearn.model_selection import train_test_split"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ee87ff74-3fbb-472a-b795-6f4d18fab215",
+ "metadata": {
+ "id": "ee87ff74-3fbb-472a-b795-6f4d18fab215"
+ },
+ "source": [
+ "We'll use the `train_test_split` function from `sklearn` to separate our data into two sets:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Este bloque de código separa los datos en dos partes: las características de los tweets (X) y los sentimientos que se quieren predecir (y). Luego divide todo en conjuntos de entrenamiento y prueba, usando el 15% de los datos para probar el modelo y el resto para entrenarlo, de manera que se pueda evaluar su rendimiento de forma confiable.*"
+ ],
+ "metadata": {
+ "id": "bWmC1zmxjXoK"
+ },
+ "id": "bWmC1zmxjXoK"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "64cec8b9-14d9-4897-9c02-cc89fcf7b3c6",
+ "metadata": {
+ "id": "64cec8b9-14d9-4897-9c02-cc89fcf7b3c6"
+ },
+ "outputs": [],
+ "source": [
+ "# Train-test split\n",
+ "X = tfidf\n",
+ "y = tweets['airline_sentiment']\n",
+ "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "066771d8-2f31-4646-9a1b-6d2b1b9b208c",
+ "metadata": {
+ "id": "066771d8-2f31-4646-9a1b-6d2b1b9b208c"
+ },
+ "source": [
+ "The `fit_logistic_regression` function is written below to streamline the training process."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "*Esta función entrena un modelo de regresión logística usando los datos que se le pasan. Ajusta automáticamente los parámetros para mejorar su desempeño y devuelve el modelo ya listo para hacer predicciones.*"
+ ],
+ "metadata": {
+ "id": "IYoQQ-fbjhWY"
+ },
+ "id": "IYoQQ-fbjhWY"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d46de0b2-af00-4a1d-b4cd-31b96ce545d1",
+ "metadata": {
+ "id": "d46de0b2-af00-4a1d-b4cd-31b96ce545d1"
+ },
+ "outputs": [],
+ "source": [
+ "def fit_logistic_regression(X, y):\n",
+ " '''Fits a logistic regression model to provided data.'''\n",
+ " model = LogisticRegressionCV(Cs=10,\n",
+ " penalty='l1',\n",
+ " cv=5,\n",
+ " solver='liblinear',\n",
+ " class_weight='balanced',\n",
+ " random_state=42,\n",
+ " refit=True).fit(X, y)\n",
+ " return model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "124aa7ea-1bc1-43e2-beeb-0ba2da9b2df9",
+ "metadata": {
+ "id": "124aa7ea-1bc1-43e2-beeb-0ba2da9b2df9"
+ },
+ "source": [
+ "We'll fit the model and compute the training and test accuracy."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "773963bd-6603-4fad-884b-09ce60afab18",
+ "metadata": {
+ "id": "773963bd-6603-4fad-884b-09ce60afab18"
+ },
+ "outputs": [],
+ "source": [
+ "# Fit the logistic regression model\n",
+ "model = fit_logistic_regression(X_train, y_train)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e10d06c1-d884-45d4-a03d-dd5d40bf70aa",
+ "metadata": {
+ "id": "e10d06c1-d884-45d4-a03d-dd5d40bf70aa",
+ "outputId": "1ff541ca-c3e9-4791-9f0c-088eef6a9eed"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training accuracy: 0.9455601998164951\n",
+ "Test accuracy: 0.894919168591224\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Get the training and test accuracy\n",
+ "print(f\"Training accuracy: {model.score(X_train, y_train)}\")\n",
+ "print(f\"Test accuracy: {model.score(X_test, y_test)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d4e186c5-1719-4deb-bdb4-614a9980f058",
+ "metadata": {
+ "id": "d4e186c5-1719-4deb-bdb4-614a9980f058"
+ },
+ "source": [
+ "The model achieved ~94% accuracy on the training set and ~89% on the test set—that's pretty good! The model generalizes reasonably well to the test data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "310dac39-4753-4ae8-8dfa-e65e5824cccb",
+ "metadata": {
+ "id": "310dac39-4753-4ae8-8dfa-e65e5824cccb"
+ },
+ "source": [
+ "Next, let's also take a look at the fitted coefficients to see if what we see makes sense.\n",
+ "\n",
+ "We can access them using `coef_`, and we can match each coefficient to the tokens from the vectorizer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6dcb6ef1-13b3-437e-813c-7118911847a4",
+ "metadata": {
+ "id": "6dcb6ef1-13b3-437e-813c-7118911847a4"
+ },
+ "outputs": [],
+ "source": [
+ "# Get coefs of all features\n",
+ "coefs = model.coef_.ravel()\n",
+ "\n",
+ "# Get all tokens\n",
+ "tokens = vectorizer.get_feature_names_out()\n",
+ "\n",
+ "# Create a token-coef dataframe\n",
+ "importance = pd.DataFrame()\n",
+ "importance['token'] = tokens\n",
+ "importance['coefs'] = coefs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3e63814e-9c0d-4f7a-a5e0-72cca2758d71",
+ "metadata": {
+ "id": "3e63814e-9c0d-4f7a-a5e0-72cca2758d71",
+ "outputId": "cfad72bc-f484-4226-d4dd-3ffc498c3e54"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ " token coefs\n",
+ "3165 thankful 8.002975\n",
+ "1091 exceptional 8.136278\n",
+ "1563 impressed 8.501364\n",
+ "648 compliment 8.981360\n",
+ "1373 great 9.080558\n",
+ "3498 wonderful 9.401606\n",
+ "1089 excellent 10.147230\n",
+ "250 awesome 10.315909\n",
+ "1746 kudo 11.623828\n",
+ "3164 thank 16.027534"
+ ]
+ },
+ "execution_count": 62,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Get the top 10 tokens with highest coefs\n",
+ "pos_coef = importance.sort_values('coefs').tail(10)\n",
+ "pos_coef"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7b3b7893-caa0-4281-98f0-92c9e7b31953",
+ "metadata": {
+ "id": "7b3b7893-caa0-4281-98f0-92c9e7b31953"
+ },
+ "source": [
+ "Let's plot the top 10 tokens with the highest/lowest coefficients."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17b1223b-e5c1-4992-bb7e-0a99651c3729",
+ "metadata": {
+ "id": "17b1223b-e5c1-4992-bb7e-0a99651c3729",
+ "outputId": "265aeff7-4356-442c-daeb-366fc93a5050"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnwAAAGxCAYAAAD4c2uhAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABh3ElEQVR4nO3deVxN+f8H8Ndpv+3aC5UtyVKaMJZRGCNlGca+hjFj35fxtWXsxjKGYTCjGGMdxmDsVPYthZFtkm1ERiqFUvfz+8Oj83OVlOLq9Ho+HufxcM/yOe/P6dZ9+ZzlSkIIASIiIiJSLB1tF0BERERE7xYDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDHxEREZHCMfARERERKRwDH5U4kiTlawoPD3/ntaxevRqdOnVC5cqVoaOjA1dX19eum5qaimHDhsHJyQlGRkbw8vLC+vXr87WfnTt3Ijg4uFC1urq6okWLFoVq40MXHh6e42ef17GTJAmDBg0q1L5+//33N64bFBSU53sjL8HBwZAkCf/9999bbf+2ZsyYga1bt77Xfb6NRYsWoWLFijAwMIAkSUhKSgIATJgwAc7OztDT04OlpSUAwM/PD35+fu+0npiYGAQHB+PGjRvvdD+vk9vvACmDnrYLIHrfjh8/rvF66tSpCAsLw8GDBzXme3h4vPNafv31V9y7dw+1a9eGWq3G8+fPX7tu27Ztcfr0acyaNQtubm5Yu3YtOnfuDLVajS5duuS5n507d+LHH38sdOhTOm9vbxw/flzjZ/8hHLuJEydi6NChWtv/25gxYwbatWuHzz//XNulvFZ0dDSGDBmCL7/8Ej179oSenh7MzMzw559/Yvr06Rg/fjyaN28OQ0NDAMCSJUveeU0xMTGYMmUK/Pz83jrkE+WGgY9KnI8//ljjta2tLXR0dHLMfx/27NkDHZ0XA+0tWrTA33//net6O3fuxL59++SQBwCNGjXCzZs3MXr0aHTs2BG6urrvrW6lMjc318r74E0qVKig7RIU6eLFiwCAvn37onbt2vL87N/DIUOGwM7OTp7/Pv4TSPSu8JQuUS4SExMxYMAAlC5dGgYGBihfvjzGjx+P9PR0jfWyT+ktW7YMbm5uMDQ0hIeHR75PtWaHvTf5448/YGpqivbt22vM79WrF+7evYuTJ0++dtugoCD8+OOPcr3ZU/Ypo2fPnmHcuHEoV64cDAwMULp0aQwcOFA+tZWXJUuWQE9PD5MnT5bn7d+/H02aNIG5uTmMjY1Rv359HDhwQGO77NOMFy9eROfOnWFhYQF7e3v07t0bycnJGutu2rQJderUgYWFBYyNjVG+fHn07t07z7rat2+PqlWrasxr2bIlJEnCpk2b5Hlnz56FJEnYvn07gJyns9507LL9+uuvqFKlCoyNjeHp6YkdO3a88dhle/78OcaPHw8nJyeYm5vj008/xZUrVzTWye2UblJSEvr06QMrKyuYmpoiMDAQ169fhyRJuY5G3r9//43HWgiBJUuWwMvLCyqVCqVKlUK7du1w/fp1jfWioqLQokUL2NnZwdDQEE5OTggMDMSdO3fkY5WWloZVq1bJx+xNp0LT09Px7bffokqVKjAyMoK1tTUaNWqEY8eOyesU5L26YcMG1K1bFyYmJjA1NUWzZs0QFRUlL/fz80O3bt0AAHXq1IEkSfJxnjBhAgDA3t5e43jmdko3IyMD06ZNg7u7OwwNDWFra4tevXrhwYMHGutlXxKxe/dueHt7Q6VSwd3dHStXrpTXCQ0NlX/HGzVqJB+70NDQXI/Z1q1bIUlSjt8vAFi6dCkkScL58+cBAGfOnEGnTp3g6uoKlUoFV1dXdO7cGTdv3sy17Ze97lR2bu/L/B6PgwcPws/PD9bW1lCpVHB2dsYXX3yBJ0+evLEeekuCqITr2bOnMDExkV8/ffpU1KhRQ5iYmIi5c+eKvXv3iokTJwo9PT0REBCgsS0AUbZsWeHh4SHWrVsntm3bJvz9/QUAsWnTpgLVERgYKFxcXHJd9vHHH4tatWrlmP/3338LAGLZsmWvbfeff/4R7dq1EwDE8ePH5enZs2dCrVaLZs2aCT09PTFx4kSxd+9eMXfuXGFiYiJq1qwpnj17Jrfj4uIiAgMDhRBCqNVqMXLkSKGvry9CQkLkdX799VchSZL4/PPPxZYtW8T27dtFixYthK6urti/f7+83uTJkwUAUblyZTFp0iSxb98+MX/+fGFoaCh69eolr3fs2DEhSZLo1KmT2Llzpzh48KAICQkR3bt3z/NY/vTTTwKAuHv3rhBCiOfPnwszMzOhUqlE37595fVmz54t9PT0REpKihBCiLCwMAFAhIWFvfHYCfHi5+/q6ipq164tNm7cKHbu3Cn8/PyEnp6eiI2NzbPG7H25urqKrl27ir/++kusW7dOODs7i0qVKonMzEx53Z49e2q8N7KyskSDBg2EkZGRmDVrlti7d6+YMmWKqFSpkgAgJk+eXOBjLYQQffv2Ffr6+mLkyJFi9+7dYu3atcLd3V3Y29uLe/fuCSGESE1NFdbW1sLHx0ds3LhRREREiA0bNoh+/fqJmJgYIYQQx48fFyqVSgQEBMjH7OLFi689Fs+fPxeNGjUSenp6YtSoUWLnzp1i27Zt4n//+59Yt26dEEIU6L06ffp0IUmS6N27t9ixY4fYsmWLqFu3rjAxMZHruHjxopgwYYIAIEJCQsTx48fFP//8I86ePSv69OkjAIjdu3eL48ePi9u3bwshhPD19RW+vr4aPwd/f39hYmIipkyZIvbt2yd+/vlnUbp0aeHh4SGePHkir+vi4iLKlCkjPDw8xOrVq8WePXtE+/btBQAREREhhBAiISFBzJgxQwAQP/74o3zsEhISXnvc7OzsRNeuXXMsq127tvD29pZfb9q0SUyaNEn88ccfIiIiQqxfv174+voKW1tb8eDBA3m9V38Hcut3ttzel/k5HnFxccLIyEg0bdpUbN26VYSHh4vffvtNdO/eXTx69CjXvlLhMfBRifdq4MsOCxs3btRYb/bs2QKA2Lt3rzwPgFCpVPKHoRBCZGZmCnd3d1GxYsUC1ZFX4KtUqZJo1qxZjvl3794VAMSMGTPybHvgwIEit//f7d69WwAQc+bM0Zi/YcMGAUAsX75cnpcd+J48eSK++OILYWFhoRHi0tLShJWVlWjZsqVGW1lZWcLT01PUrl1bnpcdQl7d74ABA4SRkZFQq9VCCCHmzp0rAIikpKQ8+/eqf/75RwAQq1evFkIIceTIEQFAjBkzRpQrV05er2nTpqJevXry69w+7F537IR48fO3t7eXA6MQQty7d0/o6OiImTNn5llj9r5e/U/Exo0b5YCZ7dUP1r/++ksAEEuXLtXYdubMma8NfG861sePHxcAxLx58zTWu337tlCpVGLMmDFCCCHOnDkjAIitW7fm2T8TExPRs2fPPNfJtnr1agFArFix4rXr5Pe9euvWLaGnpycGDx6ssd7jx4+Fg4OD6NChgzwvJCREABCnT5/WWDf7mL0chITIGXzWrVsnAIjNmzdrrHf69GkBQCxZskSe5+LiIoyMjMTNmzfleU+fPhVWVlbi66+/ludt2rQpx3swLyNGjBAqlUrjdyQmJkYAEIsWLXrtdpmZmSI1NVWYmJiIhQsXyvMLE/jyezx+//13AUBER0fnq49UNHhKl+gVBw8ehImJCdq1a6cxPygoCABynD5p0qQJ7O3t5de6urro2LEj/vnnH/kUV1GQJOmtluUl+0aV7L5la9++PUxMTHL09eHDh2jcuDFOnTqFI0eOoEmTJvKyY8eOITExET179kRmZqY8qdVq+Pv74/Tp00hLS9Nor1WrVhqva9SogWfPniEhIQEAUKtWLQBAhw4dsHHjRvz777/56leFChXg6uqK/fv3AwD27duH6tWro1u3boiLi0NsbCzS09Nx5MgRfPrpp/lq83UaNWoEMzMz+bW9vT3s7OzydaoMyP0YAMhz+4iICAAvjsvLsq/vzO9+Xj7WO3bsgCRJ6Natm8bPz8HBAZ6envJp7ooVK6JUqVIYO3YsfvrpJ8TExOSrn3nZtWsXjIyM8jxVn9/36p49e5CZmYkePXpo9MPIyAi+vr5Fevfpjh07YGlpiZYtW2rsy8vLCw4ODjn25eXlBWdnZ/m1kZER3Nzc8v1eyU3v3r3x9OlTbNiwQZ4XEhICQ0NDjZu5UlNTMXbsWFSsWBF6enrQ09ODqakp0tLScOnSpbfe/8vyezy8vLxgYGCAr776CqtWrcpxyQC9Gwx8RK94+PAhHBwccoQoOzs76Onp4eHDhxrzHRwccrSRPe/Vdd+WtbV1rm0lJiYCAKysrN6q3YcPH0JPTw+2trYa8yVJgoODQ459Xr16FSdPnkTz5s1RrVo1jWX3798HALRr1w76+voa0+zZsyGEkOt9uV8vy74b8unTpwCAhg0bYuvWrfIHeJkyZVCtWjWsW7fujX1r0qSJHAL279+Ppk2bonr16rC3t8f+/ftx9OhRPH36tNCB79U+ZPcjuw8F3f7VY5Cb7J/bqz/3l//jUdD93L9/H0II2Nvb5/j5nThxQn6si4WFBSIiIuDl5YX//e9/qFq1KpycnDB58uQ87zLPy4MHD+Dk5JTnNa35fa9mvw9r1aqVox8bNmwo0sfT3L9/H0lJSTAwMMixr3v37uXYV2HfK7mpWrUqatWqhZCQEABAVlYW1qxZg9atW2u8P7p06YLFixfjyy+/xJ49e3Dq1CmcPn0atra2hdr/y/J7PCpUqID9+/fDzs4OAwcORIUKFVChQgUsXLiwSOqg3PEuXaJXWFtb4+TJkxBCaIS+hIQEZGZmwsbGRmP9e/fu5Wgje15uf+DfRvXq1bFu3TpkZmZCT+//f20vXLgAADnCV35ZW1sjMzMTDx480PggFULg3r178ghbtrp166J9+/bo06cPgBcXhmd/SGcfl0WLFr32Tte8AsnrtG7dGq1bt0Z6ejpOnDiBmTNnokuXLnB1dUXdunVfu12TJk3wyy+/4NSpUzh58qR8IX7jxo2xb98+3Lx5E6amph/kXblvkv1zS0xM1PhQz+29mF82NjaQJAmHDx+Ww+DLXp5XvXp1rF+/HkIInD9/HqGhofj222+hUqnwzTffFHjftra2OHLkCNRq9WtDX37fq9nvw99//x0uLi4FrqUgbGxsYG1tjd27d+e6/OWR33epV69eGDBgAC5duoTr168jPj4evXr1kpcnJydjx44dmDx5ssbPJz09Pcd/wnJjZGSU4wYfADkCbUGOxyeffIJPPvkEWVlZOHPmDBYtWoRhw4bB3t4enTp1emNNVHAc4SN6RZMmTZCamprjobGrV6+Wl7/swIED8qgC8OJ/2Bs2bECFChVQpkyZIqmpTZs2SE1NxebNmzXmr1q1Ck5OTqhTp06e279u1Ci7L2vWrNGYv3nzZqSlpeXoKwD07NkT69evR0hICHr06IGsrCwAQP369WFpaYmYmBj4+PjkOhkYGBSs46/0wdfXF7NnzwYAjTsuc9OkSRNIkoSJEydCR0cHDRs2BAB8+umnCAsLw759+9CwYUPo6+u/cb9A3iNu75uvry8AaJzGA5Dvu8Nz06JFCwgh8O+//+b6s6tevXqObSRJgqenJxYsWABLS0ucPXtWXlaQkavmzZvj2bNnr70bFcj/e7VZs2bQ09NDbGzsa9+HRaVFixZ4+PAhsrKyct1P5cqVC9zm27zfOnfuDCMjI4SGhiI0NBSlS5fGZ599Ji+XJAlCiBxB/ueff5Z/f/Pi6uqKq1evajyl4OHDhxp3UANvdzx0dXVRp04d+W74l99DVLQ4wkf0ih49euDHH39Ez549cePGDVSvXh1HjhzBjBkzEBAQkOMUoI2NDRo3boyJEyfCxMQES5YsweXLl/P14RsTEyNfA3Xv3j08efJE/uYFDw8P+blfzZs3R9OmTdG/f3+kpKSgYsWKWLduHXbv3o01a9a88Rl82R/Ws2fPRvPmzaGrq4saNWqgadOmaNasGcaOHYuUlBTUr18f58+fx+TJk1GzZk1079491/batWsHY2NjtGvXDk+fPsW6detgamqKRYsWoWfPnkhMTES7du1gZ2eHBw8e4Ny5c3jw4AGWLl36xmPyskmTJuHOnTto0qQJypQpg6SkJCxcuBD6+vpy6HkdOzs7VKtWDXv37kWjRo1gbGwM4EXgS0xMRGJiIubPn//GGl537AoTXgvL398f9evXx8iRI5GSkoKPPvoIx48fl/9Tkt/H/bysfv36+Oqrr9CrVy+cOXMGDRs2hImJCeLj43HkyBFUr14d/fv3x44dO7BkyRJ8/vnnKF++PIQQ2LJlC5KSktC0aVO5verVqyM8PBzbt2+Ho6MjzMzMXhuAOnfujJCQEPTr1w9XrlxBo0aNoFarcfLkSVSpUgWdOnXK93vV1dUV3377LcaPH4/r16/D398fpUqVwv3793Hq1CmYmJhgypQpb3HUc+rUqRN+++03BAQEYOjQoahduzb09fVx584dhIWFoXXr1mjTpk2B2swerV++fDnMzMxgZGSEcuXK5Xm2wNLSEm3atEFoaCiSkpIwatQojfeAubk5GjZsiO+++w42NjZwdXVFREQEfvnlF/lbRPLSvXt3LFu2DN26dUPfvn3x8OFDzJkzB+bm5m91PH766SccPHgQgYGBcHZ2xrNnz+TH0xT2EgvKg9ZuFyH6QLx6l64QQjx8+FD069dPODo6Cj09PeHi4iLGjRun8egHIV7cpTlw4ECxZMkSUaFCBaGvry/c3d3Fb7/9lq99Z98NmNv08p2WQry4y3DIkCHCwcFBGBgYiBo1asiPrHiT9PR08eWXXwpbW1shSZIAIOLi4oQQL+4UHDt2rHBxcRH6+vrC0dFR9O/fP8fjEV5+LEu2sLAwYWpqKvz9/eVHLkRERIjAwEBhZWUl9PX1RenSpUVgYKDGY2pedxdk9l2T2bXt2LFDNG/eXJQuXVoYGBgIOzs7ERAQIA4fPpyvfg8fPlwAENOnT9eYn/34kvPnz+foD165QzGvY5f983+Vi4vLG+9Qzd7Xq4/viYuLkx8Vku3VuyGFECIxMVH06tVLWFpaCmNjY9G0aVNx4sQJAUDjrsv8HutsK1euFHXq1BEmJiZCpVKJChUqiB49eogzZ84IIYS4fPmy6Ny5s6hQoYJQqVTCwsJC1K5dW4SGhmq0Ex0dLerXry+MjY0FgFzv8nzZ06dPxaRJk0SlSpWEgYGBsLa2Fo0bNxbHjh3TWCc/71UhhNi6dato1KiRMDc3F4aGhsLFxUW0a9dO487ywt6lK8SLR6PMnTtXeHp6CiMjI2Fqairc3d3F119/La5duyavl9vvz+va/P7770W5cuWErq5ujvfC6+zdu1f+23H16tUcy+/cuSO++OILUapUKWFmZib8/f3F33//neO9mtvvgBBCrFq1SlSpUkUYGRkJDw8PsWHDhlzfl/k5HsePHxdt2rQRLi4uwtDQUFhbWwtfX1+xbdu2N/aT3p4khBDvKVsSKY4kSRg4cCAWL16s7VKIsHbtWnTt2hVHjx5FvXr1tF0OEX1AeEqXiKgYWrduHf79919Ur14dOjo6OHHiBL777js0bNiQYY+IcmDgIyIqhszMzLB+/XpMmzYNaWlpcHR0RFBQEKZNm6bt0ojoA8RTukREREQKx8eyEBERESkcAx8RERGRwjHwERERESkcb9ooAdRqNe7evQszM7Mc3w9LREREHyYhBB4/fvzG75rODwa+EuDu3bsoW7astssgIiKit3D79u1Cf1UnA18JkP2F1bdv387xVThERET0YUpJSUHZsmXlz/HCYOArAbJP45qbmzPwERERFTNFcTkWb9ogIiIiUjgGPiIiIiKFY+AjIiIiUjhew0dERET5IoRAZmYmsrKytF2KIujq6kJPT++9PDKNgY+IiIjeKCMjA/Hx8Xjy5Im2S1EUY2NjODo6wsDA4J3uh4GPiIiI8qRWqxEXFwddXV04OTnBwMCAD/IvJCEEMjIy8ODBA8TFxaFSpUqFfrhyXhj4SpDBKxJhoMrUdhlEirdigJW2SyAqUhkZGVCr1ShbtiyMjY21XY5iqFQq6Ovr4+bNm8jIyICRkdE72xdv2iAiIqJ8eZcjUCXV+zqm/MkRERERKRwDHxEREZHClbhr+MLDw9GoUSM8evQIlpaW72w/N27cQLly5RAVFQUvL69c17l37x66d++OY8eOQV9fH0lJSW9s933VT0RElB99lyS+t319iNfHXr58GUFBQYiOjoa7uzuio6O1XVKuFB/4/Pz84OXlhe+//17bpeSwYMECxMfHIzo6GhYWFtouh4iIiApo8uTJMDExwZUrV2Bqaqrtcl5L8YHvQxYbG4uPPvoIlSpV0nYpRERE9BZiY2MRGBgIFxcXbZeSJ0VfwxcUFISIiAgsXLgQkiRBkiTcuHEDABAZGQkfHx8YGxujXr16uHLlirxdbGwsWrduDXt7e5iamqJWrVrYv3+/Rtuurq6YMWMGevfuDTMzMzg7O2P58uWvrUWtVqNv375wc3PDzZs34erqis2bN2P16tWQJAlBQUG4ceMGJEnSGA5OSkqCJEkIDw8vykNDRERUIqjVasyePRsVK1aEoaEhnJ2dMX36dADAhQsX0LhxY6hUKlhbW+Orr75CamqqxvYhISGoUqUKjIyM4O7ujiVLlsjLJElCZGQkvv32W0iShODgYGRkZGDQoEFwdHSEkZERXF1dMXPmzPfa59woOvAtXLgQdevWRd++fREfH4/4+HiULVsWADB+/HjMmzcPZ86cgZ6eHnr37i1vl5qaioCAAOzfvx9RUVFo1qwZWrZsiVu3bmm0P2/ePPj4+CAqKgoDBgxA//79cfny5Rx1ZGRkoEOHDjhz5gyOHDkCFxcXnD59Gv7+/ujQoQPi4+OxcOHCIut3eno6UlJSNCYiIqKSaNy4cZg9ezYmTpyImJgYrF27Fvb29njy5An8/f1RqlQpnD59Gps2bcL+/fsxaNAgedsVK1Zg/PjxmD59Oi5duoQZM2Zg4sSJWLVqFQAgPj4eVatWxciRIxEfH49Ro0bhhx9+wLZt27Bx40ZcuXIFa9asgaurq5Z6//8UfUrXwsICBgYGMDY2hoODAwDIgWz69Onw9fUFAHzzzTcIDAzEs2fPYGRkBE9PT3h6esrtTJs2DX/88Qe2bdum8UYICAjAgAEDAABjx47FggULEB4eDnd3d3md1NRUBAYG4unTpwgPD5ev1bO1tYWhoSFUKpVc26NHj4qk3zNnzsSUKVOKpC0iIqLi6vHjx1i4cCEWL16Mnj17AgAqVKiABg0aYMWKFXj69ClWr14NExMTAMDixYvRsmVLzJ49G/b29pg6dSrmzZuHtm3bAgDKlSuHmJgYLFu2DD179oSDgwP09PRgamoqf5bfunULlSpVQoMGDSBJ0gdzqlfRI3x5qVGjhvxvR0dHAEBCQgIAIC0tDWPGjIGHhwcsLS1hamqKy5cv5xjhe7kNSZLg4OAgt5Gtc+fOSE1Nxd69e9/bjRnjxo1DcnKyPN2+ffu97JeIiOhDcunSJaSnp6NJkya5LvP09JTDHgDUr18farUaV65cwYMHD3D79m306dMHpqam8jRt2jTExsa+dp/Zd+xWrlwZQ4YMwd69e99J3wpK0SN8edHX15f/nf19gGq1GgAwevRo7NmzB3PnzkXFihWhUqnQrl07ZGRkvLaN7Hay28gWEBCANWvW4MSJE2jcuHGeNWU/bVsIIc97/vx5AXsGGBoawtDQsMDbERERKYlKpXrtMiHEa78P+OXP8xUrVqBOnToay3V1dV/brre3N+Li4rBr1y7s378fHTp0wKefforff//9LXpQdBQ/wmdgYICsrKwCbXP48GEEBQWhTZs2qF69OhwcHOSbPQqqf//+mDVrFlq1aoWIiIg817W1tQXw4pqAbB/q83yIiIg+dJUqVYJKpcKBAwdyLPPw8EB0dDTS0tLkeUePHoWOjg7c3Nxgb2+P0qVL4/r166hYsaLGVK5cuTz3a25ujo4dO2LFihXYsGEDNm/ejMTE9/e8wtwofoTP1dUVJ0+exI0bN2BqappjBC43FStWxJYtW9CyZUtIkoSJEyfma7vXGTx4MLKystCiRQvs2rULDRo0yHU9lUqFjz/+GLNmzYKrqyv+++8/TJgw4a33S0REVJIZGRlh7NixGDNmDAwMDFC/fn08ePAAFy9eRNeuXTF58mT07NkTwcHBePDgAQYPHozu3bvD3t4eABAcHIwhQ4bA3NwczZs3R3p6Os6cOYNHjx5hxIgRue5zwYIFcHR0hJeXF3R0dLBp0yY4ODho/csSFB/4Ro0ahZ49e8LDwwNPnz5FSEjIG7dZsGABevfujXr16sHGxgZjx44t9J2uw4YNg1qtRkBAAHbv3o169erlut7KlSvRu3dv+Pj4oHLlypgzZw4+++yzQu2biIjoXfkQv/3iZRMnToSenh4mTZqEu3fvwtHREf369YOxsTH27NmDoUOHolatWjA2NsYXX3yB+fPny9t++eWXMDY2xnfffYcxY8bAxMQE1atXx7Bhw167P1NTU8yePRvXrl2Drq4uatWqhZ07d8qXbWmLJF6+YIwUKSUlBRYWFugxNw4GKnNtl0OkeB/6ByBRQT179gxxcXEoV64cjIyMtF2OouR1bLM/v5OTk2FuXrjPb8Vfw0dERERU0jHwERERESmc4q/ho/+3qK9VoYeEiYiIqPjhCB8RERGRwjHwERERUb7wPs+i976OKQMfERER5Sn7m6WePHmi5UqUJ/uYvvrtXUWN1/ARERFRnnR1dWFpaSl/X7yxsfFrv5aM8kcIgSdPniAhIQGWlpZ5fl1bUWDgIyIiojdycHAAADn0UdGwtLSUj+27xMBHREREbyRJEhwdHWFnZ4fnz59ruxxF0NfXf+cje9kY+IiIiCjfdHV131tIoaLDmzaIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI6Bj4iIiEjhGPiIiIiIFI7fpVuCDF6RCANVprbLICpxVgyw0nYJRFTCcYSPiIiISOEY+IiIiIgUjoGPiIiISOEY+ApBkiRs3br1ve4zPDwckiQhKSnpve6XiIiIii8GvnwIDg6Gl5dXjvnx8fFo3rz5+y+IiIiIqAB4l24hODg4aLsEIiIiojf6IEb4hBCYM2cOypcvD5VKBU9PT/z+++8QQuDTTz+Fv78/hBAAgKSkJDg7O2P8+PHy9tu2bYOPjw+MjIxgY2ODtm3byssyMjIwZswYlC5dGiYmJqhTpw7Cw8Pl5aGhobC0tMTWrVvh5uYGIyMjNG3aFLdv35aXT5kyBefOnYMkSZAkCaGhoQByntK9cOECGjduDJVKBWtra3z11VdITU2VlwcFBeHzzz/H3Llz4ejoCGtrawwcOBDPnz+X11mzZg18fHxgZmYGBwcHdOnSBQkJCUV5uImIiKiE+SAC34QJExASEoKlS5fi4sWLGD58OLp164ZDhw5h1apVOHXqFH744QcAQL9+/WBvb4/g4GAAwF9//YW2bdsiMDAQUVFROHDgAHx8fOS2e/XqhaNHj2L9+vU4f/482rdvD39/f1y7dk1e58mTJ5g+fTpWrVqFo0ePIiUlBZ06dQIAdOzYESNHjkTVqlURHx+P+Ph4dOzYMUcfnjx5An9/f5QqVQqnT5/Gpk2bsH//fgwaNEhjvbCwMMTGxiIsLAyrVq1CaGioHCCBFwF16tSpOHfuHLZu3Yq4uDgEBQUV6Himp6cjJSVFYyIiIqKSS+undNPS0jB//nwcPHgQdevWBQCUL18eR44cwbJly7B27VosW7YM3bt3x/3797F9+3ZERUVBX18fADB9+nR06tQJU6ZMkdv09PQEAMTGxmLdunW4c+cOnJycAACjRo3C7t27ERISghkzZgAAnj9/jsWLF6NOnToAgFWrVqFKlSo4deoUateuDVNTU+jp6eV5Cve3337D06dPsXr1apiYmAAAFi9ejJYtW2L27Nmwt7cHAJQqVQqLFy+Grq4u3N3dERgYiAMHDqBv374AgN69e8ttli9fHj/88ANq166N1NRUmJqa5uuYzpw5U+N4EBERUcmm9RG+mJgYPHv2DE2bNoWpqak8rV69GrGxsQCA9u3bo23btpg5cybmzZsHNzc3efvo6Gg0adIk17bPnj0LIQTc3Nw02o6IiJDbBgA9PT2NUUF3d3dYWlri0qVL+e7HpUuX4OnpKYc9AKhfvz7UajWuXLkiz6tatSp0dXXl146OjhqnbKOiotC6dWu4uLjAzMwMfn5+AIBbt27lu5Zx48YhOTlZnrJPTxMREVHJpPURPrVaDeDFqdnSpUtrLDM0NATw4nRpZGQkdHV1NU7FAoBKpcqzbV1dXXnbl706WiZJUo7tc5v3OkKI167/8vzskcmXl2Ufg7S0NHz22Wf47LPPsGbNGtja2uLWrVto1qwZMjIy8l2LoaGhfOyIiIiItB74PDw8YGhoiFu3bsHX1zfXdUaOHAkdHR3s2rULAQEBCAwMROPGjQEANWrUwIEDB9CrV68c29WsWRNZWVlISEjAJ5988toaMjMzcebMGdSuXRsAcOXKFSQlJcHd3R0AYGBggKysrDf2Y9WqVUhLS5NH+Y4ePQodHR2NEcm8XL58Gf/99x9mzZqFsmXLAgDOnDmTr22JiIiIXkfrgc/MzAyjRo3C8OHDoVar0aBBA6SkpODYsWMwNTWFjY0NVq5ciePHj8Pb2xvffPMNevbsifPnz6NUqVKYPHkymjRpggoVKqBTp07IzMzErl27MGbMGLi5uaFr167o0aMH5s2bh5o1a+K///7DwYMHUb16dQQEBAB4Meo2ePBg/PDDD9DX18egQYPw8ccfywHQ1dUVcXFxiI6ORpkyZWBmZpZjBK1r166YPHkyevbsieDgYDx48ACDBw9G9+7d5ev33sTZ2RkGBgZYtGgR+vXrh7///htTp04t2gNOREREJY7Wr+EDgKlTp2LSpEmYOXMmqlSpgmbNmmH79u1wdXVFnz59EBwcDG9vbwDA5MmT4eTkhH79+gEA/Pz8sGnTJmzbtg1eXl5o3LgxTp48KbcdEhKCHj16YOTIkahcuTJatWqFkydPyiNoAGBsbIyxY8eiS5cuqFu3LlQqFdavXy8v/+KLL+Dv749GjRrB1tYW69aty9EHY2Nj7NmzB4mJiahVqxbatWuHJk2aYPHixfk+Dra2tggNDcWmTZvg4eGBWbNmYe7cuQU+nkREREQvk0T2A+5KqNDQUAwbNkzRX1WWkpICCwsL9JgbBwOVubbLISpxVgyw0nYJRFQMZX9+Jycnw9y8cJ/fH8QIHxERERG9Owx8RERERApX4k/plgRFOSRMRERE7wdP6RIRERFRvjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESkcAx8RERGRwjHwERERESmcnrYLoPdn8IpEGKgytV0GEb3GigFW2i6BiBSKI3xERERECsfAR0RERKRwDHxEREREClcsAp+fnx+GDRum7TI+CDwWREREVFDF4qaNLVu2QF9fX9tlEBERERVLxSLwWVm9/zvXnj9/zpBJREREilDsTum6urpi2rRp6NGjB0xNTeHi4oI///wTDx48QOvWrWFqaorq1avjzJkz8vahoaGwtLTE1q1b4ebmBiMjIzRt2hS3b9+W1wkODoaXlxdWrlyJ8uXLw9DQEEIIJCcn46uvvoKdnR3Mzc3RuHFjnDt3Tt7u3LlzaNSoEczMzGBubo6PPvpI3vfNmzfRsmVLlCpVCiYmJqhatSp27twpbxsTE4OAgACYmprC3t4e3bt3x3///ScvT0tLk/vp6OiIefPmvatDTERERApWLALfqxYsWID69esjKioKgYGB6N69O3r06IFu3brh7NmzqFixInr06AEhhLzNkydPMH36dKxatQpHjx5FSkoKOnXqpNHuP//8g40bN2Lz5s2Ijo4GAAQGBuLevXvYuXMnIiMj4e3tjSZNmiAxMREA0LVrV5QpUwanT59GZGQkvvnmG3lkcODAgUhPT8ehQ4dw4cIFzJ49G6ampgCA+Ph4+Pr6wsvLC2fOnMHu3btx//59dOjQQa5n9OjRCAsLwx9//IG9e/ciPDwckZGRbzw+6enpSElJ0ZiIiIio5CoWp3RfFRAQgK+/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SUmBhYUFesyNg4FKe6fjiUj7Vgyw0nYJRJRP2Z/fycnJhb6crliO8BERERFR/jHwERERESlcvh/LQsXfor5WWr3DmoiIiLSDI3xERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwDHxERERECsfAR0RERKRwetougN6fwSsSYaDK1HYZRKQgKwZYabsEIsoHjvARERERKRwDHxEREZHCMfARERERKRwDXxEJDQ2FpaWl/Do4OBheXl5aq4eIiIgoGwNfMfJqqCQiIiLKDwY+IiIiIoVTVOATQmDOnDkoX748VCoVPD098fvvv0MIgU8//RT+/v4QQgAAkpKS4OzsjPHjx8vbb9u2DT4+PjAyMoKNjQ3atm0rL8vIyMCYMWNQunRpmJiYoE6dOggPDy9QfSEhIahSpQqMjIzg7u6OJUuWyMtu3LgBSZKwZcsWNGrUCMbGxvD09MTx48cBAOHh4ejVqxeSk5MhSRIkSUJwcPDbHywiIiIqMRQV+CZMmICQkBAsXboUFy9exPDhw9GtWzccOnQIq1atwqlTp/DDDz8AAPr16wd7e3s5NP31119o27YtAgMDERUVhQMHDsDHx0duu1evXjh69CjWr1+P8+fPo3379vD398e1a9fyVduKFSswfvx4TJ8+HZcuXcKMGTMwceJErFq1SmO98ePHY9SoUYiOjoabmxs6d+6MzMxM1KtXD99//z3Mzc0RHx+P+Ph4jBo1Ktd9paenIyUlRWMiIiKikksxD15OS0vD/PnzcfDgQdStWxcAUL58eRw5cgTLli3D2rVrsWzZMnTv3h3379/H9u3bERUVBX19fQDA9OnT0alTJ0yZMkVu09PTEwAQGxuLdevW4c6dO3BycgIAjBo1Crt370ZISAhmzJjxxvqmTp2KefPmyaOG5cqVQ0xMDJYtW4aePXvK640aNQqBgYEAgClTpqBq1ar4559/4O7uDgsLC0iSBAcHhzz3NXPmTI1+EBERUcmmmMAXExODZ8+eoWnTphrzMzIyULNmTQBA+/bt8ccff2DmzJlYunQp3Nzc5PWio6PRt2/fXNs+e/YshBAa6wMvRtKsra3fWNuDBw9w+/Zt9OnTR2MfmZmZsLCw0Fi3Ro0a8r8dHR0BAAkJCXB3d3/jfrKNGzcOI0aMkF+npKSgbNmy+d6eiIiIlEUxgU+tVgN4cWq2dOnSGssMDQ0BAE+ePEFkZCR0dXVznIpVqVR5tq2rqytv+zJTU9N817ZixQrUqVNHY9mr7WWPOAKAJEka2+eXoaGh3GciIiIixQQ+Dw8PGBoa4tatW/D19c11nZEjR0JHRwe7du1CQEAAAgMD0bhxYwAvRtYOHDiAXr165diuZs2ayMrKQkJCAj755JMC12Zvb4/SpUvj+vXr6Nq1a4G3z2ZgYICsrKy33p6IiIhKJsUEPjMzM4waNQrDhw+HWq1GgwYNkJKSgmPHjsHU1BQ2NjZYuXIljh8/Dm9vb3zzzTfo2bMnzp8/j1KlSmHy5Mlo0qQJKlSogE6dOiEzMxO7du3CmDFj4Obmhq5du6JHjx6YN28eatasif/++w8HDx5E9erVERAQ8Mb6goODMWTIEJibm6N58+ZIT0/HmTNn8OjRI43Tr3lxdXVFamoqDhw4AE9PTxgbG8PY2Liwh46IiIgUTlF36U6dOhWTJk3CzJkzUaVKFTRr1gzbt2+Hq6sr+vTpg+DgYHh7ewMAJk+eDCcnJ/Tr1w8A4Ofnh02bNmHbtm3w8vJC48aNcfLkSbntkJAQ9OjRAyNHjkTlypXRqlUrnDx5Mt/Xxn355Zf4+eefERoaiurVq8PX1xehoaEoV65cvvtXr1499OvXDx07doStrS3mzJlTgKNDREREJZUksh9MR4qVkpICCwsL9JgbBwOVubbLISIFWTHAStslEClW9ud3cnIyzM0L9/mtqBE+IiIiIsqJgY+IiIhI4RRz0wa92aK+VoUeEiYiIqLihyN8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcHraLoDen8ErEmGgytR2GUSkcCsGWGm7BCJ6BUf4iIiIiBSOgY+IiIhI4Rj4iIiIiBSOgY+IiIhI4Rj4iIiIiBSOgY+IiIhI4Yp94Nu9ezcaNGgAS0tLWFtbo0WLFoiNjQUAfPHFFxg8eLC87rBhwyBJEi5evAgAyMzMhJmZGfbs2QMAEEJgzpw5KF++PFQqFTw9PfH777/L2z969Ahdu3aFra0tVCoVKlWqhJCQEHn5hQsX0LhxY6hUKlhbW+Orr75CamqqvDwoKAiff/45ZsyYAXt7e1haWmLKlCnIzMzE6NGjYWVlhTJlymDlypUaffz333/RsWNHlCpVCtbW1mjdujVu3LhR5MeSiIiIlKnYB760tDSMGDECp0+fxoEDB6Cjo4M2bdpArVbDz88P4eHh8roRERGwsbFBREQEAOD06dN49uwZ6tevDwCYMGECQkJCsHTpUly8eBHDhw9Ht27d5PUnTpyImJgY7Nq1C5cuXcLSpUthY2MDAHjy5An8/f1RqlQpnD59Gps2bcL+/fsxaNAgjXoPHjyIu3fv4tChQ5g/fz6Cg4PRokULlCpVCidPnkS/fv3Qr18/3L59W263UaNGMDU1xaFDh3DkyBGYmprC398fGRkZuR6T9PR0pKSkaExERERUcklCCKHtIorSgwcPYGdnhwsXLkAIAU9PTyQkJEBXVxf29vaYPHkyzp07h40bN2LmzJn4888/ceLECaSlpcHGxgYHDx5E3bp15fa+/PJLPHnyBGvXrkWrVq1gY2OTYwQOAFasWIGxY8fi9u3bMDExAQDs3LkTLVu2xN27d2Fvb4+goCCEh4fj+vXr0NF5kbXd3d1hZ2eHQ4cOAQCysrJgYWGBn3/+GZ06dcLKlSsxZ84cXLp0CZIkAQAyMjJgaWmJrVu34rPPPstRS3BwMKZMmZJjfo+5cTBQmRf+IBMR5YEPXiYqGikpKbCwsEBycjLMzQv3+V3sR/hiY2PRpUsXlC9fHubm5ihXrhwA4NatW6hWrRqsra0RERGBw4cPw9PTE61atZJH7MLDw+Hr6wsAiImJwbNnz9C0aVOYmprK0+rVq+VTxP3798f69evh5eWFMWPG4NixY3Idly5dgqenpxz2AKB+/fpQq9W4cuWKPK9q1apy2AMAe3t7VK9eXX6tq6sLa2trJCQkAAAiIyPxzz//wMzMTK7JysoKz549k+t61bhx45CcnCxP2aOFREREVDIV+69Wa9myJcqWLYsVK1bAyckJarUa1apVQ0ZGBiRJQsOGDREeHg4DAwP4+fmhWrVqyMrKwoULF3Ds2DEMGzYMAKBWqwEAf/31F0qXLq2xD0NDQwBA8+bNcfPmTfz111/Yv38/mjRpgoEDB2Lu3LkQQsgjcK96eb6+vn6OZbnNy65HrVbjo48+wm+//ZajXVtb21z3Z2hoKNdMREREVKwD38OHD3Hp0iUsW7YMn3zyCQDgyJEjGuv4+flh+fLlMDAwwLfffgtJkvDJJ59g7ty5ePr0qXz9noeHBwwNDXHr1i151C83tra2CAoKQlBQED755BOMHj0ac+fOhYeHB1atWoW0tDR5lO/o0aPQ0dGBm5vbW/fR29sbGzZsgJ2dXaGHc4mIiKhkKtandLPvWl2+fDn++ecfHDx4ECNGjNBYx8/PDxcvXsSFCxfkUOjn54fffvsN3t7ecogyMzPDqFGjMHz4cKxatQqxsbGIiorCjz/+iFWrVgEAJk2ahD///BP//PMPLl68iB07dqBKlSoAgK5du8LIyAg9e/bE33//jbCwMAwePBjdu3eHvb39W/exa9eusLGxQevWrXH48GHExcUhIiICQ4cOxZ07d966XSIiIio5inXg09HRwfr16xEZGYlq1aph+PDh+O677zTWqVatGmxsbODp6SmHO19fX2RlZeUYyZs6dSomTZqEmTNnokqVKmjWrBm2b98uXxdoYGCAcePGoUaNGmjYsCF0dXWxfv16AICxsTH27NmDxMRE1KpVC+3atUOTJk2wePHiQvXR2NgYhw4dgrOzM9q2bYsqVaqgd+/eePr0KUf8iIiIKF8Ud5cu5ZR9lw/v0iWi94F36RIVDd6lS0RERET5xsBHREREpHDF+i5dKphFfa143R8REVEJxBE+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOAY+IiIiIoVj4CMiIiJSOD1tF0Dvz+AViTBQZWq7DCKi92rFACttl0CkdRzhIyIiIlI4Bj4iIiIihWPgIyIiIlI4Br5C8PPzw7Bhw4qsvfDwcEiShKSkpCJrk4iIiIiBj4iIiEjhGPiIiIiIFI6Brwjt3r0bFhYW+PbbbyFJEqKjo+VlSUlJkCQJ4eHh8rydO3fCzc0NKpUKjRo1wo0bN3K0uXnzZlStWhWGhoZwdXXFvHnz3n1HiIiISFEY+IrI+vXr0aFDB6xevRo9evR44/q3b99G27ZtERAQgOjoaHz55Zf45ptvNNaJjIxEhw4d0KlTJ1y4cAHBwcGYOHEiQkND82w7PT0dKSkpGhMRERGVXAx8RWDJkiXo168f/vzzT7Ru3Tpf2yxduhTly5fHggULULlyZXTt2hVBQUEa68yfPx9NmjTBxIkT4ebmhqCgIAwaNAjfffddnm3PnDkTFhYW8lS2bNm37RoREREpAANfIW3evBnDhg3D3r170ahRo3xvd+nSJXz88ceQJEmeV7du3Rzr1K9fX2Ne/fr1ce3aNWRlZb227XHjxiE5OVmebt++ne+6iIiISHkY+ArJy8sLtra2CAkJgRACAKCj8+KwZr8GgOfPn2ts9/Ky1xFCaATC/G5naGgIc3NzjYmIiIhKLga+QqpQoQLCwsLw559/YvDgwQAAW1tbAEB8fLy83ss3cACAh4cHTpw4oTHv1dceHh44cuSIxrxjx47Bzc0Nurq6RdUFIiIiUjgGviLg5uaGsLAw+fSuSqXCxx9/jFmzZiEmJgaHDh3ChAkTNLbp168fYmNjMWLECFy5cgVr167NcTPGyJEjceDAAUydOhVXr17FqlWrsHjxYowaNeo99o6IiIiKOwa+IlK5cmUcPHgQ69atw8iRI7Fy5Uo8f/4cPj4+GDp0KKZNm6axvrOzMzZv3ozt27fD09MTP/30E2bMmKGxjre3NzZu3Ij169ejWrVqmDRpEr799tscN3cQERER5UUS+bkojIq1lJQUWFhYoMfcOBioeD0fEZUsKwZYabsEoreS/fmdnJxc6OvxOcJHREREpHAMfEREREQKp6ftAuj9WdTXio9oISIiKoE4wkdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERArHwEdERESkcAx8RERERAqnp+0C6P0ZvCIRBqpMbZdBRERUpFYMsNJ2CR88jvARERERKRwDHxEREZHCMfARERERKRwD31sKDw+HJElISkp6p/u5ceMGJElCdHT0O90PERERKRcDXz75+flh2LBh2i6DiIiI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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot the top 10 tokens that have the highest coefs\n",
+ "pos_coef.sort_values('coefs', ascending=False) \\\n",
+ " .plot(kind='barh',\n",
+ " xlim=(0, 18),\n",
+ " x='token',\n",
+ " color='cornflowerblue',\n",
+ " title='Top 10 tokens with highest coeffient values');"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "159e00c6-8a9f-484f-aea2-853fd5512083",
+ "metadata": {
+ "id": "159e00c6-8a9f-484f-aea2-853fd5512083",
+ "outputId": "98ff22a3-1474-4cfe-8979-99a4efd966ed"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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WvoPpg8aNAg//vhjoSXGwmBqaqr1XK1WY8SIERg7dmyGvpUrV8br16/Rrl07tGvXDps2bYKNjQ0iIyPh6emZ4SKDTp06YdeuXbhx4wZcXFyyrUOhUECh4EwZERERZS/Pt9lYt25dYdTxQU6fPp3heY0aNbKc0WrQoAGuX78OJyenTF+/evUqnj17hrlz52pmuM6dO5dp37lz58LMzAweHh4ICwvTml0jIiIiyo8S8cvmUVFRGD9+PG7fvo0tW7Zg2bJl+Pbbb7PsP3nyZJw6dQqjR4/GpUuXcOfOHezZswdjxowB8G4WzcjICMuWLcP9+/exZ88ezJgxI8v1LVy4EP369UPr1q1x69atAh8fERERlS4lIqANGDAAb9++RePGjTF69GiMGTMGw4cPz7K/q6srjh49ijt37uCzzz5D/fr14evrC3t7ewCAjY0NgoKCsGPHDtSpUwdz587FwoULs60hMDAQvXr1QuvWrfHXX38V6PiIiIiodMn1VZxU8NKvAuFVnESFj1dxElFBKYqrOEvEDBoRERFRScKARkRERCQzeb6KkwqezdBpxeq2JURERFS4OINGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyY6DrAgiIXTsdiUqFrssgoiJkO2qWrksgIhnjDBoRERGRzDCgEREREckMAxoRERGRzJTogObu7o5x48bpugwiIiKiPCnRAY2IiIioOGJAK2DJycm6LoGIiIiKuRIf0NRqNSZNmgQrKyvY2dnB399f81pkZCS6dOkCMzMzWFhYoFevXvjnn380r3t5eaFr165a6xs3bhzc3d01z93d3eHt7Y3x48fD2toabdu2LeQRERERUUlX4gPa+vXrYWpqioiICMyfPx/Tp09HSEgIhBDo2rUr4uLicPToUYSEhODevXvo3bt3vrZhYGCA8PBwrF69Ost+SUlJUKlUWg8iIiKi95X4G9W6urrCz88PAFCjRg0sX74coaGhAIArV67gwYMHcHBwAABs3LgRH330Ec6ePYtGjRrlehtOTk6YP39+jv3mzJmDgICAfIyCiIiISpMSP4Pm6uqq9dze3h4xMTG4efMmHBwcNOEMAOrUqYMyZcrg5s2bedqGm5tbrvr5+PggPj5e84iKisrTdoiIiKh0KPEzaIaGhlrPJUmCWq2GEAKSJGXo/+92PT09CCG0Xk9JScmwjKmpaa5qUSgUUCj4k05ERESUvRI/g5aVOnXqIDIyUmsW68aNG4iPj0ft2rUBADY2Nnjy5InWcpcuXSrKMomIiKgUKrUBrU2bNnB1dUW/fv1w4cIFnDlzBgMGDEDLli01hyxbt26Nc+fOYcOGDbhz5w78/Pxw7do1HVdOREREJV2pDWiSJCE4OBhly5ZFixYt0KZNG1SrVg3btm3T9PH09ISvry8mTZqERo0aISEhAQMGDNBh1URERFQaSOL9k6yoyKhUKlhaWuLuogkwV/LcNKLSxHbULF2XQET5lP79HR8fDwsLi0LZRqmdQSMiIiKSKwY0IiIiIpkp8bfZKA5shk4rtClSIiIiKn44g0ZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDJjoOsCCIhdOx2JSoWuyyCiYsR21Cxdl0BEhYgzaEREREQyw4BGREREJDMMaEREREQyw4CWC2FhYZAkCS9fvtR1KURERFQKMKDlwieffIInT57A0tJS16UQERFRKVDqA1pycnKOfYyMjGBnZwdJkoqgIiIiIirtSl1Ac3d3h7e3N8aPHw9ra2vUqFEDkiTh0qVLmj4vX76EJEkICwsDkPEQZ1BQEMqUKYMDBw6gdu3aMDMzQ/v27fHkyZOiHxARERGVOKUuoAHA+vXrYWBggPDwcBw4cCBf63jz5g0WLlyIjRs34tixY4iMjMTEiROzXSYpKQkqlUrrQURERPS+UhnQnJycMH/+fDg7O8PY2Dhf60hJScGqVavg5uaGBg0awNvbG6GhodkuM2fOHFhaWmoeDg4O+do2ERERlWylMqC5ubl98DpMTExQvXp1zXN7e3vExMRku4yPjw/i4+M1j6ioqA+ug4iIiEqeUvlTT6amppr/r6f3LqMKITRtKSkpOa7D0NBQ67kkSVrryIxCoYBCwZ90IiIiouyVyhm0f7OxsQEArRP8/33BABEREVFRK5UzaP+mVCrRtGlTzJ07F1WqVMGzZ88wdepUXZdFREREpVipn0EDgF9//RUpKSlwc3PDt99+i5kzZ+q6JCIiIirFJJHTiVNUaFQqFSwtLXF30QSYK3luGhHlnu2oWbougajUSv/+jo+Ph4WFRaFsgzNoRERERDLDgEZEREQkM6X+IgE5sBk6rdCmSImIiKj44QwaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJjIGuCyAgdu10JCoVui6DiIop21GzdF0CERUwzqARERERyQwDGhEREZHMMKARERERyQwDGhEREZHMMKARERERyQwD2v+XkpKi6xKIiIiIAJSAgLZz5064uLhAqVSiXLlyaNOmDV6/fg0A+PXXX/HRRx9BoVDA3t4e3t7emuUkScKqVavQpUsXmJqaYubMmQCAP/74Aw0bNoSxsTGqVauGgIAApKamAgAmTJiAzz//XLOOJUuWQJIk/Pnnn5o2Z2dnrF69uiiGTkRERCVUsQ5oT548QZ8+fTB48GDcvHkTYWFh6N69O4QQWLlyJUaPHo3hw4fj6tWr2LNnD5ycnLSW9/PzQ5cuXXD16lUMHjwYBw4cwNdff42xY8fixo0bWL16NYKCgjBr1rt7DLm7u+P48eNQq9UAgKNHj8La2hpHjx4FADx9+hR//fUXWrZsmWm9SUlJUKlUWg8iIiKi90lCCKHrIvLrwoULaNiwIR4+fAhHR0et1ypWrIhBgwZpZsbeJ0kSxo0bh8DAQE1bixYt0KFDB/j4+GjaNm3ahEmTJiE6Ohrx8fGwsrLCmTNn0KBBA9jY2GDixInYvXs3zpw5gy1btuC7777D06dPM92mv78/AgICMrTfXTQB5rxRLRHlE29US1S0VCoVLC0tER8fDwsLi0LZRrGeQatbty48PDzg4uKCnj17Ys2aNXjx4gViYmIQHR0NDw+PbJd3c3PTen7+/HlMnz4dZmZmmsewYcPw5MkTvHnzBpaWlqhXrx7CwsJw9epV6Onp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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot the top 10 tokens that have the lowest coefs\n",
+ "neg_coef.plot(kind='barh',\n",
+ " xlim=(0, -18),\n",
+ " x='token',\n",
+ " color='darksalmon',\n",
+ " title='Top 10 tokens with lowest coeffient values');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2eed48ea-fd35-4585-9b98-90456aaee447",
+ "metadata": {
+ "id": "2eed48ea-fd35-4585-9b98-90456aaee447"
+ },
+ "source": [
+ "Words like \"ruin,\" \"rude,\" and \"hour\" are strong indicators of negative sentiment, while \"thank,\" \"awesome,\" and \"wonderful\" are associated with positive sentiment.\n",
+ "\n",
+ "We will wrap up Part 2 with these plots. These coefficient terms and the words with the highest TF-IDF values provide different perspectives on the sentiment of tweets. If you'd like, take some time to compare the two sets of plots and see which one provides a better account of the sentiments conveyed in tweets."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a4430fbd-108f-4a02-ab64-ef36c5949e56",
+ "metadata": {
+ "id": "a4430fbd-108f-4a02-ab64-ef36c5949e56"
+ },
+ "source": [
+ "
\n",
+ "\n",
+ "## ❗ Key Points\n",
+ "\n",
+ "* A Bag-of-Words representation is a simple method to transform our text data to numbers. It focuses on word frequency but not word order.\n",
+ "* A TF-IDF representation is a step further; it also considers if a certain word distinctively appears in one document or occurs uniformally across all documents.\n",
+ "* With a numerical representation, we can perform a range of text classification task, such as sentiment analysis.\n",
+ "\n",
+ "
"
+ ]
}
- ],
- "source": [
- "# Plot the top 10 tokens that have the highest coefs\n",
- "pos_coef.sort_values('coefs', ascending=False) \\\n",
- " .plot(kind='barh', \n",
- " xlim=(0, 18),\n",
- " x='token',\n",
- " color='cornflowerblue',\n",
- " title='Top 10 tokens with highest coeffient values');"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 64,
- "id": "159e00c6-8a9f-484f-aea2-853fd5512083",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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WvoPpg8aNAg//vhjoSXGwmBqaqr1XK1WY8SIERg7dmyGvpUrV8br16/Rrl07tGvXDps2bYKNjQ0iIyPh6emZ4SKDTp06YdeuXbhx4wZcXFyyrUOhUECh4EwZERERZS/Pt9lYt25dYdTxQU6fPp3heY0aNbKc0WrQoAGuX78OJyenTF+/evUqnj17hrlz52pmuM6dO5dp37lz58LMzAweHh4ICwvTml0jIiIiyo8S8cvmUVFRGD9+PG7fvo0tW7Zg2bJl+Pbbb7PsP3nyZJw6dQqjR4/GpUuXcOfOHezZswdjxowB8G4WzcjICMuWLcP9+/exZ88ezJgxI8v1LVy4EP369UPr1q1x69atAh8fERERlS4lIqANGDAAb9++RePGjTF69GiMGTMGw4cPz7K/q6srjh49ijt37uCzzz5D/fr14evrC3t7ewCAjY0NgoKCsGPHDtSpUwdz587FwoULs60hMDAQvXr1QuvWrfHXX38V6PiIiIiodMn1VZxU8NKvAuFVnESFj1dxElFBKYqrOEvEDBoRERFRScKARkRERCQzeb6KkwqezdBpxeq2JURERFS4OINGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyw4BGREREJDMMaEREREQyY6DrAgiIXTsdiUqFrssgoiJkO2qWrksgIhnjDBoRERGRzDCgEREREckMAxoRERGRzJTogObu7o5x48bpugwiIiKiPCnRAY2IiIioOGJAK2DJycm6LoGIiIiKuRIf0NRqNSZNmgQrKyvY2dnB399f81pkZCS6dOkCMzMzWFhYoFevXvjnn380r3t5eaFr165a6xs3bhzc3d01z93d3eHt7Y3x48fD2toabdu2LeQRERERUUlX4gPa+vXrYWpqioiICMyfPx/Tp09HSEgIhBDo2rUr4uLicPToUYSEhODevXvo3bt3vrZhYGCA8PBwrF69Ost+SUlJUKlUWg8iIiKi95X4G9W6urrCz88PAFCjRg0sX74coaGhAIArV67gwYMHcHBwAABs3LgRH330Ec6ePYtGjRrlehtOTk6YP39+jv3mzJmDgICAfIyCiIiISpMSP4Pm6uqq9dze3h4xMTG4efMmHBwcNOEMAOrUqYMyZcrg5s2bedqGm5tbrvr5+PggPj5e84iKisrTdoiIiKh0KPEzaIaGhlrPJUmCWq2GEAKSJGXo/+92PT09CCG0Xk9JScmwjKmpaa5qUSgUUCj4k05ERESUvRI/g5aVOnXqIDIyUmsW68aNG4iPj0ft2rUBADY2Nnjy5InWcpcuXSrKMomIiKgUKrUBrU2bNnB1dUW/fv1w4cIFnDlzBgMGDEDLli01hyxbt26Nc+fOYcOGDbhz5w78/Pxw7do1HVdOREREJV2pDWiSJCE4OBhly5ZFixYt0KZNG1SrVg3btm3T9PH09ISvry8mTZqERo0aISEhAQMGDNBh1URERFQaSOL9k6yoyKhUKlhaWuLuogkwV/LcNKLSxHbULF2XQET5lP79HR8fDwsLi0LZRqmdQSMiIiKSKwY0IiIiIpkp8bfZKA5shk4rtClSIiIiKn44g0ZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDLDgEZEREQkMwxoRERERDJjoOsCCIhdOx2JSoWuyyCiYsR21Cxdl0BEhYgzaEREREQyw4BGREREJDMMaEREREQyw4CWC2FhYZAkCS9fvtR1KURERFQKMKDlwieffIInT57A0tJS16UQERFRKVDqA1pycnKOfYyMjGBnZwdJkoqgIiIiIirtSl1Ac3d3h7e3N8aPHw9ra2vUqFEDkiTh0qVLmj4vX76EJEkICwsDkPEQZ1BQEMqUKYMDBw6gdu3aMDMzQ/v27fHkyZOiHxARERGVOKUuoAHA+vXrYWBggPDwcBw4cCBf63jz5g0WLlyIjRs34tixY4iMjMTEiROzXSYpKQkqlUrrQURERPS+UhnQnJycMH/+fDg7O8PY2Dhf60hJScGqVavg5uaGBg0awNvbG6GhodkuM2fOHFhaWmoeDg4O+do2ERERlWylMqC5ubl98DpMTExQvXp1zXN7e3vExMRku4yPjw/i4+M1j6ioqA+ug4iIiEqeUvlTT6amppr/r6f3LqMKITRtKSkpOa7D0NBQ67kkSVrryIxCoYBCwZ90IiIiouyVyhm0f7OxsQEArRP8/33BABEREVFRK5UzaP+mVCrRtGlTzJ07F1WqVMGzZ88wdepUXZdFREREpVipn0EDgF9//RUpKSlwc3PDt99+i5kzZ+q6JCIiIirFJJHTiVNUaFQqFSwtLXF30QSYK3luGhHlnu2oWbougajUSv/+jo+Ph4WFRaFsgzNoRERERDLDgEZEREQkM6X+IgE5sBk6rdCmSImIiKj44QwaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJjIGuCyAgdu10JCoVui6DiIop21GzdF0CERUwzqARERERyQwDGhEREZHMMKARERERyQwDGhEREZHMMKARERERyQwD2v+XkpKi6xKIiIiIAJSAgLZz5064uLhAqVSiXLlyaNOmDV6/fg0A+PXXX/HRRx9BoVDA3t4e3t7emuUkScKqVavQpUsXmJqaYubMmQCAP/74Aw0bNoSxsTGqVauGgIAApKamAgAmTJiAzz//XLOOJUuWQJIk/Pnnn5o2Z2dnrF69uiiGTkRERCVUsQ5oT548QZ8+fTB48GDcvHkTYWFh6N69O4QQWLlyJUaPHo3hw4fj6tWr2LNnD5ycnLSW9/PzQ5cuXXD16lUMHjwYBw4cwNdff42xY8fixo0bWL16NYKCgjBr1rt7DLm7u+P48eNQq9UAgKNHj8La2hpHjx4FADx9+hR//fUXWrZsmWm9SUlJUKlUWg8iIiKi90lCCKHrIvLrwoULaNiwIR4+fAhHR0et1ypWrIhBgwZpZsbeJ0kSxo0bh8DAQE1bixYt0KFDB/j4+GjaNm3ahEmTJiE6Ohrx8fGwsrLCmTNn0KBBA9jY2GDixInYvXs3zpw5gy1btuC7777D06dPM92mv78/AgICMrTfXTQB5rxRLRHlE29US1S0VCoVLC0tER8fDwsLi0LZRrGeQatbty48PDzg4uKCnj17Ys2aNXjx4gViYmIQHR0NDw+PbJd3c3PTen7+/HlMnz4dZmZmmsewYcPw5MkTvHnzBpaWlqhXrx7CwsJw9epV6OnpYcSIEbh8+TISEhIQFhaW5ewZAPj4+CA+Pl7ziIqKKpD9QERERCVLsf6pJ319fYSEhODkyZM4ePAgli1bhilTpiA0NDRXy5uammo9V6vVCAgIQPfu3TP0NTY2BvDuMGdYWBiMjIzQsmVLlC1bFh999BHCw8MRFhaGcePGZbk9hUIBhYIzZURERJS9Yh3QgHeHKps3b47mzZtj2rRpcHR0REhICKpUqYLQ0FC0atUq1+tq0KABbt++neFctX9zd3fHL7/8AgMDA7Rp0wYA0LJlS2zdujXb88+IiIiIcqtYB7SIiAiEhoaiXbt2sLW1RUREBGJjY1G7dm34+/tj5MiRsLW1RYcOHZCQkIDw8HCMGTMmy/VNmzYNnTt3hoODA3r27Ak9PT1cuXIFV69e1ZzL1qJFCyQkJOCPP/7QtLm7u+PLL7+EjY0N6tSpUyRjJyIiopKrWAc0CwsLHDt2DEuWLIFKpYKjoyMWLVqEDh06AAASExMRGBiIiRMnwtraGj169Mh2fZ6enti7dy+mT5+O+fPnw9DQELVq1cLQoUM1fSwtLVG/fn1ERkZqwthnn30GtVrN2TMiIiIqEMX6Ks7iLv0qEF7FSUQfgldxEhUtXsVJREREVAoxoBERERHJTLE+B62ksBk6rdCmSImIiKj44QwaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJDAMaERERkcwwoBERERHJjIGuCyAgdu10JCoVui6DiCjfbEfN0nUJRCUKZ9CIiIiIZIYBjYiIiEhmGNCIiIiIZIYBLRfCwsIgSRJevnyp61KIiIioFGBAIyIiIpKZUhXQkpOTdV0CERERUY5KdEBzd3eHt7c3xo8fD2tra9SoUQOSJOHSpUuaPi9fvoQkSQgLC9O07du3DzVr1oRSqUSrVq3w8OHDDOs+efIkWrRoAaVSCQcHB4wdOxavX78u/EERERFRiVeiAxoArF+/HgYGBggPD8eBAwdy7B8VFYXu3bujY8eOuHTpEoYOHYoffvhBq8/Vq1fh6emJ7t2748qVK9i2bRtOnDgBb2/vbNedlJQElUql9SAiIiJ6X4kPaE5OTpg/fz6cnZ1hbGycY/+VK1eiWrVqCAwMhLOzM/r16wcvLy+tPgsWLEDfvn0xbtw41KhRA5988gmWLl2KDRs2IDExMct1z5kzB5aWlpqHg4PDhw6PiIiISqASH9Dc3Nzy1P/mzZto2rQpJEnStDVr1kyrz/nz5xEUFAQzMzPNw9PTE2q1Gg8ePMhy3T4+PoiPj9c8oqKi8jYYIiIiKhVK/E89mZqaav6/nt67PCqE0LSlpKRo9f/3a1lRq9UYMWIExo4dm+G1ypUrZ7mcQqGAQsGfdCIiIqLslfiA9m82NjYAgCdPnqB+/foAoHXBAADUqVMHwcHBWm2nT5/Wet6gQQNcv34dTk5OhVYrERERlV4l/hDnvymVSjRt2hRz587FjRs3cOzYMUydOlWrz8iRI3Hv3j2MHz8et2/fxubNmxEUFKTVZ/LkyTh16hRGjx6NS5cu4c6dO9izZw/GjBlThKMhIiKikqpUBTQA+PXXX5GSkgI3Nzd8++23mDlzptbrlStXxq5du/DHH3+gbt26WLVqFWbPnq3Vx9XVFUePHsWdO3fw2WefoX79+vD19YW9vX1RDoWIiIhKKEnk5qQrKhQqlQqWlpa4u2gCzJU8N42Iii/bUbN0XQJRkUn//o6Pj4eFhUWhbKPUzaARERERyR0DGhEREZHMlKqrOOXKZui0QpsiJSIiouKHM2hEREREMsOARkRERCQzDGhEREREMsOARkRERCQzDGhEREREMsOARkRERCQzDGhEREREMsP7oOlQ+q9sqVQqHVdCREREuZX+vV2Yv5bJgKZDz58/BwA4ODjouBIiIiLKq+fPn8PS0rJQ1s2ApkNWVlYAgMjIyEJ7g3VFpVLBwcEBUVFRJe5XEji24oljK544tuKrJI8vPj4elStX1nyPFwYGNB3S03t3CqClpWWJ++NNZ2FhwbEVQxxb8cSxFU8leWxAyR5f+vd4oay70NZMRERERPnCgEZEREQkMwxoOqRQKODn5weFQqHrUgocx1Y8cWzFE8dWPJXksQEle3xFMTZJFOY1okRERESUZ5xBIyIiIpIZBjQiIiIimWFAIyIiIpIZBjQiIiIimWFAIyIiIpIZBjQdWbFiBapWrQpjY2M0bNgQx48f13VJheLhw4cYMmQIqlatCqVSierVq8PPzw/Jycm6Lq1AJSUloV69epAkCZcuXdJ1OQXmzz//RJMmTaBUKmFtbY3u3bvruqQC8ddff6FLly6wtraGhYUFmjdvjiNHjui6rHyZNWsWPvnkE5iYmKBMmTKZ9omMjMTnn38OU1NTWFtbY+zYscXi32BOY7t8+TL69OkDBwcHKJVK1K5dGz/++GPRF5oPuXnf0j1//hyVKlWCJEl4+fJlkdT3IXI7tqCgILi6usLY2Bh2dnbw9vYuuiLzKTdjO3v2LDw8PFCmTBmULVsW7dq1y9f3AgOaDmzbtg3jxo3DlClTcPHiRXz22Wfo0KEDIiMjdV1agbt16xbUajVWr16N69evIzAwEKtWrcJ//vMfXZdWoCZNmoQKFSrouowCtWvXLvTv3x+DBg3C5cuXER4ejr59++q6rALRqVMnpKam4vDhwzh//jzq1auHzp074+nTp7ouLc+Sk5PRs2dPjBo1KtPX09LS0KlTJ7x+/RonTpzA1q1bsWvXLkyYMKGIK827nMZ2/vx52NjYYNOmTbh+/TqmTJkCHx8fLF++vIgrzbucxvZvQ4YMgauraxFUVTByM7bFixdjypQp+OGHH3D9+nWEhobC09OzCKvMn5zGlpCQAE9PT1SuXBkRERE4ceIELCws4OnpiZSUlLxtTFCRa9y4sRg5cqRWW61atcQPP/ygo4qK1vz580XVqlV1XUaB2bdvn6hVq5a4fv26ACAuXryo65I+WEpKiqhYsaJYu3atrkspcLGxsQKAOHbsmKZNpVIJAOLQoUM6rOzDrFu3TlhaWmZo37dvn9DT0xOPHz/WtG3ZskUoFAoRHx9fhBXmX1Zjy8w333wjWrVqVbgFFaCcxrZixQrRsmVLERoaKgCIFy9eFFltHyqrscXFxQmlUlki/72dPXtWABCRkZGatitXrggA4u7du3naBmfQilhycjLOnz+Pdu3aabW3a9cOJ0+e1FFVRSs+Ph5WVla6LqNA/PPPPxg2bBg2btwIExMTXZdTYC5cuIDHjx9DT08P9evXh729PTp06IDr16/rurQPVq5cOdSuXRsbNmzA69evkZqaitWrV6N8+fJo2LChrssrcKdOncLHH3+sNcPr6emJpKQknD9/XoeVFY6S9Ply48YNTJ8+HRs2bCjUH+UuaiEhIVCr1Xj8+DFq166NSpUqoVevXoiKitJ1aR/M2dkZ1tbW+OWXX5CcnIy3b9/il19+wUcffQRHR8c8ravkvOPFxLNnz5CWloby5ctrtZcvX75YHl7Jq3v37mHZsmUYOXKkrkv5YEIIeHl5YeTIkXBzc9N1OQXq/v37AAB/f39MnToVe/fuRdmyZdGyZUvExcXpuLoPI0kSQkJCcPHiRZibm8PY2BiBgYHYv39/jucCFUdPnz7N8HlTtmxZGBkZlbjPnFOnTmH79u0YMWKErkv5YElJSejTpw8WLFiAypUr67qcAnX//n2o1WrMnj0bS5Yswc6dOxEXF4e2bdsWi3Mjs2Nubo6wsDBs2rQJSqUSZmZmOHDgAPbt2wcDA4M8rYsBTUckSdJ6LoTI0CZn/v7+kCQp28e5c+e0lomOjkb79u3Rs2dPDB06VEeV5yy3Y1u2bBlUKhV8fHx0XXKu5XZsarUaADBlyhR8+eWXaNiwIdatWwdJkrBjxw4djyJzuR2bEALffPMNbG1tcfz4cZw5cwZdunRB586d8eTJE10PA0D+/n1lJ7PPFl195hT02NJdv34dXbp0wbRp09C2bdtCqDxnBTk2Hx8f1K5dG19//XUhV507BTk2tVqNlJQULF26FJ6enmjatCm2bNmCO3fu6ORinYIc29u3bzF48GA0b94cp0+fRnh4OD766CN07NgRb9++zVNdeYtz9MGsra2hr6+f4b9cY2JiMvxXrpx5e3vjq6++yrZPlSpVNP8/OjoarVq1QrNmzfDzzz8XcnUfJrdjmzlzJk6fPp3hx3Ld3NzQr18/rF+/vjDLzJfcji0hIQEAUKdOHU27QqFAtWrVZHsxS27HdvjwYezduxcvXryAhYUFgHdXVYeEhGD9+vX44YcfiqLcbOX131d27OzsEBERodX24sULpKSk6OQzpyDHlu7GjRto3bo1hg0bhqlTp35AdR+mIMd2+PBhXL16FTt37gTwLlAD775DpkyZgoCAgA+qNa8Kcmz29vYAtD9fbGxsYG1trZPPl4Ic2+bNm/Hw4UOcOnVKc1h68+bNKFu2LP773//muJ1/Y0ArYkZGRmjYsCFCQkLQrVs3TXtISAi6dOmiw8ryxtraGtbW1rnq+/jxY7Rq1UozCyP3cylyO7alS5di5syZmufR0dHw9PTEtm3b0KRJk8IsMd9yO7aGDRtCoVDg9u3b+PTTTwEAKSkpePjwYZ7PoygquR3bmzdvACDD36Genp5m5lDX8vLvKyfNmjXDrFmz8OTJE80X48GDB6FQKHRyzl1Bjg14N3PWunVrDBw4ELNmzSqw9eZHQY5t165dWjMuZ8+exeDBg3H8+HFUr169QLaRFwU5tubNmwMAbt++jUqVKgEA4uLi8OzZM518vhTk2N68eQM9PT2t2en053n9fGFA04Hx48ejf//+cHNz08woRUZGlojzst4XHR0Nd3d3VK5cGQsXLkRsbKzmNTs7Ox1W9uHePy/EzMwMAFC9enXNh05xZWFhgZEjR8LPzw8ODg5wdHTEggULAAA9e/bUcXUfplmzZihbtiwGDhyIadOmQalUYs2aNXjw4AE6deqk6/LyLDIyEnFxcYiMjERaWprmfktOTk4wMzNDu3btUKdOHfTv3x8LFixAXFwcJk6ciGHDhmlmEOUqp7Fdv34drVq1Qrt27TB+/HjNkQl9fX3Y2NjosPKc5TS290PYs2fPAAC1a9eW/bmSOY2tZs2a6NKlC7799lv8/PPPsLCwgI+PD2rVqoVWrVrptvgc5DS2tm3b4vvvv8fo0aMxZswYqNVqzJ07FwYGBnkf2wdcZUof4KeffhKOjo7CyMhINGjQQBw9elTXJRWKdevWCQCZPkqaBw8elJjbbAghRHJyspgwYYKwtbUV5ubmok2bNuLatWu6LqtAnD17VrRr105YWVkJc3Nz0bRpU7Fv3z5dl5UvAwcOzPTf15EjRzR9Hj16JDp16iSUSqWwsrIS3t7eIjExUXdF51JOY/Pz88v0dUdHR53WnRu5ed/+7ciRI8XmNhu5GVt8fLwYPHiwKFOmjLCyshLdunXTujWFXOVmbAcPHhTNmzcXlpaWomzZsqJ169bi1KlTed6WJMT/P7BNRERERLIg75OBiIiIiEohBjQiIiIimWFAIyIiIpIZBjQiIiIimWFAIyIiIpIZBjQiIiIimWFAIyIiIpIZBjQiIiIimWFAIyIiIpIZBjQiIiIimWFAIyIiIpKZ/weNbqe2GUqqyAAAAABJRU5ErkJggg==",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.4"
+ },
+ "colab": {
+ "provenance": []
}
- ],
- "source": [
- "# Plot the top 10 tokens that have the lowest coefs\n",
- "neg_coef.plot(kind='barh', \n",
- " xlim=(0, -18),\n",
- " x='token',\n",
- " color='darksalmon',\n",
- " title='Top 10 tokens with lowest coeffient values');"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "2eed48ea-fd35-4585-9b98-90456aaee447",
- "metadata": {},
- "source": [
- "Words like \"ruin,\" \"rude,\" and \"hour\" are strong indicators of negative sentiment, while \"thank,\" \"awesome,\" and \"wonderful\" are associated with positive sentiment. \n",
- "\n",
- "We will wrap up Part 2 with these plots. These coefficient terms and the words with the highest TF-IDF values provide different perspectives on the sentiment of tweets. If you'd like, take some time to compare the two sets of plots and see which one provides a better account of the sentiments conveyed in tweets."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a4430fbd-108f-4a02-ab64-ef36c5949e56",
- "metadata": {},
- "source": [
- "
\n",
- "\n",
- "## ❗ Key Points\n",
- "\n",
- "* A Bag-of-Words representation is a simple method to transform our text data to numbers. It focuses on word frequency but not word order. \n",
- "* A TF-IDF representation is a step further; it also considers if a certain word distinctively appears in one document or occurs uniformally across all documents. \n",
- "* With a numerical representation, we can perform a range of text classification task, such as sentiment analysis. \n",
- "\n",
- "