{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Classifying Against Categorical Variables]\n", "\n", "\n", "If you are trying to build something like a k-NN classifier applied to items in a dataframe where some of the defining characteristics are categorical, rather than numerical variables, what do you do?\n", "\n", "\n", "The problem with k-NN is that you need a distance metric, and it is not obvious how to define appropriate distance metrics for categorical variables.\n", "\n", "Some categorical variables may have a natural mapping into a number space (*'cold'*,*'warm'*,*'hot'*) so you could define mappings for those easily enough:" ] }, { "cell_type": "code", "execution_count": 2, "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", "
temptempvals
0cold0
1warm50
2cold0
3veryhot100
4hot70
\n", "
" ], "text/plain": [ " temp tempvals\n", "0 cold 0\n", "1 warm 50\n", "2 cold 0\n", "3 veryhot 100\n", "4 hot 70" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "df = pd.DataFrame({'temp':['cold', 'warm', 'cold', 'veryhot', 'hot']})\n", "\n", "tempval = {'cold': 0, 'warm': 50, 'hot':70,'veryhot':100}\n", "\n", "df['tempvals'] = df['temp'].map(tempval)\n", "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But many categories are harder to map sensibly so that the distance metric makes sense.\n", "\n", "One thing to note is that https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html does support different distance measures set via the metric parameter (allowed values here: https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.DistanceMetric.html ); different metrics have different properties so you need to balance the metric with the coding scheme.\n", "\n", "You can explore what values different metrics give using this sort of pattern:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 0., 20., 100.],\n", " [ 20., 0., 80.],\n", " [100., 80., 0.]])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.neighbors import DistanceMetric\n", "\n", "#Define a metric\n", "dist = DistanceMetric.get_metric('euclidean')\n", "\n", "#Find the distance between pairs\n", "#The first row is the distance from the first item to the first, second, and third item\n", "#The second row is the distance from the second item to the first, second, and third item\n", "#etc\n", "dist.pairwise([[0],[20],[100]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To explore the measured values you get in a dataframe column, generate a list of the pairs of values:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[(0, 50), (0, 100), (0, 70), (50, 100), (50, 70), (100, 70)]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import itertools\n", "pairs = [p for p in itertools.combinations(df['tempvals'].unique(), 2)]\n", "pairs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "then measure their distances under a particular measure to see if you are happy with it:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 0. , 50. , 20. , 70.71067812,\n", " 53.85164807, 101.98039027],\n", " [ 50. , 0. , 30. , 50. ,\n", " 58.30951895, 104.40306509],\n", " [ 20. , 30. , 0. , 58.30951895,\n", " 50. , 100. ],\n", " [ 70.71067812, 50. , 58.30951895, 0. ,\n", " 30. , 58.30951895],\n", " [ 53.85164807, 58.30951895, 50. , 30. ,\n", " 0. , 50. ],\n", " [101.98039027, 104.40306509, 100. , 58.30951895,\n", " 50. , 0. ]])" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dist = DistanceMetric.get_metric('euclidean')\n", "\n", "#So the first row is the distance between the (0,50) and each of the other items etc\n", "dist.pairwise(pairs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note you can use other metrics or define your own:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 0., 20., 100.],\n", " [ 20., 40., 120.],\n", " [100., 120., 200.]])" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def mydist(x,y):\n", " ''' Not necessarily a useful metric! '''\n", " return x+y\n", "\n", "dist = DistanceMetric.get_metric(mydist)\n", "dist.pairwise([[0],[20],[100]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So you could define your own metric if you wanted to.\n", "\n", "A lot of stuff classed as \"AI\" is based on making classifications based on quite naive (or just convenient to calculate) coding schemes.\n", "\n", "Remember the Wizard of Oz?!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Categorical Variables - A Better Way: Dummy Variables\n", "\n", "The k-NN classifer needs to measure the distance between values somehow, but with a categorical variable, the problem we are faced with is: *how do you sensibly measure the distance between any two items?*\n", "\n", "If you try to code a categorical variable (where the value are categories; e.g. for furniture: *chair*, *table*, *settee*), trying to map a variable's value onto a single `float` or `int` value that makes sense when trying to find a sensible distance between the values applied to any two categories. \n", "\n", "Yes, it's easy enough to give a numerical code to each item (*chair=1*, *table=2*, *settee=3*); but how do you arrange those items on a number line so you can find a meaningful distance between them?\n", "\n", "Remember [Stevens' NOIR](https://learn2.open.ac.uk/mod/oucontent/olinkremote.php?website=TM351&targetdoc=Part+2+Acquiring+and+representing+data&targetptr=2.4): the ordering of nominal items is arbitrary.\n", "\n", "Instead, if you have a thing (a row in your dataset) with a furniture column, you could map items as identified in the furniture column onto a set of columns, one for each type of furniture.\n", "\n", "Take the following data frame as an example:" ] }, { "cell_type": "code", "execution_count": 14, "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", "
tempweather
0coldrainy
1warmsunny
2coldovercast
3veryhotsunny
4hotsunny
\n", "
" ], "text/plain": [ " temp weather\n", "0 cold rainy\n", "1 warm sunny\n", "2 cold overcast\n", "3 veryhot sunny\n", "4 hot sunny" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "df3 = pd.DataFrame({'temp':['cold', 'warm', 'cold', 'veryhot', 'hot'],\n", " 'weather':['rainy','sunny','overcast','sunny','sunny']})\n", "df3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The weather is a categorical item that is harder to represent numerically that the categorical temperature variable.\n", "\n", "But what if we define a column for each weather type, and then encode a row with a value that associates the row with that weather type.\n", "\n", "These are *dummy variables*. For each row, set the dummy variable to 1 if it is true for the thing, 0 if it isn't.\n", "\n", "And *pandas* has a convenient function for generating such variable: `pd.get_dummies()`.\n", "\n", "Simply pass it a column of string values and it will create a column for each value, with a 1 or a zero saying whether the original column contained that categorical value or not." ] }, { "cell_type": "code", "execution_count": 13, "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", "
overcastrainysunny
0010
1001
2100
3001
4001
\n", "
" ], "text/plain": [ " overcast rainy sunny\n", "0 0 1 0\n", "1 0 0 1\n", "2 1 0 0\n", "3 0 0 1\n", "4 0 0 1" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.get_dummies(df3['weather'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's easy enough to add the dummy columns to the original data frame:" ] }, { "cell_type": "code", "execution_count": 15, "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", "
tempweatherovercastrainysunny
0coldrainy010
1warmsunny001
2coldovercast100
3veryhotsunny001
4hotsunny001
\n", "
" ], "text/plain": [ " temp weather overcast rainy sunny\n", "0 cold rainy 0 1 0\n", "1 warm sunny 0 0 1\n", "2 cold overcast 1 0 0\n", "3 veryhot sunny 0 0 1\n", "4 hot sunny 0 0 1" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df3 = pd.concat([df3, pd.get_dummies(df3['weather'])], axis=1)\n", "df3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the above case, you get a column for each type of weather, with a 1 or 0 identifying whether the weather was that type of weather (!).\n", "\n", "The `pd.get_dummies()` function essentially takes a column in long format, creates a new column for each unique item in that column (that is, maps the long column on a wide set of columns), and uses a binary code to associate / encode the wide columns with the value that appeared in the long column.\n", "\n", "When you measure the distance between the weather items represented using dummy variables, you measure the distance across those three dimensions (or however many weather dimensions there are).\n", "\n", "You can now classify using those numerical dummy variable columns (which contain a meaningful number: 1 for it's that sort of weather, 0 for it isn't) rather than the nominal weather column. The k-NN classifier will accept as many dimensions as you give it.\n", "\n", "Rather than classify on two dimensions (temp and some attempted numerical coding of weather), classify on four numerical cols (*temp*, *rainy*, *sunny*, *overcast*).\n", "\n", "If you're comparing shopping carts, for example, to try to identify different sorts of shopper, you might have a column for every possible item that a supermarket sells. For any given shopping trolley, put a number, N, or zero for if a person bought N items or 0 of each thing. That would be quite a naive encoding but it'd be a start. (For a readable overview of how they approached this with the original Tesco Clubcard, see [Scoring Points: How Tesco Continues to Win Customer Loyalty, Terry Hunt, Clive Humby, Tim Phillips](https://www.abebooks.co.uk/servlet/SearchResults?kn=scoring%20points&sortby=17); my review [here](https://blog.ouseful.info/2008/11/06/the-tesco-data-business-notes-on-scoring-points/).)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" } }, "nbformat": 4, "nbformat_minor": 2 }