{ "cells": [ { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Lectures 5: Class demo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Imports, Announcements, LOs" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "### Imports" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "# import the libraries\n", "import os\n", "import sys\n", "sys.path.append(os.path.join(os.path.abspath(\"../\"), \"code\"))\n", "from plotting_functions import *\n", "from utils import *\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "\n", "from sklearn.compose import make_column_transformer\n", "from sklearn.feature_extraction.text import CountVectorizer\n", "from sklearn.preprocessing import StandardScaler, OneHotEncoder\n", "\n", "from sklearn.tree import DecisionTreeClassifier\n", "from sklearn.neighbors import KNeighborsClassifier\n", "from sklearn.svm import SVC\n", "\n", "%matplotlib inline\n", "\n", "pd.set_option(\"display.max_colwidth\", 200)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Incorporating text features in the Spotify dataset" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "Recall that we had dropped `song_title` feature when we worked with the Spotify dataset in Lab 1. \n", "\n", "Let's try to include it in our pipeline and examine whether we get better results. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "spotify_df = pd.read_csv(\"../data/spotify.csv\", index_col=0)\n", "X_spotify = spotify_df.drop(columns=[\"target\"])\n", "y_spotify = spotify_df[\"target\"]" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "outputs": [], "source": [ "X_train, X_test, y_train, y_test = train_test_split(\n", " X_spotify, y_spotify, test_size=0.2, random_state=123\n", ")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "(1613, 15)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train.shape" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
acousticnessdanceabilityduration_msenergyinstrumentalnesskeylivenessloudnessmodespeechinesstempotime_signaturevalencesong_titleartist
15050.0047700.5852147400.6140.000155100.0762-5.59400.0370114.0594.00.2730Cool for the SummerDemi Lovato
8130.1140000.6652167280.5130.30300000.1220-7.31410.3310100.3443.00.0373Damn Son Where'd You Find This? (feat. Kelly Holiday) - Markus Maximus RemixMarkus Maximus
6150.0302000.7982165850.4810.00000070.1280-10.48810.3140127.1364.00.6400Trill HoeWestern Tink
3190.1060000.9121940400.3170.00020860.0723-12.71900.037899.3464.00.9490Who Is He (And What Is He to You?)Bill Withers
3200.0211000.6972364560.9050.89300060.1190-7.78700.0339119.9774.00.3110AcamarFrankey
................................................
20120.0010600.5842744040.9320.00269010.1290-3.50110.333074.9764.00.2110Like A Bitch - Kill The Noise RemixKill The Noise
13460.0000210.5352035000.9740.000149100.2630-3.56600.1720116.9564.00.4310Flag of the BeastEmmure
14060.5030000.4102563330.6480.00000070.2190-4.46910.036260.3914.00.3420Don't You Cry For MeCobi
13890.7050000.8942223070.1610.00330040.3120-14.31110.0880104.9684.00.8180장가갈 수 있을까 Can I Get Married?Coffeeboy
15340.6230000.4703949200.1560.18700020.1040-17.03610.0399118.1764.00.0591Blue BalladPhil Woods
\n", "

1613 rows × 15 columns

\n", "
" ], "text/plain": [ " acousticness danceability duration_ms energy instrumentalness key \\\n", "1505 0.004770 0.585 214740 0.614 0.000155 10 \n", "813 0.114000 0.665 216728 0.513 0.303000 0 \n", "615 0.030200 0.798 216585 0.481 0.000000 7 \n", "319 0.106000 0.912 194040 0.317 0.000208 6 \n", "320 0.021100 0.697 236456 0.905 0.893000 6 \n", "... ... ... ... ... ... ... \n", "2012 0.001060 0.584 274404 0.932 0.002690 1 \n", "1346 0.000021 0.535 203500 0.974 0.000149 10 \n", "1406 0.503000 0.410 256333 0.648 0.000000 7 \n", "1389 0.705000 0.894 222307 0.161 0.003300 4 \n", "1534 0.623000 0.470 394920 0.156 0.187000 2 \n", "\n", " liveness loudness mode speechiness tempo time_signature valence \\\n", "1505 0.0762 -5.594 0 0.0370 114.059 4.0 0.2730 \n", "813 0.1220 -7.314 1 0.3310 100.344 3.0 0.0373 \n", "615 0.1280 -10.488 1 0.3140 127.136 4.0 0.6400 \n", "319 0.0723 -12.719 0 0.0378 99.346 4.0 0.9490 \n", "320 0.1190 -7.787 0 0.0339 119.977 4.0 0.3110 \n", "... ... ... ... ... ... ... ... \n", "2012 0.1290 -3.501 1 0.3330 74.976 4.0 0.2110 \n", "1346 0.2630 -3.566 0 0.1720 116.956 4.0 0.4310 \n", "1406 0.2190 -4.469 1 0.0362 60.391 4.0 0.3420 \n", "1389 0.3120 -14.311 1 0.0880 104.968 4.0 0.8180 \n", "1534 0.1040 -17.036 1 0.0399 118.176 4.0 0.0591 \n", "\n", " song_title \\\n", "1505 Cool for the Summer \n", "813 Damn Son Where'd You Find This? (feat. Kelly Holiday) - Markus Maximus Remix \n", "615 Trill Hoe \n", "319 Who Is He (And What Is He to You?) \n", "320 Acamar \n", "... ... \n", "2012 Like A Bitch - Kill The Noise Remix \n", "1346 Flag of the Beast \n", "1406 Don't You Cry For Me \n", "1389 장가갈 수 있을까 Can I Get Married? \n", "1534 Blue Ballad \n", "\n", " artist \n", "1505 Demi Lovato \n", "813 Markus Maximus \n", "615 Western Tink \n", "319 Bill Withers \n", "320 Frankey \n", "... ... \n", "2012 Kill The Noise \n", "1346 Emmure \n", "1406 Cobi \n", "1389 Coffeeboy \n", "1534 Phil Woods \n", "\n", "[1613 rows x 15 columns]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "outputs": [ { "data": { "text/plain": [ "Index(['acousticness', 'danceability', 'duration_ms', 'energy',\n", " 'instrumentalness', 'key', 'liveness', 'loudness', 'mode',\n", " 'speechiness', 'tempo', 'time_signature', 'valence', 'song_title',\n", " 'artist'],\n", " dtype='object')" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train.columns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dummy model " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
dummy
fit_time0.000 (+/- 0.000)
score_time0.000 (+/- 0.000)
test_score0.508 (+/- 0.001)
train_score0.508 (+/- 0.000)
\n", "
" ], "text/plain": [ " dummy\n", "fit_time 0.000 (+/- 0.000)\n", "score_time 0.000 (+/- 0.000)\n", "test_score 0.508 (+/- 0.001)\n", "train_score 0.508 (+/- 0.000)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.dummy import DummyClassifier\n", "\n", "results = {}\n", "dummy_model = DummyClassifier()\n", "# mean_std_cross_val_scores is defined in ../code/utils.py\n", "results['dummy'] = mean_std_cross_val_scores(dummy_model, X_train, y_train, return_train_score = True) \n", "pd.DataFrame(results)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Feature categorization" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Index(['acousticness', 'danceability', 'duration_ms', 'energy',\n", " 'instrumentalness', 'key', 'liveness', 'loudness', 'mode',\n", " 'speechiness', 'tempo', 'time_signature', 'valence', 'song_title',\n", " 'artist'],\n", " dtype='object')" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train.columns" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "key\n", "1 200\n", "7 169\n", "0 166\n", "9 152\n", "2 145\n", "11 143\n", "5 141\n", "6 127\n", "10 122\n", "8 110\n", "4 88\n", "3 50\n", "Name: count, dtype: int64" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[\"key\"].value_counts()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "time_signature\n", "4.0 1514\n", "3.0 76\n", "5.0 22\n", "1.0 1\n", "Name: count, dtype: int64" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[\"time_signature\"].value_counts()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "mode\n", "1 1002\n", "0 611\n", "Name: count, dtype: int64" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[\"mode\"].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's look at the distribution of values in the `song_title` column. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "song_title\n", "Pyramids 2\n", "Look At Wrist 2\n", "Baby 2\n", "The One 2\n", "Best Friend 2\n", " ..\n", "City Of Dreams - Radio Edit 1\n", "Face It 1\n", "The Winner Is - from Little Miss Sunshine 1\n", "History 1\n", "Blue Ballad 1\n", "Name: count, Length: 1579, dtype: int64" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[\"song_title\"].value_counts()" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "- Most of the song titles are unique, which makes sense. \n", "- What would happen if we apply one-hot encoding to this feature? \n", "- How about encoding this as a text feature? " ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "artist\n", "Drake 14\n", "Disclosure 12\n", "Rick Ross 11\n", "WALK THE MOON 10\n", "Crystal Castles 8\n", " ..\n", "Classixx 1\n", "Jordan Feliz 1\n", "Travis Hayes 1\n", "The Silvertones 1\n", "Phil Woods 1\n", "Name: count, Length: 1131, dtype: int64" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[\"artist\"].value_counts()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "numeric_feats = ['acousticness', 'danceability', 'energy',\n", " 'instrumentalness', 'liveness', 'loudness',\n", " 'speechiness', 'tempo', 'valence']\n", "categorical_feats = ['time_signature', 'key']\n", "passthrough_feats = ['mode']\n", "artist_cat_feat = ['artist']\n", "text_feat = 'song_title' # Define the text feature" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "```{important}\n", "Note that unlike other feature types we are defining `text_feature` as a string and not as a list. \n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Column transformer without `song_title` and `artist` features" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "preprocessor_no_text = make_column_transformer(\n", " (StandardScaler(), numeric_feats), \n", " (\"passthrough\", passthrough_feats), \n", " (OneHotEncoder(handle_unknown = \"ignore\"), categorical_feats), \n", ")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Visualizing the transformed data " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(1613, 26)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "transformed_no_text = preprocessor_no_text.fit_transform(X_train)\n", "transformed_no_text.shape" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
ColumnTransformer(transformers=[('standardscaler', StandardScaler(),\n",
       "                                 ['acousticness', 'danceability', 'energy',\n",
       "                                  'instrumentalness', 'liveness', 'loudness',\n",
       "                                  'speechiness', 'tempo', 'valence']),\n",
       "                                ('passthrough', 'passthrough', ['mode']),\n",
       "                                ('onehotencoder',\n",
       "                                 OneHotEncoder(handle_unknown='ignore'),\n",
       "                                 ['time_signature', 'key'])])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "ColumnTransformer(transformers=[('standardscaler', StandardScaler(),\n", " ['acousticness', 'danceability', 'energy',\n", " 'instrumentalness', 'liveness', 'loudness',\n", " 'speechiness', 'tempo', 'valence']),\n", " ('passthrough', 'passthrough', ['mode']),\n", " ('onehotencoder',\n", " OneHotEncoder(handle_unknown='ignore'),\n", " ['time_signature', 'key'])])" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "preprocessor_no_text" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['time_signature_1.0',\n", " 'time_signature_3.0',\n", " 'time_signature_4.0',\n", " 'time_signature_5.0',\n", " 'key_0',\n", " 'key_1',\n", " 'key_2',\n", " 'key_3',\n", " 'key_4',\n", " 'key_5',\n", " 'key_6',\n", " 'key_7',\n", " 'key_8',\n", " 'key_9',\n", " 'key_10',\n", " 'key_11']" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ohe_feat_names = preprocessor_no_text.named_transformers_[\"onehotencoder\"].get_feature_names_out().tolist()\n", "ohe_feat_names" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "feat_names = numeric_feats + passthrough_feats + ohe_feat_names" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
acousticnessdanceabilityenergyinstrumentalnesslivenessloudnessspeechinesstempovalencemode...key_2key_3key_4key_5key_6key_7key_8key_9key_10key_11
0-0.697633-0.194548-0.318116-0.492359-0.7378980.395794-0.617752-0.293827-0.9081490.0...0.00.00.00.00.00.00.00.01.00.0
1-0.2762910.295726-0.7955520.598355-0.438792-0.0523942.728394-0.802595-1.8612381.0...0.00.00.00.00.00.00.00.00.00.0
2-0.5995401.110806-0.946819-0.492917-0.399607-0.8794572.5349090.1912740.5758701.0...0.00.00.00.00.01.00.00.00.00.0
3-0.3071501.809445-1.722063-0.492168-0.763368-1.460798-0.608647-0.8396161.8253580.0...0.00.00.00.01.00.00.00.00.00.0
4-0.6346420.4918351.0574682.723273-0.458384-0.175645-0.653035-0.074294-0.7544910.0...0.00.00.00.01.00.00.00.00.00.0
..................................................................
1608-0.711944-0.2006761.185100-0.483229-0.3930770.9411762.751157-1.743639-1.1588561.0...0.00.00.00.00.00.00.00.00.00.0
1609-0.715953-0.5009691.383637-0.4923800.4820380.9242390.918743-0.186361-0.2692530.0...0.00.00.00.00.00.00.00.01.00.0
16101.224228-1.267021-0.157395-0.4929170.1946870.688940-0.626857-2.284681-0.6291381.0...0.00.00.00.00.01.00.00.00.00.0
16112.0034191.699134-2.459489-0.4810320.802042-1.875632-0.037298-0.6310641.2956401.0...0.00.01.00.00.00.00.00.00.00.0
16121.687114-0.899316-2.4831250.180574-0.556344-2.585697-0.584746-0.141104-1.7730861.0...1.00.00.00.00.00.00.00.00.00.0
\n", "

1613 rows × 26 columns

\n", "
" ], "text/plain": [ " acousticness danceability energy instrumentalness liveness \\\n", "0 -0.697633 -0.194548 -0.318116 -0.492359 -0.737898 \n", "1 -0.276291 0.295726 -0.795552 0.598355 -0.438792 \n", "2 -0.599540 1.110806 -0.946819 -0.492917 -0.399607 \n", "3 -0.307150 1.809445 -1.722063 -0.492168 -0.763368 \n", "4 -0.634642 0.491835 1.057468 2.723273 -0.458384 \n", "... ... ... ... ... ... \n", "1608 -0.711944 -0.200676 1.185100 -0.483229 -0.393077 \n", "1609 -0.715953 -0.500969 1.383637 -0.492380 0.482038 \n", "1610 1.224228 -1.267021 -0.157395 -0.492917 0.194687 \n", "1611 2.003419 1.699134 -2.459489 -0.481032 0.802042 \n", "1612 1.687114 -0.899316 -2.483125 0.180574 -0.556344 \n", "\n", " loudness speechiness tempo valence mode ... key_2 key_3 \\\n", "0 0.395794 -0.617752 -0.293827 -0.908149 0.0 ... 0.0 0.0 \n", "1 -0.052394 2.728394 -0.802595 -1.861238 1.0 ... 0.0 0.0 \n", "2 -0.879457 2.534909 0.191274 0.575870 1.0 ... 0.0 0.0 \n", "3 -1.460798 -0.608647 -0.839616 1.825358 0.0 ... 0.0 0.0 \n", "4 -0.175645 -0.653035 -0.074294 -0.754491 0.0 ... 0.0 0.0 \n", "... ... ... ... ... ... ... ... ... \n", "1608 0.941176 2.751157 -1.743639 -1.158856 1.0 ... 0.0 0.0 \n", "1609 0.924239 0.918743 -0.186361 -0.269253 0.0 ... 0.0 0.0 \n", "1610 0.688940 -0.626857 -2.284681 -0.629138 1.0 ... 0.0 0.0 \n", "1611 -1.875632 -0.037298 -0.631064 1.295640 1.0 ... 0.0 0.0 \n", "1612 -2.585697 -0.584746 -0.141104 -1.773086 1.0 ... 1.0 0.0 \n", "\n", " key_4 key_5 key_6 key_7 key_8 key_9 key_10 key_11 \n", "0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 \n", "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "2 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 \n", "3 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 \n", "4 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 \n", "... ... ... ... ... ... ... ... ... \n", "1608 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "1609 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 \n", "1610 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 \n", "1611 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "1612 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "\n", "[1613 rows x 26 columns]" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.DataFrame(transformed_no_text, columns=feat_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Building models" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.000 (+/- 0.000)0.000 (+/- 0.000)0.508 (+/- 0.001)0.508 (+/- 0.000)
Decision Tree (no_text)0.016 (+/- 0.000)0.003 (+/- 0.000)0.688 (+/- 0.023)1.000 (+/- 0.000)
KNN (no_text)0.005 (+/- 0.001)0.015 (+/- 0.020)0.676 (+/- 0.028)0.788 (+/- 0.009)
SVM (no_text)0.054 (+/- 0.004)0.021 (+/- 0.001)0.737 (+/- 0.017)0.806 (+/- 0.011)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.000 (+/- 0.000) 0.000 (+/- 0.000) \n", "Decision Tree (no_text) 0.016 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN (no_text) 0.005 (+/- 0.001) 0.015 (+/- 0.020) \n", "SVM (no_text) 0.054 (+/- 0.004) 0.021 (+/- 0.001) \n", "\n", " test_score train_score \n", "dummy 0.508 (+/- 0.001) 0.508 (+/- 0.000) \n", "Decision Tree (no_text) 0.688 (+/- 0.023) 1.000 (+/- 0.000) \n", "KNN (no_text) 0.676 (+/- 0.028) 0.788 (+/- 0.009) \n", "SVM (no_text) 0.737 (+/- 0.017) 0.806 (+/- 0.011) " ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "models = {\n", " \"Decision Tree\": DecisionTreeClassifier(),\n", " \"KNN\": KNeighborsClassifier(),\n", " \"SVM\": SVC() \n", "}\n", "\n", "for (name, model) in models.items():\n", " pipe_model = make_pipeline(preprocessor_no_text, model)\n", " results[name + \" (no_text)\"] = mean_std_cross_val_scores(pipe_model, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Incorporating \"song_title\" feature" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's incorporate bag-of-words representation of \"song_title\" feature in our column transformer. " ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['acousticness',\n", " 'danceability',\n", " 'energy',\n", " 'instrumentalness',\n", " 'liveness',\n", " 'loudness',\n", " 'speechiness',\n", " 'tempo',\n", " 'valence']" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numeric_feats" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'song_title'" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text_feat" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "preprocessor = make_column_transformer(\n", " (StandardScaler(), numeric_feats), \n", " (\"passthrough\", passthrough_feats), \n", " (OneHotEncoder(handle_unknown = \"ignore\"), categorical_feats), \n", " (CountVectorizer(stop_words=\"english\"), text_feat)\n", ")" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "# Transform the data\n", "transformed = preprocessor.fit_transform(X_train)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
ColumnTransformer(transformers=[('standardscaler', StandardScaler(),\n",
       "                                 ['acousticness', 'danceability', 'energy',\n",
       "                                  'instrumentalness', 'liveness', 'loudness',\n",
       "                                  'speechiness', 'tempo', 'valence']),\n",
       "                                ('passthrough', 'passthrough', ['mode']),\n",
       "                                ('onehotencoder',\n",
       "                                 OneHotEncoder(handle_unknown='ignore'),\n",
       "                                 ['time_signature', 'key']),\n",
       "                                ('countvectorizer',\n",
       "                                 CountVectorizer(stop_words='english'),\n",
       "                                 'song_title')])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "ColumnTransformer(transformers=[('standardscaler', StandardScaler(),\n", " ['acousticness', 'danceability', 'energy',\n", " 'instrumentalness', 'liveness', 'loudness',\n", " 'speechiness', 'tempo', 'valence']),\n", " ('passthrough', 'passthrough', ['mode']),\n", " ('onehotencoder',\n", " OneHotEncoder(handle_unknown='ignore'),\n", " ['time_signature', 'key']),\n", " ('countvectorizer',\n", " CountVectorizer(stop_words='english'),\n", " 'song_title')])" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "preprocessor" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "# Get the vocabulary\n", "vocab = preprocessor.named_transformers_['countvectorizer'].get_feature_names_out()" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "1910" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "column_names = numeric_feats + passthrough_feats + ohe_feat_names + vocab.tolist()\n", "len(column_names)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
acousticnessdanceabilityenergyinstrumentalnesslivenessloudnessspeechinesstempovalencemode...너와의루시아변명여기이곳에서이대로있어줘요있을까장가갈지금
0-0.697633-0.194548-0.318116-0.492359-0.7378980.395794-0.617752-0.293827-0.9081490.0...0.00.00.00.00.00.00.00.00.00.0
1-0.2762910.295726-0.7955520.598355-0.438792-0.0523942.728394-0.802595-1.8612381.0...0.00.00.00.00.00.00.00.00.00.0
2-0.5995401.110806-0.946819-0.492917-0.399607-0.8794572.5349090.1912740.5758701.0...0.00.00.00.00.00.00.00.00.00.0
3-0.3071501.809445-1.722063-0.492168-0.763368-1.460798-0.608647-0.8396161.8253580.0...0.00.00.00.00.00.00.00.00.00.0
4-0.6346420.4918351.0574682.723273-0.458384-0.175645-0.653035-0.074294-0.7544910.0...0.00.00.00.00.00.00.00.00.00.0
..................................................................
1608-0.711944-0.2006761.185100-0.483229-0.3930770.9411762.751157-1.743639-1.1588561.0...0.00.00.00.00.00.00.00.00.00.0
1609-0.715953-0.5009691.383637-0.4923800.4820380.9242390.918743-0.186361-0.2692530.0...0.00.00.00.00.00.00.00.00.00.0
16101.224228-1.267021-0.157395-0.4929170.1946870.688940-0.626857-2.284681-0.6291381.0...0.00.00.00.00.00.00.00.00.00.0
16112.0034191.699134-2.459489-0.4810320.802042-1.875632-0.037298-0.6310641.2956401.0...0.00.00.00.00.00.00.01.01.00.0
16121.687114-0.899316-2.4831250.180574-0.556344-2.585697-0.584746-0.141104-1.7730861.0...0.00.00.00.00.00.00.00.00.00.0
\n", "

1613 rows × 1910 columns

\n", "
" ], "text/plain": [ " acousticness danceability energy instrumentalness liveness \\\n", "0 -0.697633 -0.194548 -0.318116 -0.492359 -0.737898 \n", "1 -0.276291 0.295726 -0.795552 0.598355 -0.438792 \n", "2 -0.599540 1.110806 -0.946819 -0.492917 -0.399607 \n", "3 -0.307150 1.809445 -1.722063 -0.492168 -0.763368 \n", "4 -0.634642 0.491835 1.057468 2.723273 -0.458384 \n", "... ... ... ... ... ... \n", "1608 -0.711944 -0.200676 1.185100 -0.483229 -0.393077 \n", "1609 -0.715953 -0.500969 1.383637 -0.492380 0.482038 \n", "1610 1.224228 -1.267021 -0.157395 -0.492917 0.194687 \n", "1611 2.003419 1.699134 -2.459489 -0.481032 0.802042 \n", "1612 1.687114 -0.899316 -2.483125 0.180574 -0.556344 \n", "\n", " loudness speechiness tempo valence mode ... 너와의 루시아 변명 \\\n", "0 0.395794 -0.617752 -0.293827 -0.908149 0.0 ... 0.0 0.0 0.0 \n", "1 -0.052394 2.728394 -0.802595 -1.861238 1.0 ... 0.0 0.0 0.0 \n", "2 -0.879457 2.534909 0.191274 0.575870 1.0 ... 0.0 0.0 0.0 \n", "3 -1.460798 -0.608647 -0.839616 1.825358 0.0 ... 0.0 0.0 0.0 \n", "4 -0.175645 -0.653035 -0.074294 -0.754491 0.0 ... 0.0 0.0 0.0 \n", "... ... ... ... ... ... ... ... ... ... \n", "1608 0.941176 2.751157 -1.743639 -1.158856 1.0 ... 0.0 0.0 0.0 \n", "1609 0.924239 0.918743 -0.186361 -0.269253 0.0 ... 0.0 0.0 0.0 \n", "1610 0.688940 -0.626857 -2.284681 -0.629138 1.0 ... 0.0 0.0 0.0 \n", "1611 -1.875632 -0.037298 -0.631064 1.295640 1.0 ... 0.0 0.0 0.0 \n", "1612 -2.585697 -0.584746 -0.141104 -1.773086 1.0 ... 0.0 0.0 0.0 \n", "\n", " 여기 이곳에서 이대로 있어줘요 있을까 장가갈 지금 \n", "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 \n", "2 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 \n", "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "... ... ... ... ... ... ... ... \n", "1608 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "1609 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "1610 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "1611 0.0 0.0 0.0 0.0 1.0 1.0 0.0 \n", "1612 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", "\n", "[1613 rows x 1910 columns]" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.DataFrame(transformed.toarray(), columns=column_names)\n", "df" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Visualizing the vocabulary " ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['000', '10', '100', '10cm', '11', '112', '12', '1208', '144', '18'],\n", " dtype=object)" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[0:10]" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['duele', 'duet', 'duke', 'dustland', 'dutchie', 'dynamite',\n", " 'earth', 'easy', 'eazy', 'echelon'], dtype=object)" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[500:510]" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['wide', 'wifey', 'wild', 'wildcard', 'wildfire', 'wiley',\n", " 'willing', 'win', 'wind', 'window'], dtype=object)" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[1800:1810]" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['000', 'ap', 'blind', 'cha', 'dallask', 'duele', 'flashlight',\n", " 'grace', 'icarus', 'lafa', 'making', 'neck', 'pharaohs', 'redeem',\n", " 'seeb', 'soundtrack', 'talons', 'unanswered', 'wide'], dtype=object)" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[0::100]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "Let's find songs containing the word _earth_ in them. " ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "506" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "earth_index_vocab = np.where(vocab == \"earth\")[0][0]\n", "earth_index_vocab" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "532" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "earth_index_in_df = len(numeric_feats) + len(passthrough_feats) + len(ohe_feat_names) + earth_index_vocab\n", "earth_index_in_df" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
dutchiedynamiteeartheasy
3800.00.01.00.0
6390.00.01.00.0
\n", "
" ], "text/plain": [ " dutchie dynamite earth easy\n", "380 0.0 0.0 1.0 0.0\n", "639 0.0 0.0 1.0 0.0" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "earth_songs = df[df.iloc[:, earth_index_in_df] == 1]\n", "earth_songs.iloc[:, earth_index_in_df - 2 : earth_index_in_df + 2]" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Index([380, 639], dtype='int64')" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "earth_songs.index" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1851 Softest Place On Earth\n", "1948 Earth Song - Remastered Version\n", "Name: song_title, dtype: object" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train.iloc[earth_songs.index][\"song_title\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Model building " ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.000 (+/- 0.000)0.000 (+/- 0.000)0.508 (+/- 0.001)0.508 (+/- 0.000)
Decision Tree (no_text)0.016 (+/- 0.000)0.003 (+/- 0.000)0.688 (+/- 0.023)1.000 (+/- 0.000)
KNN (no_text)0.005 (+/- 0.001)0.015 (+/- 0.020)0.676 (+/- 0.028)0.788 (+/- 0.009)
SVM (no_text)0.054 (+/- 0.004)0.021 (+/- 0.001)0.737 (+/- 0.017)0.806 (+/- 0.011)
Decision Tree (text)0.035 (+/- 0.002)0.005 (+/- 0.001)0.700 (+/- 0.027)1.000 (+/- 0.000)
KNN (text)0.012 (+/- 0.002)0.031 (+/- 0.004)0.682 (+/- 0.028)0.786 (+/- 0.010)
SVM (text)0.059 (+/- 0.003)0.014 (+/- 0.001)0.733 (+/- 0.027)0.866 (+/- 0.004)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.000 (+/- 0.000) 0.000 (+/- 0.000) \n", "Decision Tree (no_text) 0.016 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN (no_text) 0.005 (+/- 0.001) 0.015 (+/- 0.020) \n", "SVM (no_text) 0.054 (+/- 0.004) 0.021 (+/- 0.001) \n", "Decision Tree (text) 0.035 (+/- 0.002) 0.005 (+/- 0.001) \n", "KNN (text) 0.012 (+/- 0.002) 0.031 (+/- 0.004) \n", "SVM (text) 0.059 (+/- 0.003) 0.014 (+/- 0.001) \n", "\n", " test_score train_score \n", "dummy 0.508 (+/- 0.001) 0.508 (+/- 0.000) \n", "Decision Tree (no_text) 0.688 (+/- 0.023) 1.000 (+/- 0.000) \n", "KNN (no_text) 0.676 (+/- 0.028) 0.788 (+/- 0.009) \n", "SVM (no_text) 0.737 (+/- 0.017) 0.806 (+/- 0.011) \n", "Decision Tree (text) 0.700 (+/- 0.027) 1.000 (+/- 0.000) \n", "KNN (text) 0.682 (+/- 0.028) 0.786 (+/- 0.010) \n", "SVM (text) 0.733 (+/- 0.027) 0.866 (+/- 0.004) " ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "models = {\n", " \"Decision Tree\": DecisionTreeClassifier(),\n", " \"KNN\": KNeighborsClassifier(),\n", " \"SVM\": SVC() \n", "}\n", "\n", "for (name, model) in models.items():\n", " pipe_model = make_pipeline(preprocessor, model)\n", " results[name + \" (text)\"] = mean_std_cross_val_scores(pipe_model, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results).T" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "- Not a big difference in the results. \n", "- Seems like there is more overfitting when we included the `song_title` feature.\n", "- The training score of SVC is much higher when we include all features. Hyperparameter optimization of `C` and `gamma` may help. " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "- What about the `artist` column?\n", "- Does it make sense to apply BOW encoding to it? \n", "- Let's look at the distribution of values in the `artist` column. " ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "artist\n", "Drake 14\n", "Disclosure 12\n", "Rick Ross 11\n", "WALK THE MOON 10\n", "Crystal Castles 8\n", " ..\n", "Classixx 1\n", "Jordan Feliz 1\n", "Travis Hayes 1\n", "The Silvertones 1\n", "Phil Woods 1\n", "Name: count, Length: 1131, dtype: int64" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train['artist'].value_counts()" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "artist\n", "Drake 14\n", "Disclosure 12\n", "Rick Ross 11\n", "WALK THE MOON 10\n", "Crystal Castles 8\n", "Big Time Rush 8\n", "FIDLAR 8\n", "Fall Out Boy 8\n", "Demi Lovato 7\n", "Kanye West 7\n", "Kina Grannis 7\n", "Backstreet Boys 7\n", "Beach House 6\n", "Young Thug 6\n", "*NSYNC 6\n", "Name: count, dtype: int64" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "most_frequent = X_train[\"artist\"].value_counts().iloc[:15]\n", "most_frequent" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "preprocessor_artist = make_column_transformer(\n", " (StandardScaler(), numeric_feats), \n", " (\"passthrough\", passthrough_feats), \n", " (OneHotEncoder(handle_unknown = \"ignore\"), categorical_feats),\n", " (OneHotEncoder(dtype=int, handle_unknown=\"ignore\", categories=[most_frequent.index.values]), artist_cat_feat),\n", " (CountVectorizer(max_features = 100, stop_words=\"english\"), text_feat)\n", ")" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.000 (+/- 0.000)0.000 (+/- 0.000)0.508 (+/- 0.001)0.508 (+/- 0.000)
Decision Tree (no_text)0.016 (+/- 0.000)0.003 (+/- 0.000)0.688 (+/- 0.023)1.000 (+/- 0.000)
KNN (no_text)0.005 (+/- 0.001)0.015 (+/- 0.020)0.676 (+/- 0.028)0.788 (+/- 0.009)
SVM (no_text)0.054 (+/- 0.004)0.021 (+/- 0.001)0.737 (+/- 0.017)0.806 (+/- 0.011)
Decision Tree (text)0.035 (+/- 0.002)0.005 (+/- 0.001)0.700 (+/- 0.027)1.000 (+/- 0.000)
KNN (text)0.012 (+/- 0.002)0.031 (+/- 0.004)0.682 (+/- 0.028)0.786 (+/- 0.010)
SVM (text)0.059 (+/- 0.003)0.014 (+/- 0.001)0.733 (+/- 0.027)0.866 (+/- 0.004)
Decision Tree (all)0.028 (+/- 0.001)0.005 (+/- 0.001)0.684 (+/- 0.035)1.000 (+/- 0.000)
KNN (all)0.012 (+/- 0.000)0.026 (+/- 0.001)0.681 (+/- 0.032)0.792 (+/- 0.008)
SVM (all)0.052 (+/- 0.004)0.013 (+/- 0.000)0.741 (+/- 0.027)0.833 (+/- 0.006)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.000 (+/- 0.000) 0.000 (+/- 0.000) \n", "Decision Tree (no_text) 0.016 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN (no_text) 0.005 (+/- 0.001) 0.015 (+/- 0.020) \n", "SVM (no_text) 0.054 (+/- 0.004) 0.021 (+/- 0.001) \n", "Decision Tree (text) 0.035 (+/- 0.002) 0.005 (+/- 0.001) \n", "KNN (text) 0.012 (+/- 0.002) 0.031 (+/- 0.004) \n", "SVM (text) 0.059 (+/- 0.003) 0.014 (+/- 0.001) \n", "Decision Tree (all) 0.028 (+/- 0.001) 0.005 (+/- 0.001) \n", "KNN (all) 0.012 (+/- 0.000) 0.026 (+/- 0.001) \n", "SVM (all) 0.052 (+/- 0.004) 0.013 (+/- 0.000) \n", "\n", " test_score train_score \n", "dummy 0.508 (+/- 0.001) 0.508 (+/- 0.000) \n", "Decision Tree (no_text) 0.688 (+/- 0.023) 1.000 (+/- 0.000) \n", "KNN (no_text) 0.676 (+/- 0.028) 0.788 (+/- 0.009) \n", "SVM (no_text) 0.737 (+/- 0.017) 0.806 (+/- 0.011) \n", "Decision Tree (text) 0.700 (+/- 0.027) 1.000 (+/- 0.000) \n", "KNN (text) 0.682 (+/- 0.028) 0.786 (+/- 0.010) \n", "SVM (text) 0.733 (+/- 0.027) 0.866 (+/- 0.004) \n", "Decision Tree (all) 0.684 (+/- 0.035) 1.000 (+/- 0.000) \n", "KNN (all) 0.681 (+/- 0.032) 0.792 (+/- 0.008) \n", "SVM (all) 0.741 (+/- 0.027) 0.833 (+/- 0.006) " ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "models = {\n", " \"Decision Tree\": DecisionTreeClassifier(),\n", " \"KNN\": KNeighborsClassifier(),\n", " \"SVM\": SVC() \n", "}\n", "\n", "for (name, model) in models.items():\n", " pipe_model = make_pipeline(preprocessor_artist, model)\n", " results[name + \" (all)\"] = mean_std_cross_val_scores(pipe_model, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tiny bit improvement in the mean CV scores but we are still overfitting. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "



" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## (Optional) Incorporating text features in the restaurant survey dataset" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Do you recall [the restaurants survey](https://ubc.ca1.qualtrics.com/jfe/form/SV_73VuZiuwM1eDVrw) you completed at the start of the course?\n", "\n", "Let's use that data for this demo. You'll find a [wrangled version](https://github.ubc.ca/MDS-2023-24/DSCI_571_sup-learn-1_students/blob/master/lectures/data/cleaned_restaurant_data.csv) in the course repository." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "df = pd.read_csv('../data/cleaned_restaurant_data.csv')" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
north_americaeat_out_freqagen_peoplepricefood_typenoise_levelgood_servercommentsrestaurant_nametarget
0Yes3.02910.0120.0ItalianmediumYesAmbienceNaNdislike
1Yes2.0233.020.0Canadian/Americanno musicNofood tastes badNaNdislike
2Yes2.02120.015.0ChinesemediumYesbad foodNaNdislike
3No2.02414.018.0OthermediumNoOverall vibe on the restaurantNaNdislike
4Yes5.02330.020.0ChinesemediumYesA bad dayNaNdislike
....................................
959No10.022NaNNaNNaNNaNNaNNaNNaNlike
960Yes1.020NaNNaNNaNNaNNaNNaNNaNlike
961No1.02240.050.0ChinesemediumYesThe self service sauce table is very clean and the sauces were always filled up.Haidilaolike
962Yes3.021NaNNaNNaNNaNNaNNaNNaNlike
963Yes3.02720.022.0OthermediumYesLots of meat that was very soft and tasty. Hearty and amazing broth. Good noodle thickness and consistencyUno Beef Noodlelike
\n", "

964 rows × 11 columns

\n", "
" ], "text/plain": [ " north_america eat_out_freq age n_people price food_type \\\n", "0 Yes 3.0 29 10.0 120.0 Italian \n", "1 Yes 2.0 23 3.0 20.0 Canadian/American \n", "2 Yes 2.0 21 20.0 15.0 Chinese \n", "3 No 2.0 24 14.0 18.0 Other \n", "4 Yes 5.0 23 30.0 20.0 Chinese \n", ".. ... ... ... ... ... ... \n", "959 No 10.0 22 NaN NaN NaN \n", "960 Yes 1.0 20 NaN NaN NaN \n", "961 No 1.0 22 40.0 50.0 Chinese \n", "962 Yes 3.0 21 NaN NaN NaN \n", "963 Yes 3.0 27 20.0 22.0 Other \n", "\n", " noise_level good_server \\\n", "0 medium Yes \n", "1 no music No \n", "2 medium Yes \n", "3 medium No \n", "4 medium Yes \n", ".. ... ... \n", "959 NaN NaN \n", "960 NaN NaN \n", "961 medium Yes \n", "962 NaN NaN \n", "963 medium Yes \n", "\n", " comments \\\n", "0 Ambience \n", "1 food tastes bad \n", "2 bad food \n", "3 Overall vibe on the restaurant \n", "4 A bad day \n", ".. ... \n", "959 NaN \n", "960 NaN \n", "961 The self service sauce table is very clean and the sauces were always filled up. \n", "962 NaN \n", "963 Lots of meat that was very soft and tasty. Hearty and amazing broth. Good noodle thickness and consistency \n", "\n", " restaurant_name target \n", "0 NaN dislike \n", "1 NaN dislike \n", "2 NaN dislike \n", "3 NaN dislike \n", "4 NaN dislike \n", ".. ... ... \n", "959 NaN like \n", "960 NaN like \n", "961 Haidilao like \n", "962 NaN like \n", "963 Uno Beef Noodle like \n", "\n", "[964 rows x 11 columns]" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
eat_out_freqagen_peopleprice
count964.000000964.0000006.960000e+02696.000000
mean2.58518723.9751041.439254e+041472.179152
std2.2464864.5567163.790481e+0537903.575636
min0.00000010.000000-2.000000e+000.000000
25%1.00000021.0000001.000000e+0118.000000
50%2.00000022.0000002.000000e+0125.000000
75%3.00000026.0000003.000000e+0140.000000
max15.00000046.0000001.000000e+071000000.000000
\n", "
" ], "text/plain": [ " eat_out_freq age n_people price\n", "count 964.000000 964.000000 6.960000e+02 696.000000\n", "mean 2.585187 23.975104 1.439254e+04 1472.179152\n", "std 2.246486 4.556716 3.790481e+05 37903.575636\n", "min 0.000000 10.000000 -2.000000e+00 0.000000\n", "25% 1.000000 21.000000 1.000000e+01 18.000000\n", "50% 2.000000 22.000000 2.000000e+01 25.000000\n", "75% 3.000000 26.000000 3.000000e+01 40.000000\n", "max 15.000000 46.000000 1.000000e+07 1000000.000000" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Are there any unusual values in this data that you notice?\n", "Let's get rid of these outliers. " ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(942, 11)" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "upperbound_price = 200\n", "lowerbound_people = 1\n", "df = df[~(df['price'] > 200)]\n", "restaurant_df = df[~(df['n_people'] < lowerbound_people)]\n", "restaurant_df.shape" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
eat_out_freqagen_peopleprice
count942.000000942.000000674.000000674.000000
mean2.59805723.99256924.97329434.023279
std2.2577874.58257022.01666029.018622
min0.00000010.0000001.0000000.000000
25%1.00000021.00000010.00000018.000000
50%2.00000022.00000020.00000025.000000
75%3.00000026.00000030.00000040.000000
max15.00000046.000000200.000000200.000000
\n", "
" ], "text/plain": [ " eat_out_freq age n_people price\n", "count 942.000000 942.000000 674.000000 674.000000\n", "mean 2.598057 23.992569 24.973294 34.023279\n", "std 2.257787 4.582570 22.016660 29.018622\n", "min 0.000000 10.000000 1.000000 0.000000\n", "25% 1.000000 21.000000 10.000000 18.000000\n", "50% 2.000000 22.000000 20.000000 25.000000\n", "75% 3.000000 26.000000 30.000000 40.000000\n", "max 15.000000 46.000000 200.000000 200.000000" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurant_df.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data splitting \n", "\n", "We aim to predict whether a restaurant is liked or disliked." ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [], "source": [ "# Separate `X` and `y`. \n", "\n", "X = restaurant_df.drop(columns=['target'])\n", "y = restaurant_df['target']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below I'm perturbing this data just to demonstrate a few concepts. Don't do it in real life. " ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "X.at[459, 'food_type'] = 'Quebecois'\n", "X['price'] = X['price'] * 100" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [], "source": [ "# Split the data\n", "\n", "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### EDA " ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "data": { "image/png": "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eubvnfYJAADV6fTp09q+fbsk6a9//asmTJjgtJyz8YiKfp9W9vuwaB2HDx/Wr371qzs1GUAlcWUfphQaGup4X54uarf7+uuvJUm/+c1vnCb6kopd2Xe34OBgSdKxY8fKLFf0Pn17jJ23t7ckKScnp9T4H374oXINrGEaN26sZs2aSarc8SD9z/Y7depUmfcN2u9lBACgJrKf40hyDFLsjLPznHbt2jneF+2O70xp8+/2HA1A+ZHsw5QeeeQRWa1WSbdGoq8oe5f+goKCUsv8v//3/8qsw9PT8451VFbfvn0l3RqI0H4vujOffPKJJKlJkyaOQW7s/P39Jcnp/XR227ZtK7MdVbmOrhYZGSlJ2rRpk65du1bhePtowTdv3tTWrVtLLbdhw4bKNRAAADcoettiad/f6enp+uc//1liepMmTRxj02zatKnUZaSlpZX643dYWJjatGkjSfr444/L3W4AFUeyD1Py9fXVCy+8IElatWqVVq1aVWb53NzcYoPO2Ad7++yzz5wO0Pe3v/1NO3fuLLPO5s2bS1KZyXhlxcTESLr1JT1x4kSnbdywYYOjm15MTEyJHgoPPfSQpFuj1Du7gn/x4kW9+eabZbajKtfR1WJjYyVJWVlZio6OLnay48ztVyQef/xxx/q+9tprTsdASEpK0vr1613UYgAAXK/ogLbOEnabzaYXXnih1B8C7I/bO3TokP7+97+XmF9YWKg//vGPpS7fYrE4vpP37Nmj+fPnl9negoICnTx5sswyAJwj2Ydp/elPf1Lnzp1lGIb+4z/+Q//f//f/6YsvvtClS5eUmZmp06dPKyEhQX/4wx8UFBSktWvXOmLtj6z78ssvNWbMGB06dEgZGRn69ttvFRsbq5iYmDuOum5/CsCnn36q3bt365dfflF+fr7y8/Pv+pn3Xbt2ddxjt379ekVFRSk5OVkZGRk6efKk3nzzTY0ZM0aSFBgYqNdff71EHePGjZOHh4dycnL06KOPKjExUdeuXdP58+e1YsUK9erVq8xB74qu4549e/Tpp58qKyvLZevoat27d9drr70mSUpISFD37t21YsUKnTp1StevX9fFixeVnJys+Ph49ejRo8QIwVarVQsXLpR0q6v+I488ou3bt+vq1as6d+6c/vznP+uJJ54ocbsEAAA1Sffu3R0J/8svv6ylS5fq1KlTunLlirZv365+/fppx44dpZ7n/P73v3d0xY+JidHs2bN18uRJXbt2TXv37tXjjz+uhIQEtW/fvtQ2TJw4Uf3795d06wf0xx9/XJs3b9YPP/yg69ev69y5c/r88881depUhYSElPspOgBuYwAmdunSJaNfv36GpDu+li5d6oj75ZdfjB49epRaNiwszNi/f7/jc1JSUollf/fdd4a3t7fT+FmzZt31uuXl5Rljxowpc52Cg4ON1NTUUutYuHBhqbFt27Y1UlNTHZ8//vjjEvE//vij0aJFC6fx48aNu+t1nDVrliHJaNeuXallxo0bZ0gy+vXrd8f6CgsLjblz5xr169e/4/HQrVs3p3VMnTq11JjAwEBj165dZR4XAABUty+//NLw8fEp9fts0qRJZX4Hf/fdd0br1q1LjZ81a5YxduxYQ5IxcOBAp23IysoyRo0aVa5ztNjY2CreIoA5cWUfptaqVSvt3r1bmzdv1jPPPKPg4GD5+PjI09NTLVu21K9//WvFxcXpm2++0R/+8AdHXIMGDbR7927NmjVLnTp1ktVqVePGjfXggw/qT3/6k/7973+rZcuWZS67c+fOSk5O1m9+8xu1bdvWcX+7q3h5eWnlypXaunWrRowYoTZt2sjT01N+fn56+OGHtXDhQh07dqzM5+BOnjxZCQkJGjBggPz8/OTt7a2OHTtq8uTJ+vrrr+/4DN0WLVpo3759eu6559S+fXvHOAk1lcVi0euvv660tDRNnjxZ3bp1k5+fn+rXry9fX1916dJFMTExSkhI0N69e53WsWDBAm3btk2DBw9WkyZNHNvsj3/8o1JSUsq8kgEAQE3wyCOP6N///rdGjx6tli1bytPTU61bt1ZUVJQ2b96s+Pj4MuM7d+6so0eP6o9//KNCQ0NltVrVokULDR48WJs3b1ZcXJxjNH5fX1+ndTRq1Ehr167VV199pfHjx+vee+9Vw4YN5eHhoWbNmqlXr16aMmWK9u3bp0WLFrl8GwB1gcUwnNzsCwColLNnzzoS/qSkJEc3RQAA6pKuXbvq22+/1R/+8ActXbq0upsD1Elc2QcAAADgMmfOnHE8ui88PLyaWwPUXST7AAAAAMqtrEfY5ufn6/e//70Mw5C3t7eGDRvmxpYBKMqjuhsA1EWGYeiXX36pcFz9+vXl4+NTBS2qGvb79SqiXr16d3wKAAAAqD7Lly/XunXrFBMTo1//+tfy9/fXL7/8on//+9+Kj4/Xvn37JEmTJk2Sn59f9TYWqMNI9oFqcO7cuUoN5NavXz/t3r3b9Q2qIo0aNapwTLt27XT27FnXNwYAALjM/v37tX///lLnjxkzRjNnznRjiwDcjmQfAAAAQLn95je/UUFBgXbu3KlTp07pxx9/VH5+vlq2bKlevXpp/PjxGjJkSHU3E6jzGI0fAAAAAACTqZNX9gsLC3XhwgU1atRIFoulupsDAKilDMNQdna22rRpo3r1GPPWrDhvAAC4grvPG+pksn/hwgUFBgZWdzMAACZx/vx5tW3btrqbgSrCeQMAwJXcdd5QJ5N9+6Bh58+fl6+vb6Xrsdls2rFjhyIjI+Xp6emq5qEGYl/XHezrusFV+zkrK0uBgYGVGowStYerzhvchb9jKIrjAXYcC9XP3ecNdTLZt3fB8/X1vetkv0GDBvL19eU/jMmxr+sO9nXd4Or9TNduc3PVeYO78HcMRXE8wI5joeZw13kDNxgCAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmEyFk33DMLRnzx5NnjxZDz30kPz8/OTl5aU2bdpo5MiRSkpKKjN+3759GjZsmFq0aCEfHx/df//9mjNnjnJzc8uMS01N1bPPPit/f395e3srNDRUkyZN0vXr1yu6CgAAAAAAmFqFk/1du3apb9++WrRokQ4cOKBWrVqpS5cuys7O1oYNGzRgwAC98cYbTmNXrlypvn376rPPPpPValXnzp118uRJzZw5U4888ohu3LjhNC4pKUnh4eFatWqVCgoKFBYWpkuXLuntt99WeHi4Ll++XNHVAAAAAADAtCp1Zb9Dhw569913dfXqVZ04cUIpKSnKyMjQ9OnTJUlz587V5s2bi8WdPXtWMTExKigo0MKFC3X+/HmlpKQoLS1N9913nw4cOKApU6aUWF52draefvpp5eTkaOLEifrhhx906NAhpaenq0+fPjp9+rRiYmIqufoAAAAAAJhPhZP9nj17KjU1Vb/97W/VpEkTx3QvLy/NmzdPQ4YMkSQtW7asWFx8fLzy8vIUGRmpyZMny2KxSJLatWun5cuXS5I++OCDElfp33//fV25ckWdO3fW4sWL5enpKUlq1qyZVq1aJQ8PD23ZskUpKSkVXRUAAAAAAEzJo6IBvr6+Zc6PiIjQtm3b9P333zumGYahjRs3SpLTq/C9e/dWp06ddPz4cW3atEn/+Z//6Zi3YcMGSVJ0dLTq169fLC4oKEiDBg3S9u3btW7dOnXr1q2iq1OjBE/b4pJ6zi4Y6pJ6AACA+XH+AQDm5PLR+O0D7fn4+Dimpaen6+LFi5KkPn36OI2zT9+/f79jWn5+vg4dOlThOAAAAAAA6jKXJvuGYWjt2rWSiifnaWlpkiSr1ao2bdo4jQ0JCSlWVrp1n7/NZis2vzxxAAAAAADUZRXuxl+WZcuW6fDhw/Ly8tIrr7zimJ6ZmSlJ8vPzc9yrfzv7/f/2sre/Lzo+wJ3ibpeXl6e8vDzH56ysLEmSzWZz/JhQGfbYu6mjKGt9wyX1uKo9+B+u3teoudjXdYOr9jPHCQAAqKlcluynpKTo5ZdflnRrNP7Q0FDHPHvXfi8vr1LjrVarJCknJ6dEXFmxzuJuN3/+fM2ePbvE9B07dqhBgwalxpVXYmLiXdchSQt7uqQabd261TUVoQRX7WvUfOzruuFu93Npj4wFAACobi5J9s+cOaOoqCjl5uZqzJgxmjRpUrH53t7ekqSbN2+WWof9ynvRe/3tcfbYop/Lirvd9OnTFRsb6/iclZWlwMBARUZG3nHAwbLYbDYlJiYqIiLC8ZSAu9El7vO7rkOSjsYNdkk9+B+u3teoudjXdYOr9rO9pxicMwxDycnJ2rRpk7766isdP35cN27cUPPmzfXwww/r97//vf73//7fJeLi4uKc/khfVGpqqjp16lTqvLlz52rXrl3KzMxUQECAhg8frhkzZsjPz88VqwYAQI1318n+pUuXFBERoYsXL2ro0KFasWJFia769q72169fl2EYTrvy27vhF+2uX/R9Zmam/P39yxV3O6vV6ugBUJSnp6dLTuZdVU9egfNbHCqKBKXquGpfo+ZjX9cNd7ufOUbKtmvXLg0aNEiSVK9ePXXo0EH33HOP0tLStGHDBm3YsEEzZszQnDlznMYHBgYqKCjI6bzSeuYlJSVp6NChysnJUYsWLRQWFqbjx4/r7bff1saNG7V37161atXKNSsIAEANdlcD9F27dk0RERE6deqU+vXrp7Vr1zo98enYsaOkW1fhL1y44LSu06dPFysrScHBwY767PPLEwcAAKqfYRjq0KGD3n33XV29elUnTpxQSkqKMjIyNH36dEm3bv3bvHmz0/jx48drz549Tl/OfgTIzs7W008/rZycHE2cOFE//PCDDh06pPT0dPXp00enT592+ghgAADMqNLJ/s8//6zHHntMR48eVY8ePZSQkFBqV/qgoCC1bt1akpScnOy0jH16r169HNM8PDzUrVu3CscBAIDq17NnT6Wmpuq3v/1tsR54Xl5emjdvnoYMGSLp1gC/rvD+++/rypUr6ty5sxYvXuy4YNCsWTOtWrVKHh4e2rJli1JSUlyyPAAAarJKJft5eXkaNmyY9u/fr7CwMG3fvl2NGjUqtbzFYtHw4cMlSR999FGJ+Xv37tXx48fl6empJ554oti8ESNGSJJWrFihgoKCYvPS09O1c+dOSdLIkSMrsyoAAKCK+Pr6ysOj9DsGIyIiJEnff/+9S5a3YcMGSVJ0dLTq169fbF5QUJDjloJ169a5ZHkAANRkFU72CwoKNHr0aO3atUuhoaFKTExU06ZN7xg3efJkeXl5aceOHYqPj5dh3HrM3Llz5zR+/HhJ0vPPP+/oAWA3YcIENW/eXKmpqYqNjXU85igjI0NjxoxRfn6+hgwZovDw8IquCgAAqEb2p+6U1jMwKSlJTz31lAYMGKBRo0Zp4cKFunTpktOy+fn5OnTokCSpT58+TsvYp+/fv/9umw4AQI1X4QH6/vGPf+jTTz+VdGuwnaeeesppOX9/f61du9bxuX379lq2bJmee+45TZkyRUuWLFHLli119OhR2Ww2hYeHKz4+vkQ9vr6+WrNmjaKiorR06VKtXr1aQUFBSk1N1Y0bNxQcHKzly5dXdDUAAEA1MgzDcZ5QWnL+z3/+s9jn9evXKy4uTu+++66io6OLzTt79qzjgkBISIjT+uzT09LS7qbpAADUChVO9u2PupNufVmW9oXZrl27EtPGjh2rDh06aP78+dq7d6++++47hYSE6JlnntHUqVOdPlpPkgYOHKiDBw86HqNz5MiRYo/RKWskfgAAUPMsW7ZMhw8flpeXl1555ZVi8/z9/fXaa69p+PDhCgkJkY+Pjw4fPqy5c+dq27ZtGj9+vJo1a6bHH3/cEWN/Oo9U+hN67NOLlnUmLy+v2PmO/RGLNpvN8YNCTWZvY3nbaq1vuHS5qFkqejzAvDgWqp+7t32Fk/3o6OgSv6ZXRO/evZWQkFDhuLCwMK1evbrSywUAADVDSkqKXn75ZUm3RuMPDQ0tNv/FF18sEdO7d29t2bJFI0eO1MaNG/Xqq68qKirK8Thf+y0B0q0BAJ2xP4Y3JyenzPbNnz9fs2fPLjF9x44dpT7yryZKTEwsV7mFPV2zvK1bt7qmIlSJ8h4PMD+Ohepz48YNty6vwsk+AABAZZ05c0ZRUVHKzc3VmDFjNGnSpHLHWiwWLViwQBs3btSpU6f07bffqmvXrpJUrHfgzZs3nfYWtF+tL22MALvp06crNjbW8TkrK0uBgYGKjIyUr69vudtbXWw2mxITExUREeH0kci36xL3uUuWezRusEvqgWtV9HiAeXEsVD97TzF3IdkHAABucenSJUVEROjixYsaOnSoVqxY4bgyX1733nuvmjZtqmvXrunkyZOOZL9o1/3MzEz5+/uXiLV337/T7X9Wq9XRC6AoT0/PWnWCXN725hVUbB+UtTzUXLXt+EXV4VioPu7e7pV69B4AAEBFXLt2TRERETp16pT69euntWvXVvqkxx6Xn5/vmBYcHOyYfvr0aadx9ukdO3as1HIBAKhNSPYBAECV+vnnn/XYY4/p6NGj6tGjhxISEu7Ylb40V69e1Y8//ihJatu2rWO6h4eHunXrJklKTk52Gmuf3qtXr0otGwCA2oRkHwAAVJm8vDwNGzZM+/fvV1hYmLZv365GjRpVur7FixfLMAw1btxYPXr0KDZvxIgRkqQVK1aooKCg2Lz09HTt3LlTkjRy5MhKLx8AgNqCZB8AAFSJgoICjR49Wrt27VJoaKgSExPVtGnTMmOOHTuml156SceOHSs2PTc3V/PmzdNbb70lSZo6dWqJUfcnTJig5s2bKzU1VbGxsY5HHGVkZGjMmDHKz8/XkCFDFB4e7sK1BACgZmKAPgAAUCX+8Y9/6NNPP5Uk1atXT0899ZTTcv7+/lq7dq2kW6NFv/fee3rvvffUokULBQUFSZJSU1MdjyyKiYnRtGnTStTj6+urNWvWKCoqSkuXLtXq1asVFBTkiA0ODtby5curYE0BAKh5SPYBAECVsD/qTpLS0tKUlpbmtFy7du0c74ODgzVnzhzt3btXx48f14kTJ3Tz5k21bNlSjz32mJ5//nkNHlz6I94GDhyogwcPau7cudq1a5eOHDmigIAADR8+XDNmzLjjSPwAAJgFyT4AAKgS0dHRio6OrlCMn5+fZsyYcVfLDQsL0+rVq++qDgAAajvu2QcAAAAAwGRI9gEAAAAAMBmSfQAAAAAATIZ79k0oeNoWl9V1dsFQl9UFAAAAAHAPruwDAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmAzJPgAAAAAAJkOyDwAAAACAyZDsAwAAAABgMiT7AAAAAACYDMk+AAAAAAAmQ7IPAAAAAIDJkOwDAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmAzJPgAAAAAAJkOyDwAAAACAyZDsAwAAAABgMiT7AAAAAACYDMk+AAAAAAAmQ7IPAAAAAIDJkOwDAAAAAGAyJPsAAAAAAJgMyT4AAKgShmFoz549mjx5sh566CH5+fnJy8tLbdq00ciRI5WUlFRm/L59+zRs2DC1aNFCPj4+uv/++zVnzhzl5uaWGZeamqpnn31W/v7+8vb2VmhoqCZNmqTr16+7cO0AAKjZSPYBAECV2LVrl/r27atFixbpwIEDatWqlbp06aLs7Gxt2LBBAwYM0BtvvOE0duXKlerbt68+++wzWa1Wde7cWSdPntTMmTP1yCOP6MaNG07jkpKSFB4erlWrVqmgoEBhYWG6dOmS3n77bYWHh+vy5ctVucoAANQYJPsAAKBKGIahDh066N1339XVq1d14sQJpaSkKCMjQ9OnT5ckzZ07V5s3by4Wd/bsWcXExKigoEALFy7U+fPnlZKSorS0NN133306cOCApkyZUmJ52dnZevrpp5WTk6OJEyfqhx9+0KFDh5Senq4+ffro9OnTiomJccu6AwBQ3Uj2AQBAlejZs6dSU1P129/+Vk2aNHFM9/Ly0rx58zRkyBBJ0rJly4rFxcfHKy8vT5GRkZo8ebIsFoskqV27dlq+fLkk6YMPPihxlf7999/XlStX1LlzZy1evFienp6SpGbNmmnVqlXy8PDQli1blJKSUmXrDABATUGyDwAAqoSvr688PDxKnR8RESFJ+v777x3TDMPQxo0bJcnpVfjevXurU6dOstls2rRpU7F5GzZskCRFR0erfv36xeYFBQVp0KBBkqR169ZVYm0AAKhdSPYBAEC1sA+05+Pj45iWnp6uixcvSpL69OnjNM4+ff/+/Y5p+fn5OnToUIXjAAAwqwon+2fOnNGyZcv0wgsvqGvXrvLw8JDFYtHcuXNLjYmLi5PFYinzdfz48VLjGVUXAABzMQxDa9eulVQ8OU9LS5MkWa1WtWnTxmlsSEhIsbLSrfv8bTZbsfnliQMAwKxK71tXiiVLlmjJkiWVWlhgYKCCgoKczmvQoIHT6UlJSRo6dKhycnLUokULhYWF6fjx43r77be1ceNG7d27V61atapUewAAQPVYtmyZDh8+LC8vL73yyiuO6ZmZmZIkPz8/x736t7Pf/28ve/v7ouMD3CnOmby8POXl5Tk+Z2VlSZJsNpvjB4WazN7G8rbVWt9w6XJRs1T0eIB5cSxUP3dv+won+82bN1dUVJR69uypHj166MMPP9T69evLFTt+/HjFxcWVe1m3j6q7aNEieXp6KiMjQ8OGDVNycrJiYmJKjOILAABqrpSUFL388suSbo3GHxoa6phn79rv5eVVarzVapUk5eTklIgrK9ZZnDPz58/X7NmzS0zfsWNHqRcnaqLExMRylVvY0zXL27p1q2sqQpUo7/EA8+NYqD6lPTa2qlQ42Z8xY0axz2vWrHFZY253+6i69sF27KPqhoaGOkbV7datW5W1AwAAuMaZM2cUFRWl3NxcjRkzRpMmTSo239vbW5J08+bNUuuwX3Uveq+/Pc4eW/RzWXHOTJ8+XbGxsY7PWVlZCgwMVGRkpHx9fcuMrQlsNpsSExMVERHheCJBWbrEfe6S5R6NG+ySeuBaFT0eYF4cC9XP3lPMXSqc7LtTeUbV3b59u9atW0eyDwBADXfp0iVFRETo4sWLGjp0qFasWFGiq769q/3169dlGIbTrvz2bvhFu+sXfZ+ZmSl/f/9yxTljtVodvQCK8vT0rFUnyOVtb16B89slKrM81Fy17fhF1eFYqD7u3u5uHY0/KSlJTz31lAYMGKBRo0Zp4cKFunTpktOyjKoLAIB5XLt2TRERETp16pT69euntWvXOj3p6dixo6RbV+EvXLjgtK7Tp08XKytJwcHBjvrs88sTBwCAWbk12f/nP/+pdevWKSkpSevXr9fUqVMVEhKiFStWlCjLqLoAAJjDzz//rMcee0xHjx5Vjx49lJCQUGpX+qCgILVu3VqSlJyc7LSMfXqvXr0c0zw8PBy9/CoSBwCAWbmlG7+/v79ee+01DR8+XCEhIfLx8dHhw4c1d+5cbdu2TePHj1ezZs30+OOPO2Jqw6i6rh7R0lWj4boSo3XewuildQf7um5w1X7mOLmzvLw8DRs2TPv371dYWJi2b9+uRo0alVreYrFo+PDheu+99/TRRx/pN7/5TbH5e/fu1fHjx+Xp6aknnnii2LwRI0Zo//79WrFihf74xz8WuwUwPT1dO3fulCSNHDnShWsIAEDN5JZk/8UXXywxrXfv3tqyZYtGjhypjRs36tVXX1VUVJTj3rzaNKquq0a0dNVouK7EyLrFMXpp3cG+rhvudj+7e1Td2qagoECjR4/Wrl27FBoaqsTERDVt2vSOcZMnT9ZHH32kHTt2KD4+XpMmTZLFYtG5c+c0fvx4SdLzzz/v6AFgN2HCBMXHxys1NVWxsbHFnuIzZswY5efna8iQIQoPD6+S9QUAoCap1gH6LBaLFixYoI0bN+rUqVP69ttv1bVrV0m1Y1RdV49o6arRcF2JkXVvYfTSuoN9XTe4aj+7e1Td2uYf//iHPv30U0lSvXr19NRTTzkt5+/vr7Vr1zo+t2/fXsuWLdNzzz2nKVOmaMmSJWrZsqWOHj0qm82m8PBwxcfHl6jH19dXa9asUVRUlJYuXarVq1crKChIqampunHjhoKDg7V8+fIqWVcAAGqaah+N/95771XTpk117do1nTx50pHs16ZRdV1Vj6tGw3Ulkp3iGL207mBf1w13u585RspW9Ba6tLS0UsfYadeuXYlpY8eOVYcOHTR//nzt3btX3333nUJCQvTMM89o6tSpTi8CSNLAgQN18OBBzZ07V7t27dKRI0cUEBCg4cOHa8aMGXc8ZwAAwCyqPdmX/udkKT8/3zHNPqquzWbT6dOnnSb7jKoLAEDNFR0drejo6ErH9+7dWwkJCRWOCwsL0+rVqyu9XAAAzMCto/E7c/XqVf3444+SpLZt2zqmM6ouAAAAAACVU+3J/uLFi2UYhho3bqwePXoUmzdixAhJ0ooVK1RQUFBsHqPqAgAAAADgXJUn+8eOHdNLL72kY8eOFZuem5urefPm6a233pIkTZ06tcSo+xMmTFDz5s0do+raH3HEqLoAAAAAAJSuwsl+cnKymjdv7nitWbNG0q3H2xWdfv78eUm3Rjx+77331KVLF7Vs2VLdu3dX9+7d1axZM73++usqLCxUTEyMpk2bVmJZ9lF1vb29tXTpUgUEBKh79+4KCgpScnIyo+oCAAAAAOBEhZN9m82mjIwMx8s+0u6NGzeKTbd3uw8ODtacOXM0ZMgQNWzYUCdOnNCRI0fUtGlTjRo1Stu3b9eHH34oi8X5SPT2UXVHjx4ti8WiI0eOqFWrVoqNjVVKSkqJZ+wCAAAAAFDXVXg0/v79+8swjHKX9/Pz04wZMyq6mGIYVRcAAAAAgPKr9gH6AAAAAACAa5HsAwAAAABgMiT7AAAAAACYDMk+AAAAAAAmQ7IPAAAAAIDJkOwDAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmAzJPgAAAAAAJkOyDwAAAACAyZDsAwAAAABgMiT7AAAAAACYDMk+AAAAAAAmQ7IPAAAAAIDJkOwDAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmAzJPgAAAAAAJkOyDwAAAACAyZDsAwCAKnPmzBktW7ZML7zwgrp27SoPDw9ZLBbNnTu31Ji4uDhZLJYyX8ePHy81PjU1Vc8++6z8/f3l7e2t0NBQTZo0SdevX6+CNQQAoGbyqO4GAAAA81qyZImWLFlSqdjAwEAFBQU5ndegQQOn05OSkjR06FDl5OSoRYsWCgsL0/Hjx/X2229r48aN2rt3r1q1alWp9gAAUJuQ7AMAgCrTvHlzRUVFqWfPnurRo4c+/PBDrV+/vlyx48ePV1xcXLmXlZ2draefflo5OTmaOHGiFi1aJE9PT2VkZGjYsGFKTk5WTEyMNm/eXMm1AQCg9iDZBwAAVWbGjBnFPq9Zs6bKlvX+++/rypUr6ty5sxYvXqz69etLkpo1a6ZVq1YpNDRUW7ZsUUpKirp161Zl7QAAoCbgnn0AAGAKGzZskCRFR0c7En27oKAgDRo0SJK0bt06t7cNAAB348o+3CJ42haX1HN2wVCX1AMAqPmSkpJ07NgxZWRkqGnTpurZs6fGjh2r1q1blyibn5+vQ4cOSZL69OnjtL4+ffpo+/bt2r9/f5W2GwCAmoBkHwAA1Ej//Oc/i31ev3694uLi9O677yo6OrrYvLNnz8pms0mSQkJCnNZnn56Wlub6xgIAUMOQ7AMAgBrF399fr732moYPH66QkBD5+Pjo8OHDmjt3rrZt26bx48erWbNmevzxxx0xmZmZjvdNmjRxWq99etGyzuTl5SkvL8/xOSsrS5Jks9kcPyjUZPY2lret1vqGS5eLmqWixwPMi2Oh+rl725PsAwCAGuXFF18sMa13797asmWLRo4cqY0bN+rVV19VVFSULBaLJCk3N9dR1svLy2m9VqtVkpSTk1Pm8ufPn6/Zs2eXmL5jx45SH/lXEyUmJpar3MKerlne1q1bXVMRqkR5jweYH8dC9blx44Zbl0eyDwAAagWLxaIFCxZo48aNOnXqlL799lt17dpVkuTt7e0od/PmzWKf7exX6318fMpczvTp0xUbG+v4nJWVpcDAQEVGRsrX19cVq1KlbDabEhMTFRERIU9PzzuW7xL3uUuWezRusEvqgWtV9HiAeXEsVD97TzF3IdkHAAC1xr333qumTZvq2rVrOnnypCPZL9p1PzMzU/7+/iVi7d33S+vmb2e1Wh29AIry9PSsVSfI5W1vXoHFZctDzVXbjl9UHY6F6uPu7c6j9wAAQK1iP1nKz893TAsODnZMP336tNM4+/SOHTtWcQsBAKh+JPsAAKDWuHr1qn788UdJUtu2bR3TPTw81K1bN0lScnKy01j79F69elVxKwEAqH4k+wAAoNZYvHixDMNQ48aN1aNHj2LzRowYIUlasWKFCgoKis1LT0/Xzp07JUkjR450T2MBAKhGJPsAAKDGOHbsmF566SUdO3as2PTc3FzNmzdPb731liRp6tSpJUbdnzBhgpo3b67U1FTFxsY6HnGUkZGhMWPGKD8/X0OGDFF4eLh7VgYAgGpEsg8AAKpMcnKymjdv7nitWbNG0q3H2xWdfv78eUm3Rot+77331KVLF7Vs2VLdu3dX9+7d1axZM73++usqLCxUTEyMpk2bVmJZvr6+WrNmjby9vbV06VIFBASoe/fuCgoKUnJysoKDg7V8+XK3rj8AANWFZB8AAFQZm82mjIwMx8v++LsbN24Um27vdh8cHKw5c+ZoyJAhatiwoU6cOKEjR46oadOmGjVqlLZv364PP/xQFovzEeQHDhyogwcPavTo0bJYLDpy5IhatWql2NhYpaSkqHXr1m5bdwAAqhOP3gMAAFWmf//+Mgyj3OX9/Pw0Y8aMu1pmWFiYVq9efVd1AABQ23FlHwAAAAAAkyHZBwAAAADAZCqc7J85c0bLli3TCy+8oK5du8rDw0MWi0Vz5869Y+y+ffs0bNgwtWjRQj4+Prr//vs1Z84c5ebmlhmXmpqqZ599Vv7+/vL29lZoaKgmTZqk69evV7T5AAAAAACYXoXv2V+yZImWLFlS4QWtXLlS48aNU0FBgQICAhQYGKijR49q5syZSkhI0O7du9WgQYMScUlJSRo6dKhycnLUokULhYWF6fjx43r77be1ceNG7d27V61atapwewAAAAAAMKsKX9lv3ry5oqKi9Oabb2rbtm0aOXLkHWPOnj2rmJgYFRQUaOHChTp//rxSUlKUlpam++67TwcOHNCUKVNKxGVnZ+vpp59WTk6OJk6cqB9++EGHDh1Senq6+vTpo9OnTysmJqaiqwAAAAAAgKlVONmfMWOGEhIS9MYbb+jRRx9Vw4YN7xgTHx+vvLw8RUZGavLkyY7H5bRr187xvNsPPvhAly9fLhb3/vvv68qVK+rcubMWL14sT09PSVKzZs20atUqeXh4aMuWLUpJSanoagAAAAAAYFpVPkCfYRjauHGjJDm9Ct+7d2916tRJNptNmzZtKjZvw4YNkqTo6GjVr1+/2LygoCANGjRIkrRu3bqqaDoAAAAAALVSlSf76enpunjxoiSpT58+TsvYp+/fv98xLT8/X4cOHapwHAAAAAAAdV2FB+irqLS0NEmS1WpVmzZtnJYJCQkpVla6dZ+/zWYrNr88cc7k5eUpLy/P8TkrK0uSZLPZHMuoDHvs3dRRlLW+4ZJ6XKmmrZur2lPZ5VbX8uE+7Ou6wVX7meMEAADUVFWe7GdmZkqS/Pz8HPfq365JkybFyt7+3j6/PHHOzJ8/X7Nnzy4xfceOHU6fAFBRiYmJd12HJC3s6ZJqXGrr1q0uqcdV6+aq9lSWq/Y1aj72dd1wt/v5xo0bLmoJAACAa1V5sp+bmytJ8vLyKrWM1WqVJOXk5JSIKyvWWZwz06dPV2xsrONzVlaWAgMDFRkZKV9f3zusQelsNpsSExMVERHhGDzwbnSJ+/yu63C1o3GDXVKPq9bNVe2pKFfva9Rc7Ou6wVX72d5TDAAAoKap8mTf29tbknTz5s1Sy9i72Pv4+JSIs8cW/VxWnDNWq9Xxw0BRnp6eLjmZd1U9eQXOez5UJ1clO65at+pOvly1r1Hzsa/rhrvdzxwjAACgpqryAfrsXe2vX78uw3B+37a9G37R7vpF35fWTd9ZHAAAAAAAdV2VJ/sdO3aUdOsq/IULF5yWOX36dLGykhQcHOy4YmKfX544AAAAAADquipP9oOCgtS6dWtJUnJystMy9um9evVyTPPw8FC3bt0qHAcAAAAAQF1X5cm+xWLR8OHDJUkfffRRifl79+7V8ePH5enpqSeeeKLYvBEjRkiSVqxYoYKCgmLz0tPTtXPnTknSyJEjq6LpAAAAAADUSlWe7EvS5MmT5eXlpR07dig+Pt5x7/65c+c0fvx4SdLzzz/v6AFgN2HCBDVv3lypqamKjY11PM84IyNDY8aMUX5+voYMGaLw8HB3rAYAAAAAALVChZP95ORkNW/e3PFas2aNpFvPsi86/fz5846Y9u3ba9myZapXr56mTJmiwMBAdevWTR07dtSJEycUHh6u+Pj4Esvy9fXVmjVr5O3traVLlyogIEDdu3dXUFCQkpOTFRwcrOXLl9/F6gMAAAAAYD4VTvZtNpsyMjIcL/vj727cuFFs+u3d7seOHauvvvpKUVFRysnJ0XfffaeQkBDFxcVpz549uueee5wub+DAgTp48KBGjx4ti8WiI0eOqFWrVoqNjVVKSkqJ3gAAAAAAANR1HhUN6N+/f6mP0LuT3r17KyEhocJxYWFhWr16daWWCQAAAABAXeOWe/YBAAAAAID7kOwDAAAAAGAyJPsAAAAAAJgMyT4AAAAAACZDsg8AAAAAgMmQ7AMAAAAAYDIk+wAAAAAAmAzJPgAAAAAAJkOyDwAAAACAyZDsAwAAAABgMiT7AACgypw5c0bLli3TCy+8oK5du8rDw0MWi0Vz5869Y+y+ffs0bNgwtWjRQj4+Prr//vs1Z84c5ebmlhmXmpqqZ599Vv7+/vL29lZoaKgmTZqk69evu2itAACo+TyquwEAAMC8lixZoiVLllQ4buXKlRo3bpwKCgoUEBCgwMBAHT16VDNnzlRCQoJ2796tBg0alIhLSkrS0KFDlZOToxYtWigsLEzHjx/X22+/rY0bN2rv3r1q1aqVK1YNAIAajSv7AACgyjRv3lxRUVF68803tW3bNo0cOfKOMWfPnlVMTIwKCgq0cOFCnT9/XikpKUpLS9N9992nAwcOaMqUKSXisrOz9fTTTysnJ0cTJ07UDz/8oEOHDik9PV19+vTR6dOnFRMTUxWrCQBAjUOyDwAAqsyMGTOUkJCgN954Q48++qgaNmx4x5j4+Hjl5eUpMjJSkydPlsVikSS1a9dOy5cvlyR98MEHunz5crG4999/X1euXFHnzp21ePFieXp6SpKaNWumVatWycPDQ1u2bFFKSoqL1xIAgJqHZB8AANQYhmFo48aNkuT0Knzv3r3VqVMn2Ww2bdq0qdi8DRs2SJKio6NVv379YvOCgoI0aNAgSdK6deuqoukAANQoJPsAAKDGSE9P18WLFyVJffr0cVrGPn3//v2Oafn5+Tp06FCF4wAAMCuSfQAAUGOkpaVJkqxWq9q0aeO0TEhISLGy0q37/G02W7H55YkDAMCsGI0fAADUGJmZmZIkPz8/x736t2vSpEmxsre/t88vT5wzeXl5ysvLc3zOysqSJNlsNscPCjWZvY3lbau1vuHS5aJmqejxAPPiWKh+7t72JPsAAKDGyM3NlSR5eXmVWsZqtUqScnJySsSVFesszpn58+dr9uzZJabv2LHD6eP+aqrExMRylVvY0zXL27p1q2sqQpUo7/EA8+NYqD43btxw6/JI9gEAQI3h7e0tSbp582apZexX3X18fErE2WOLfi4rzpnp06crNjbW8TkrK0uBgYGKjIyUr69vOdbCPbrEfe50urWeoTndC/XGwXrKK3TeO6IqHI0b7LZlofxsNpsSExMVERHheEIF6iaOhepn7ynmLiT7AACgxrB3tb9+/boMw3Dald/eDb9od/2i7zMzM+Xv71+uOGesVqujF0BRnp6eNeoEOa+g7EQ+r9ByxzKuVJO2DUqqaccvqg/HQvVx93ZngD4AAFBjdOzYUdKtq/AXLlxwWub06dPFykpScHCw4yTKPr88cQAAmBXJPgAAqDGCgoLUunVrSVJycrLTMvbpvXr1ckzz8PBQt27dKhwHAIBZkewDAIAaw2KxaPjw4ZKkjz76qMT8vXv36vjx4/L09NQTTzxRbN6IESMkSStWrFBBQUGxeenp6dq5c6ckaeTIkVXRdAAAahSSfQAAUKNMnjxZXl5e2rFjh+Lj42UYtx4Nd+7cOY0fP16S9Pzzzzt6ANhNmDBBzZs3V2pqqmJjYx2POMrIyNCYMWOUn5+vIUOGKDw83L0rBABANSDZBwAAVSY5OVnNmzd3vNasWSPp1uPtik4/f/68I6Z9+/ZatmyZ6tWrpylTpigwMFDdunVTx44ddeLECYWHhys+Pr7Esnx9fbVmzRp5e3tr6dKlCggIUPfu3RUUFKTk5GQFBwdr+fLlblt3AACqE6Pxu0CXuM/dOtotAAC1hc1mU0ZGRonpN27cKPa84du73Y8dO1YdOnTQ/PnztXfvXn333XcKCQnRM888o6lTpzp9tJ4kDRw4UAcPHtTcuXO1a9cuHTlyRAEBARo+fLhmzJhxx5H4AQAwC5J91CrB07a4rK6zC4a6rC4AgHP9+/d3dMOvqN69eyshIaHCcWFhYVq9enWllgkAgFnQjR8AAAAAAJMh2QcAAAAAwGRI9gEAAAAAMBmSfQAAAAAATIZkHwAAAAAAkyHZBwAAAADAZEj2AQAAAAAwGZJ9AAAAAABMhmQfAAAAAACTIdkHAAAAAMBkSPYBAAAAADAZkn0AAAAAAEzGo7obAAAAAFSF4GlbXFLP2QVDXVIPALgTV/YBAAAAADAZruwDAADgrrnqKrrElXQAcAWu7AMAAAAAYDJuTfajo6NlsVjKfOXm5jqN3bdvn4YNG6YWLVrIx8dH999/v+bMmVNqeQAAAAAA6qpq6cbfsWNHtWzZ0um8evVK/v6wcuVKjRs3TgUFBQoICFBgYKCOHj2qmTNnKiEhQbt371aDBg2qutkAAAAAANQK1ZLsv/baa4qOji5X2bNnzyomJkYFBQVauHChJk2aJIvFonPnzmnw4ME6cOCApkyZonfeeadqGw3Tqci9hdb6hhb2lLrEfa68AkuxedxXCAAAAKCmqfH37MfHxysvL0+RkZGaPHmyLJZbiVa7du20fPlySdIHH3ygy5cvV2czAQAAAACoMWp0sm8YhjZu3ChJiomJKTG/d+/e6tSpk2w2mzZt2uTu5gEAAAAAUCNVS7K/bt06PfnkkxowYIBGjx6tv/71r/rpp59KlEtPT9fFixclSX369HFal336/v37q67BAAAAAADUItVyz/6WLcXvlf7kk080a9YsrVq1So8++qhjelpamiTJarWqTZs2TusKCQkpVhYAAAAAgLrOrcl+aGio5s2bp6FDh6p9+/ayWCzat2+f3njjDe3fv19PPvmk9uzZo+7du0uSMjMzJUl+fn6Oe/Vv16RJk2JlncnLy1NeXp7jc1ZWliTJZrPJZrNVen3ssdZ6RqXrqOnuZvsUZa1fu7eRfR8729eu2kaoGez7k/1qbq7azxwnAACgpnJrsv/GG2+UmBYREaF+/fqpb9+++ve//62pU6fqiy++kCTl5uZKkry8vEqt02q1SpJycnJKLTN//nzNnj27xPQdO3a45JF9c7oX3nUdNdXWrVtdUs/Cni6ppto529eu2kaoWRITE6u7CXCDu93PN27ccFFLAAAAXKtauvHfzsvLS3PmzNHgwYO1e/duZWZmqkmTJvL29pYk3bx5s9RY+xV7Hx+fUstMnz5dsbGxjs9ZWVkKDAxUZGSkfH19K91um82mxMREvXGwnvIKnfc8qO2Oxg12ST1d4j53ST3VxVrP0JzuhU73tau2EWoG+//riIgIeXp6VndzUEVctZ/tPcUAAABqmhqR7EvSww8/LEkqLCzU6dOnFR4e7uiif/36dRmG4bQrv737vr2sM1ar1dEDoChPT0+XnMznFVpKPHvdLFyV7Jhl+zjb1ySE5uSqvw+o2e52P3OMAACAmqrGPHqv6AlTfn6+JKljx46Sbl29v3DhgtO406dPFysLAAAAAEBdV2OS/WPHjjnet23bVpIUFBSk1q1bS5KSk5Odxtmn9+rVq4pbCAAAAABA7VBjkv23335bktSpUycFBARIkiwWi4YPHy5J+uijj0rE7N27V8ePH5enp6eeeOIJ9zUWAAAAAIAazG3JfmJioqZPn64zZ84Um/7TTz9p4sSJWr16tSRp5syZxeZPnjxZXl5e2rFjh+Lj42UYtx59du7cOY0fP16S9Pzzzzt6AAAAAAAAUNe5Ldn/5ZdftGDBAoWEhKht27bq2bOnHnzwQbVs2VJ//etfZbFYNGvWLD3zzDPF4tq3b69ly5apXr16mjJligIDA9WtWzd17NhRJ06cUHh4uOLj4921GgAAwA2io6NlsVjKfNkf0Xu7ffv2adiwYWrRooV8fHx0//33a86cOaWWBwDAjNw2Gn94eLhef/117du3TydPntTRo0dlGIYCAgLUt29fvfTSS6Xedz927Fh16NBB8+fP1969e/Xdd98pJCREzzzzjKZOnep4RB8AADCXjh07qmXLlk7n1atX8prFypUrNW7cOBUUFCggIECBgYE6evSoZs6cqYSEBO3evVsNGjSo6mYDAFDt3JbsBwYGau7cuZWO7927txISElzYIgAAUNO99tprio6OLlfZs2fPKiYmRgUFBVq4cKEmTZoki8Wic+fOafDgwTpw4ICmTJmid955p2obDQBADVBjBugDAAC4G/Hx8crLy1NkZKQmT54si8UiSWrXrp2WL18uSfrggw90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9jfHyLr2ZZaHK9YRAACzmDZtmtNBgpOSkrR+/XpJ0n/8x39UaLT88nrllVckSenp6Y7v5tLk5+c73t97772KioqSJC1atEi7d+8uMzYrK0sXL168q7YCZkWyD9RCLVq0UL9+/bR8+XJduHBBly9f1rp16/TrX//a8Zi/pUuXloibOHGi45m0r732mh5//HFt3rxZP/zwg65fv65z587p888/19SpUxUSEqK//OUvxeK7du2qCRMmSJLWr1+vqKgoJScnKyMjQydPntSbb76pMWPGSLr1XN3XX3+91HUIDg7W559/riFDhuirr75SRkaGvv/+e82cOVMvvPCCJKlDhw6aOHFihbbN3a4jAABmEBAQoO+++06PPPKIPv/8c129elXnzp3Tn//8Zz3xxBMyDENNmzZVXFxclSx/1KhRGjVqlCTpv/7rvxQZGaktW7bo4sWLjkfzvv/+++rdu3exRwVL0nvvvadWrVopLy9PkZGR+sMf/qDk5GT9+OOPunbtmr7//nutW7dO48ePV9u2bZWcnFwl6wDUdjx6D6iF/vGPf2jw4MFOr3rXq1dPf/nLXxQREVFinqenpz777DONHz9e69at0+bNm7V58+ZSl2O1WktMW7JkibKysrRq1Spt3bpVW7duLVEmODhY27Ztk5+fX6l19+vXT88++6z+9Kc/Oe1m2KZNG23evFk+Pj6l1uGMK9YRAIDarkOHDnrjjTf00ksv6dFHHy0x39fXVwkJCeW+Va4y/t//+3/y8vLSqlWrlJiYWOZtBUW1bdtWX331lUaOHKkjR47onXfe0TvvvFNqeb7LAee4sg/UQj169FBKSopefPFFtWvXTlarVS1atNDw4cO1Z88e/eEPfyg1tlGjRlq7dq2++uorjR8/Xvfee68aNmwoDw8PNWvWTL169dKUKVO0b98+LVq0qES8l5eXVq5cqa1bt2rEiBFq06aNPD095efnp4cfflgLFy7UsWPH1KlTpzuux9y5c7Vp0yZFRESoefPmslqt6tChgyZPnqyjR4/qvvvuq9T2udt1BADADF588UUlJSXpySefVOvWreXl5aV27dppwoQJOnbsWLHb/aqCt7e3Vq5cqV27dunZZ59Vu3bt5O3tLV9fX3Xu3FmjR4/Whg0bnLajY8eOOnz4sFauXKnhw4erbdu2slqt8vLyUps2bTRw4EAtXLhQ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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "X_train.hist(bins=20, figsize=(12, 8));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Do you see anything interesting in these plots? " ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "food_type\n", "Other 189\n", "Canadian/American 131\n", "Chinese 102\n", "Indian 36\n", "Italian 32\n", "Thai 20\n", "Fusion 18\n", "Mexican 17\n", "fusion 3\n", "Quebecois 1\n", "Name: count, dtype: int64" ] }, "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train['food_type'].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Error in data collection? Probably \"Fusion\" and \"fusion\" categories should be combined?" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [], "source": [ "X_train['food_type'] = X_train['food_type'].replace(\"fusion\", \"Fusion\")\n", "X_test['food_type'] = X_test['food_type'].replace(\"fusion\", \"Fusion\")" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "food_type\n", "Other 189\n", "Canadian/American 131\n", "Chinese 102\n", "Indian 36\n", "Italian 32\n", "Fusion 21\n", "Thai 20\n", "Mexican 17\n", "Quebecois 1\n", "Name: count, dtype: int64" ] }, "execution_count": 55, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train['food_type'].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, usually we should spend lots of time in EDA, but let's stop here so that we have time to learn about transformers and pipelines. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dummy Classifier" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
dummy
fit_time0.001 (+/- 0.000)
score_time0.001 (+/- 0.000)
test_score0.515 (+/- 0.002)
train_score0.515 (+/- 0.000)
\n", "
" ], "text/plain": [ " dummy\n", "fit_time 0.001 (+/- 0.000)\n", "score_time 0.001 (+/- 0.000)\n", "test_score 0.515 (+/- 0.002)\n", "train_score 0.515 (+/- 0.000)" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.dummy import DummyClassifier\n", "\n", "results_df = {}\n", "dummy = DummyClassifier()\n", "results_df['dummy'] = mean_std_cross_val_scores(dummy, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have a relatively balanced distribution of both 'like' and 'dislike' classes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Preprocessing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How can we horizontally stack \n", "- preprocessed numeric features, \n", "- preprocessed binary features, \n", "- preprocessed ordinal features, and \n", "- preprocessed categorical features?\n", "\n", "Let's define a column transformer. " ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [], "source": [ "numeric_feats = ['age', 'n_people', 'price'] # Continuous and quantitative features\n", "categorical_feats = ['north_america', 'food_type'] # Discrete and qualitative features\n", "binary_feats = ['good_server'] # Categorical features with only two possible values \n", "ordinal_feats = ['noise_level'] # Some natural ordering in the categories \n", "noise_cats = ['no music', 'low', 'medium', 'high', 'crazy loud']\n", "drop_feats = ['comments', 'restaurant_name', 'eat_out_freq'] # Dropping text feats and `eat_out_freq` because it's not that useful" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "noise_level\n", "medium 232\n", "low 186\n", "high 75\n", "no music 37\n", "crazy loud 18\n", "Name: count, dtype: int64" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train['noise_level'].value_counts()" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "noise_levels = [\"no music\", \"low\", \"medium\", \"high\", \"crazy loud\"]" ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [], "source": [ "from sklearn.impute import SimpleImputer\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.preprocessing import OneHotEncoder\n", "from sklearn.preprocessing import OrdinalEncoder\n", "\n", "from sklearn.compose import make_column_transformer\n", "\n", "numeric_transformer = make_pipeline(SimpleImputer(strategy=\"median\"),\n", " StandardScaler()) \n", "binary_transformer = make_pipeline(SimpleImputer(strategy=\"most_frequent\"), \n", " OneHotEncoder(drop=\"if_binary\"))\n", "ordinal_transformer = make_pipeline(SimpleImputer(strategy=\"most_frequent\"), \n", " OrdinalEncoder(categories=[noise_levels]))\n", "categorical_transformer = make_pipeline(SimpleImputer(strategy=\"most_frequent\"), \n", " OneHotEncoder(sparse_output=False, handle_unknown=\"ignore\"))\n", "\n", "preprocessor = make_column_transformer(\n", " (numeric_transformer, numeric_feats), \n", " (binary_transformer, binary_feats), \n", " (ordinal_transformer, ordinal_feats),\n", " (categorical_transformer, categorical_feats),\n", " (\"drop\", drop_feats)\n", ")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How does the transformed data look like? " ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(753, 17)" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "transformed = preprocessor.fit_transform(X_train)\n", "transformed.shape" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
ColumnTransformer(transformers=[('pipeline-1',\n",
       "                                 Pipeline(steps=[('simpleimputer',\n",
       "                                                  SimpleImputer(strategy='median')),\n",
       "                                                 ('standardscaler',\n",
       "                                                  StandardScaler())]),\n",
       "                                 ['age', 'n_people', 'price']),\n",
       "                                ('pipeline-2',\n",
       "                                 Pipeline(steps=[('simpleimputer',\n",
       "                                                  SimpleImputer(strategy='most_frequent')),\n",
       "                                                 ('onehotencoder',\n",
       "                                                  OneHotEncoder(drop='if_binary'))]),\n",
       "                                 ['good_server']),\n",
       "                                ('pipeline-3',...\n",
       "                                                  OrdinalEncoder(categories=[['no '\n",
       "                                                                              'music',\n",
       "                                                                              'low',\n",
       "                                                                              'medium',\n",
       "                                                                              'high',\n",
       "                                                                              'crazy '\n",
       "                                                                              'loud']]))]),\n",
       "                                 ['noise_level']),\n",
       "                                ('pipeline-4',\n",
       "                                 Pipeline(steps=[('simpleimputer',\n",
       "                                                  SimpleImputer(strategy='most_frequent')),\n",
       "                                                 ('onehotencoder',\n",
       "                                                  OneHotEncoder(handle_unknown='ignore',\n",
       "                                                                sparse_output=False))]),\n",
       "                                 ['north_america', 'food_type']),\n",
       "                                ('drop', 'drop',\n",
       "                                 ['comments', 'restaurant_name',\n",
       "                                  'eat_out_freq'])])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "ColumnTransformer(transformers=[('pipeline-1',\n", " Pipeline(steps=[('simpleimputer',\n", " SimpleImputer(strategy='median')),\n", " ('standardscaler',\n", " StandardScaler())]),\n", " ['age', 'n_people', 'price']),\n", " ('pipeline-2',\n", " Pipeline(steps=[('simpleimputer',\n", " SimpleImputer(strategy='most_frequent')),\n", " ('onehotencoder',\n", " OneHotEncoder(drop='if_binary'))]),\n", " ['good_server']),\n", " ('pipeline-3',...\n", " OrdinalEncoder(categories=[['no '\n", " 'music',\n", " 'low',\n", " 'medium',\n", " 'high',\n", " 'crazy '\n", " 'loud']]))]),\n", " ['noise_level']),\n", " ('pipeline-4',\n", " Pipeline(steps=[('simpleimputer',\n", " SimpleImputer(strategy='most_frequent')),\n", " ('onehotencoder',\n", " OneHotEncoder(handle_unknown='ignore',\n", " sparse_output=False))]),\n", " ['north_america', 'food_type']),\n", " ('drop', 'drop',\n", " ['comments', 'restaurant_name',\n", " 'eat_out_freq'])])" ] }, "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "preprocessor" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[\"north_america_Don't want to share\",\n", " 'north_america_No',\n", " 'north_america_Yes',\n", " 'food_type_Canadian/American',\n", " 'food_type_Chinese',\n", " 'food_type_Fusion',\n", " 'food_type_Indian',\n", " 'food_type_Italian',\n", " 'food_type_Mexican',\n", " 'food_type_Other',\n", " 'food_type_Quebecois',\n", " 'food_type_Thai']" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Getting feature names from a column transformer\n", "ohe_feat_names = preprocessor.named_transformers_['pipeline-4']['onehotencoder'].get_feature_names_out(categorical_feats).tolist()\n", "ohe_feat_names" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['age', 'n_people', 'price']" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numeric_feats" ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [], "source": [ "feat_names = numeric_feats + binary_feats + ordinal_feats + ohe_feat_names" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[-0.66941678, 0.31029469, -0.36840629, ..., 0. ,\n", " 0. , 0. ],\n", " [-0.66941678, 0.31029469, -0.05422496, ..., 0. ,\n", " 0. , 0. ],\n", " [-0.89515383, 0.82336432, -0.25058829, ..., 0. ,\n", " 0. , 0. ],\n", " ...,\n", " [-0.89515383, -0.97237936, -0.64331495, ..., 0. ,\n", " 0. , 0. ],\n", " [-0.89515383, -0.20277493, -0.25058829, ..., 1. ,\n", " 0. , 0. ],\n", " [-0.89515383, 1.33643394, -0.05422496, ..., 0. ,\n", " 0. , 0. ]])" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "transformed" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
agen_peoplepricegood_servernoise_levelnorth_america_Don't want to sharenorth_america_Nonorth_america_Yesfood_type_Canadian/Americanfood_type_Chinesefood_type_Fusionfood_type_Indianfood_type_Italianfood_type_Mexicanfood_type_Otherfood_type_Quebecoisfood_type_Thai
0-0.6694170.310295-0.3684060.03.00.01.00.00.01.00.00.00.00.00.00.00.0
1-0.6694170.310295-0.0542251.01.00.00.01.01.00.00.00.00.00.00.00.00.0
2-0.8951540.823364-0.2505881.02.00.01.00.01.00.00.00.00.00.00.00.00.0
3-0.669417-0.202775-0.2505881.02.00.00.01.00.00.00.00.00.00.00.01.00.0
40.007794-0.202775-0.0542251.03.00.00.01.00.00.00.01.00.00.00.00.00.0
......................................................
7480.685006-0.715845-0.6433151.02.00.01.00.00.01.00.00.00.00.00.00.00.0
7490.007794-0.613231-0.9182241.02.00.01.00.00.00.00.00.00.00.01.00.00.0
750-0.895154-0.972379-0.6433150.01.00.00.01.01.00.00.00.00.00.00.00.00.0
751-0.895154-0.202775-0.2505881.02.00.00.01.00.00.00.00.00.00.01.00.00.0
752-0.8951541.336434-0.0542251.03.01.00.00.00.01.00.00.00.00.00.00.00.0
\n", "

753 rows × 17 columns

\n", "
" ], "text/plain": [ " age n_people price good_server noise_level \\\n", "0 -0.669417 0.310295 -0.368406 0.0 3.0 \n", "1 -0.669417 0.310295 -0.054225 1.0 1.0 \n", "2 -0.895154 0.823364 -0.250588 1.0 2.0 \n", "3 -0.669417 -0.202775 -0.250588 1.0 2.0 \n", "4 0.007794 -0.202775 -0.054225 1.0 3.0 \n", ".. ... ... ... ... ... \n", "748 0.685006 -0.715845 -0.643315 1.0 2.0 \n", "749 0.007794 -0.613231 -0.918224 1.0 2.0 \n", "750 -0.895154 -0.972379 -0.643315 0.0 1.0 \n", "751 -0.895154 -0.202775 -0.250588 1.0 2.0 \n", "752 -0.895154 1.336434 -0.054225 1.0 3.0 \n", "\n", " north_america_Don't want to share north_america_No north_america_Yes \\\n", "0 0.0 1.0 0.0 \n", "1 0.0 0.0 1.0 \n", "2 0.0 1.0 0.0 \n", "3 0.0 0.0 1.0 \n", "4 0.0 0.0 1.0 \n", ".. ... ... ... \n", "748 0.0 1.0 0.0 \n", "749 0.0 1.0 0.0 \n", "750 0.0 0.0 1.0 \n", "751 0.0 0.0 1.0 \n", "752 1.0 0.0 0.0 \n", "\n", " food_type_Canadian/American food_type_Chinese food_type_Fusion \\\n", "0 0.0 1.0 0.0 \n", "1 1.0 0.0 0.0 \n", "2 1.0 0.0 0.0 \n", "3 0.0 0.0 0.0 \n", "4 0.0 0.0 0.0 \n", ".. ... ... ... \n", "748 0.0 1.0 0.0 \n", "749 0.0 0.0 0.0 \n", "750 1.0 0.0 0.0 \n", "751 0.0 0.0 0.0 \n", "752 0.0 1.0 0.0 \n", "\n", " food_type_Indian food_type_Italian food_type_Mexican food_type_Other \\\n", "0 0.0 0.0 0.0 0.0 \n", "1 0.0 0.0 0.0 0.0 \n", "2 0.0 0.0 0.0 0.0 \n", "3 0.0 0.0 0.0 0.0 \n", "4 1.0 0.0 0.0 0.0 \n", ".. ... ... ... ... \n", "748 0.0 0.0 0.0 0.0 \n", "749 0.0 0.0 0.0 1.0 \n", "750 0.0 0.0 0.0 0.0 \n", "751 0.0 0.0 0.0 1.0 \n", "752 0.0 0.0 0.0 0.0 \n", "\n", " food_type_Quebecois food_type_Thai \n", "0 0.0 0.0 \n", "1 0.0 0.0 \n", "2 0.0 0.0 \n", "3 1.0 0.0 \n", "4 0.0 0.0 \n", ".. ... ... \n", "748 0.0 0.0 \n", "749 0.0 0.0 \n", "750 0.0 0.0 \n", "751 0.0 0.0 \n", "752 0.0 0.0 \n", "\n", "[753 rows x 17 columns]" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.DataFrame(transformed, columns = feat_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have new columns for the categorical features. Let's create a pipeline with the preprocessor and SVC. " ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.001 (+/- 0.000)0.001 (+/- 0.000)0.515 (+/- 0.002)0.515 (+/- 0.000)
Decision Tree (numeric-only)0.003 (+/- 0.000)0.001 (+/- 0.000)0.497 (+/- 0.038)0.833 (+/- 0.010)
KNN (numeric-only)0.003 (+/- 0.001)0.004 (+/- 0.000)0.525 (+/- 0.034)0.674 (+/- 0.015)
SVM (numeric-only)0.012 (+/- 0.000)0.005 (+/- 0.000)0.587 (+/- 0.033)0.623 (+/- 0.006)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.001 (+/- 0.000) 0.001 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.003 (+/- 0.000) 0.001 (+/- 0.000) \n", "KNN (numeric-only) 0.003 (+/- 0.001) 0.004 (+/- 0.000) \n", "SVM (numeric-only) 0.012 (+/- 0.000) 0.005 (+/- 0.000) \n", "\n", " test_score train_score \n", "dummy 0.515 (+/- 0.002) 0.515 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.497 (+/- 0.038) 0.833 (+/- 0.010) \n", "KNN (numeric-only) 0.525 (+/- 0.034) 0.674 (+/- 0.015) \n", "SVM (numeric-only) 0.587 (+/- 0.033) 0.623 (+/- 0.006) " ] }, "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.tree import DecisionTreeClassifier\n", "from sklearn.neighbors import KNeighborsClassifier\n", "from sklearn.svm import SVC\n", "models = {\n", " \"Decision Tree\": DecisionTreeClassifier(),\n", " \"KNN\": KNeighborsClassifier(),\n", " \"SVM\": SVC() \n", "}\n", "\n", "for (name, model) in models.items():\n", " pipe_num_model = make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), model)\n", " results_df[name +' (numeric-only)'] = mean_std_cross_val_scores(pipe_num_model, X_train[numeric_feats], y_train, return_train_score=True)\n", "pd.DataFrame(results_df).T" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.001 (+/- 0.000)0.001 (+/- 0.000)0.515 (+/- 0.002)0.515 (+/- 0.000)
Decision Tree (numeric-only)0.003 (+/- 0.000)0.001 (+/- 0.000)0.497 (+/- 0.038)0.833 (+/- 0.010)
KNN (numeric-only)0.003 (+/- 0.001)0.004 (+/- 0.000)0.525 (+/- 0.034)0.674 (+/- 0.015)
SVM (numeric-only)0.012 (+/- 0.000)0.005 (+/- 0.000)0.587 (+/- 0.033)0.623 (+/- 0.006)
Decision Tree(non-text feats)0.009 (+/- 0.000)0.003 (+/- 0.000)0.590 (+/- 0.039)0.889 (+/- 0.008)
KNN(non-text feats)0.008 (+/- 0.000)0.004 (+/- 0.000)0.598 (+/- 0.023)0.737 (+/- 0.008)
SVM(non-text feats)0.019 (+/- 0.000)0.008 (+/- 0.000)0.687 (+/- 0.011)0.733 (+/- 0.008)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.001 (+/- 0.000) 0.001 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.003 (+/- 0.000) 0.001 (+/- 0.000) \n", "KNN (numeric-only) 0.003 (+/- 0.001) 0.004 (+/- 0.000) \n", "SVM (numeric-only) 0.012 (+/- 0.000) 0.005 (+/- 0.000) \n", "Decision Tree(non-text feats) 0.009 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN(non-text feats) 0.008 (+/- 0.000) 0.004 (+/- 0.000) \n", "SVM(non-text feats) 0.019 (+/- 0.000) 0.008 (+/- 0.000) \n", "\n", " test_score train_score \n", "dummy 0.515 (+/- 0.002) 0.515 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.497 (+/- 0.038) 0.833 (+/- 0.010) \n", "KNN (numeric-only) 0.525 (+/- 0.034) 0.674 (+/- 0.015) \n", "SVM (numeric-only) 0.587 (+/- 0.033) 0.623 (+/- 0.006) \n", "Decision Tree(non-text feats) 0.590 (+/- 0.039) 0.889 (+/- 0.008) \n", "KNN(non-text feats) 0.598 (+/- 0.023) 0.737 (+/- 0.008) \n", "SVM(non-text feats) 0.687 (+/- 0.011) 0.733 (+/- 0.008) " ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for (name, model) in models.items():\n", " pipe_model = make_pipeline(preprocessor, model)\n", " results_df[name + '(non-text feats)'] = mean_std_cross_val_scores(pipe_model, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results_df).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are getting better results when we include numeric, categorical, binary, ordinal features. \n", "


" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Incorporating text features \n", "\n", "We haven't incorporated the comments feature into our pipeline yet, even though it holds significant value in indicating whether the restaurant was liked or not." ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
north_americaeat_out_freqagen_peoplepricefood_typenoise_levelgood_servercommentsrestaurant_name
80No2.02130.02200.0ChinesehighNoThe environment was very not clean. The food tasted awful.NaN
934Yes4.02130.03000.0Canadian/AmericanlowYesThe building and the room gave a very comfy feeling. Immediately after sitting down it felt like we were right at home.NaN
911No4.02040.02500.0Canadian/AmericanmediumYesI was hungryChambar
459Yes5.021NaNNaNQuebecoisNaNNaNNaNNaN
62Yes2.02420.03000.0IndianhighYesbad tasteeast is east
.................................
106No3.02710.01500.0ChinesemediumYesFood wasn't great.NaN
333No1.02412.0800.0OthermediumYesNaNNaN
393Yes4.0205.01500.0Canadian/AmericanlowNoNaNNaN
376Yes5.020NaNNaNNaNNaNNaNNaNNaN
525Don't want to share4.02050.03000.0ChinesehighYesNaNHaidilao
\n", "

753 rows × 10 columns

\n", "
" ], "text/plain": [ " north_america eat_out_freq age n_people price \\\n", "80 No 2.0 21 30.0 2200.0 \n", "934 Yes 4.0 21 30.0 3000.0 \n", "911 No 4.0 20 40.0 2500.0 \n", "459 Yes 5.0 21 NaN NaN \n", "62 Yes 2.0 24 20.0 3000.0 \n", ".. ... ... ... ... ... \n", "106 No 3.0 27 10.0 1500.0 \n", "333 No 1.0 24 12.0 800.0 \n", "393 Yes 4.0 20 5.0 1500.0 \n", "376 Yes 5.0 20 NaN NaN \n", "525 Don't want to share 4.0 20 50.0 3000.0 \n", "\n", " food_type noise_level good_server \\\n", "80 Chinese high No \n", "934 Canadian/American low Yes \n", "911 Canadian/American medium Yes \n", "459 Quebecois NaN NaN \n", "62 Indian high Yes \n", ".. ... ... ... \n", "106 Chinese medium Yes \n", "333 Other medium Yes \n", "393 Canadian/American low No \n", "376 NaN NaN NaN \n", "525 Chinese high Yes \n", "\n", " comments \\\n", "80 The environment was very not clean. The food tasted awful. \n", "934 The building and the room gave a very comfy feeling. Immediately after sitting down it felt like we were right at home. \n", "911 I was hungry \n", "459 NaN \n", "62 bad taste \n", ".. ... \n", "106 Food wasn't great. \n", "333 NaN \n", "393 NaN \n", "376 NaN \n", "525 NaN \n", "\n", " restaurant_name \n", "80 NaN \n", "934 NaN \n", "911 Chambar \n", "459 NaN \n", "62 east is east \n", ".. ... \n", "106 NaN \n", "333 NaN \n", "393 NaN \n", "376 NaN \n", "525 Haidilao \n", "\n", "[753 rows x 10 columns]" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create bag-of-words representation of the `comments` feature. But first we need to impute the rows where there are no comments. There is a small complication if we want to put `SimpleImputer` and `CountVectorizer` in a pipeline. \n", "- `SimpleImputer` takes a 2D array as input and produced 2D array as output. \n", "- `CountVectorizer` takes a 1D array as input. \n", "\n", "To deal with this, we will use sklearn's `FunctionTransformer` to convert the 2D output of `SimpleImputer` into a 1D array which can be passed to `CountVectorizer` as input. " ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.6493951434878588" ] }, "execution_count": 71, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.preprocessing import FunctionTransformer\n", "from sklearn.feature_extraction.text import CountVectorizer\n", "\n", "reshape_for_countvectorizer = FunctionTransformer(lambda X: X.squeeze(), validate=False)\n", "text_transformer = make_pipeline(SimpleImputer(strategy=\"constant\", fill_value=\"missing\"), \n", " reshape_for_countvectorizer, \n", " CountVectorizer(stop_words=\"english\"))\n", "text_pipe = make_pipeline(text_transformer, SVC())\n", "cross_val_score(text_pipe, X_train[['comments']], y_train).mean()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pretty good scores just with text features! Let's examine the transformed data. " ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [], "source": [ "transformed = text_transformer.fit_transform(X_train[['comments']], y_train)" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<753x548 sparse matrix of type ''\n", "\twith 1841 stored elements in Compressed Sparse Row format>" ] }, "execution_count": 73, "metadata": {}, "output_type": "execute_result" } ], "source": [ "transformed" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's a sparse matrix. Let's explore the the vocabulary. " ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['18', '30', '40mins', '65', 'actually', 'addition', 'affordable',\n", " 'alcohol', 'ale', 'allergic'], dtype=object)" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab = text_transformer.named_steps[\"countvectorizer\"].get_feature_names_out()\n", "vocab[:10]" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['18', '30', '40mins', '65', 'actually', 'addition', 'affordable',\n", " 'alcohol', 'ale', 'allergic'], dtype=object)" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[0:10]" ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['fusion', 'games', 'gave', 'general', 'genuinely', 'getting',\n", " 'ginger', 'girlfriends', 'gluten', 'going'], dtype=object)" ] }, "execution_count": 76, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[200:210]" ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['undressed', 'unfresh', 'uni', 'unique', 'unreasonable', 'upset',\n", " 'usual', 'uwu', 'value', 'vancouver', 'variety', 'vds', 've',\n", " 'vegan', 'vibe', 'vibes', 'vietnamese', 'view', 'visit', 'wait',\n", " 'waited', 'waiter', 'waiters', 'waiting', 'waitress', 'walking',\n", " 'want', 'warm', 'washrooms', 'wasn', 'water', 'watery', 'way',\n", " 'weekend', 'went', 'wet', 'wife', 'wind', 'window', 'wine',\n", " 'wings', 'winter', 'work', 'worst', 'wrong', 'yelling', 'yield',\n", " 'yummy'], dtype=object)" ] }, "execution_count": 77, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[500:600]" ] }, { "cell_type": "code", "execution_count": 78, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['18', 'ask', 'better', 'cash', 'closing', 'country', 'dessert',\n", " 'drunk', 'expecting', 'figuring', 'fusion', 'having', 'impeccable',\n", " 'knowledgeable', 'love', 'nice', 'pain', 'played', 'quality',\n", " 'removed', 'sauces', 'sitting', 'spoke', 'tacky', 'time',\n", " 'undressed', 'waited', 'wings'], dtype=object)" ] }, "execution_count": 78, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab[0::20]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Do we get better scores if we combine all features? Let's define a column transformer which carries out \n", "- imputation and scaling on numeric features\n", "- imputation and one-hot encoding with `drop=\"if_binary\"` on binary features\n", "- imputation and one-hot encoding with `handle_unknown=\"ignore\"` on categorical features\n", "- imputation, reshaping, and bag-of-words transformation on the text feature" ] }, { "cell_type": "code", "execution_count": 79, "metadata": {}, "outputs": [], "source": [ "from sklearn.feature_extraction.text import CountVectorizer\n", "text_feat = ['comments']\n", "\n", "preprocessor_all = make_column_transformer(\n", " (numeric_transformer, numeric_feats), \n", " (binary_transformer, binary_feats), \n", " (ordinal_transformer, ordinal_feats),\n", " (categorical_transformer, categorical_feats),\n", " (text_transformer, text_feat), \n", " (\"drop\", drop_feats)\n", ")" ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<753x565 sparse matrix of type ''\n", "\twith 6927 stored elements in Compressed Sparse Row format>" ] }, "execution_count": 80, "metadata": {}, "output_type": "execute_result" } ], "source": [ "preprocessor_all.fit_transform(X_train)" ] }, { "cell_type": "code", "execution_count": 81, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.001 (+/- 0.000)0.001 (+/- 0.000)0.515 (+/- 0.002)0.515 (+/- 0.000)
Decision Tree (numeric-only)0.003 (+/- 0.000)0.001 (+/- 0.000)0.497 (+/- 0.038)0.833 (+/- 0.010)
KNN (numeric-only)0.003 (+/- 0.001)0.004 (+/- 0.000)0.525 (+/- 0.034)0.674 (+/- 0.015)
SVM (numeric-only)0.012 (+/- 0.000)0.005 (+/- 0.000)0.587 (+/- 0.033)0.623 (+/- 0.006)
Decision Tree(non-text feats)0.009 (+/- 0.000)0.003 (+/- 0.000)0.590 (+/- 0.039)0.889 (+/- 0.008)
KNN(non-text feats)0.008 (+/- 0.000)0.004 (+/- 0.000)0.598 (+/- 0.023)0.737 (+/- 0.008)
SVM(non-text feats)0.019 (+/- 0.000)0.008 (+/- 0.000)0.687 (+/- 0.011)0.733 (+/- 0.008)
Decision Tree(text)0.008 (+/- 0.001)0.001 (+/- 0.000)0.618 (+/- 0.036)0.735 (+/- 0.004)
KNN(text)0.004 (+/- 0.000)0.006 (+/- 0.002)0.572 (+/- 0.023)0.646 (+/- 0.026)
SVM(text)0.010 (+/- 0.000)0.003 (+/- 0.000)0.649 (+/- 0.022)0.728 (+/- 0.005)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.001 (+/- 0.000) 0.001 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.003 (+/- 0.000) 0.001 (+/- 0.000) \n", "KNN (numeric-only) 0.003 (+/- 0.001) 0.004 (+/- 0.000) \n", "SVM (numeric-only) 0.012 (+/- 0.000) 0.005 (+/- 0.000) \n", "Decision Tree(non-text feats) 0.009 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN(non-text feats) 0.008 (+/- 0.000) 0.004 (+/- 0.000) \n", "SVM(non-text feats) 0.019 (+/- 0.000) 0.008 (+/- 0.000) \n", "Decision Tree(text) 0.008 (+/- 0.001) 0.001 (+/- 0.000) \n", "KNN(text) 0.004 (+/- 0.000) 0.006 (+/- 0.002) \n", "SVM(text) 0.010 (+/- 0.000) 0.003 (+/- 0.000) \n", "\n", " test_score train_score \n", "dummy 0.515 (+/- 0.002) 0.515 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.497 (+/- 0.038) 0.833 (+/- 0.010) \n", "KNN (numeric-only) 0.525 (+/- 0.034) 0.674 (+/- 0.015) \n", "SVM (numeric-only) 0.587 (+/- 0.033) 0.623 (+/- 0.006) \n", "Decision Tree(non-text feats) 0.590 (+/- 0.039) 0.889 (+/- 0.008) \n", "KNN(non-text feats) 0.598 (+/- 0.023) 0.737 (+/- 0.008) \n", "SVM(non-text feats) 0.687 (+/- 0.011) 0.733 (+/- 0.008) \n", "Decision Tree(text) 0.618 (+/- 0.036) 0.735 (+/- 0.004) \n", "KNN(text) 0.572 (+/- 0.023) 0.646 (+/- 0.026) \n", "SVM(text) 0.649 (+/- 0.022) 0.728 (+/- 0.005) " ] }, "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for (name, model) in models.items():\n", " pipe_model = make_pipeline(text_transformer, model)\n", " results_df[name + '(text)'] = mean_std_cross_val_scores(pipe_model, X_train[['comments']], y_train, return_train_score=True)\n", "pd.DataFrame(results_df).T" ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fit_timescore_timetest_scoretrain_score
dummy0.001 (+/- 0.000)0.001 (+/- 0.000)0.515 (+/- 0.002)0.515 (+/- 0.000)
Decision Tree (numeric-only)0.003 (+/- 0.000)0.001 (+/- 0.000)0.497 (+/- 0.038)0.833 (+/- 0.010)
KNN (numeric-only)0.003 (+/- 0.001)0.004 (+/- 0.000)0.525 (+/- 0.034)0.674 (+/- 0.015)
SVM (numeric-only)0.012 (+/- 0.000)0.005 (+/- 0.000)0.587 (+/- 0.033)0.623 (+/- 0.006)
Decision Tree(non-text feats)0.009 (+/- 0.000)0.003 (+/- 0.000)0.590 (+/- 0.039)0.889 (+/- 0.008)
KNN(non-text feats)0.008 (+/- 0.000)0.004 (+/- 0.000)0.598 (+/- 0.023)0.737 (+/- 0.008)
SVM(non-text feats)0.019 (+/- 0.000)0.008 (+/- 0.000)0.687 (+/- 0.011)0.733 (+/- 0.008)
Decision Tree(text)0.008 (+/- 0.001)0.001 (+/- 0.000)0.618 (+/- 0.036)0.735 (+/- 0.004)
KNN(text)0.004 (+/- 0.000)0.006 (+/- 0.002)0.572 (+/- 0.023)0.646 (+/- 0.026)
SVM(text)0.010 (+/- 0.000)0.003 (+/- 0.000)0.649 (+/- 0.022)0.728 (+/- 0.005)
Decision Tree(all)0.016 (+/- 0.001)0.005 (+/- 0.001)0.624 (+/- 0.022)0.893 (+/- 0.006)
KNN(all)0.013 (+/- 0.000)0.012 (+/- 0.001)0.625 (+/- 0.027)0.748 (+/- 0.015)
SVM(all)0.023 (+/- 0.000)0.008 (+/- 0.001)0.699 (+/- 0.017)0.786 (+/- 0.008)
\n", "
" ], "text/plain": [ " fit_time score_time \\\n", "dummy 0.001 (+/- 0.000) 0.001 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.003 (+/- 0.000) 0.001 (+/- 0.000) \n", "KNN (numeric-only) 0.003 (+/- 0.001) 0.004 (+/- 0.000) \n", "SVM (numeric-only) 0.012 (+/- 0.000) 0.005 (+/- 0.000) \n", "Decision Tree(non-text feats) 0.009 (+/- 0.000) 0.003 (+/- 0.000) \n", "KNN(non-text feats) 0.008 (+/- 0.000) 0.004 (+/- 0.000) \n", "SVM(non-text feats) 0.019 (+/- 0.000) 0.008 (+/- 0.000) \n", "Decision Tree(text) 0.008 (+/- 0.001) 0.001 (+/- 0.000) \n", "KNN(text) 0.004 (+/- 0.000) 0.006 (+/- 0.002) \n", "SVM(text) 0.010 (+/- 0.000) 0.003 (+/- 0.000) \n", "Decision Tree(all) 0.016 (+/- 0.001) 0.005 (+/- 0.001) \n", "KNN(all) 0.013 (+/- 0.000) 0.012 (+/- 0.001) \n", "SVM(all) 0.023 (+/- 0.000) 0.008 (+/- 0.001) \n", "\n", " test_score train_score \n", "dummy 0.515 (+/- 0.002) 0.515 (+/- 0.000) \n", "Decision Tree (numeric-only) 0.497 (+/- 0.038) 0.833 (+/- 0.010) \n", "KNN (numeric-only) 0.525 (+/- 0.034) 0.674 (+/- 0.015) \n", "SVM (numeric-only) 0.587 (+/- 0.033) 0.623 (+/- 0.006) \n", "Decision Tree(non-text feats) 0.590 (+/- 0.039) 0.889 (+/- 0.008) \n", "KNN(non-text feats) 0.598 (+/- 0.023) 0.737 (+/- 0.008) \n", "SVM(non-text feats) 0.687 (+/- 0.011) 0.733 (+/- 0.008) \n", "Decision Tree(text) 0.618 (+/- 0.036) 0.735 (+/- 0.004) \n", "KNN(text) 0.572 (+/- 0.023) 0.646 (+/- 0.026) \n", "SVM(text) 0.649 (+/- 0.022) 0.728 (+/- 0.005) \n", "Decision Tree(all) 0.624 (+/- 0.022) 0.893 (+/- 0.006) \n", "KNN(all) 0.625 (+/- 0.027) 0.748 (+/- 0.015) \n", "SVM(all) 0.699 (+/- 0.017) 0.786 (+/- 0.008) " ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for (name, model) in models.items():\n", " pipe_model = make_pipeline(preprocessor_all, model)\n", " results_df[name + '(all)'] = mean_std_cross_val_scores(pipe_model, X_train, y_train, return_train_score=True)\n", "pd.DataFrame(results_df).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some improvement when we combine all features! " ] } ], "metadata": { "celltoolbar": "Slideshow", "kernelspec": { "display_name": "571", "language": "python", "name": "571" }, "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.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }