2396 lines
185 KiB
Plaintext
2396 lines
185 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Problem Set 5: Credit Card Fraud Detection\n",
|
||
"\n",
|
||
"**Release Date:** 12 March 2024\n",
|
||
"\n",
|
||
"**Due Date:** 23:59, 30 March 2024"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Overview\n",
|
||
"\n",
|
||
"In class, we discussed about logistic regression, and how it can be useful as a classification algorithm. In this problem set, we get some hands-on practice by implementing logistic regression on a Credit Card Fraud Detection dataset. Note that for this problem set, you should only be using the scikit-learn (sklearn) library for the last part (Tasks 5.x) on SVM.\n",
|
||
"\n",
|
||
"**Required Files**:\n",
|
||
"\n",
|
||
"* ps5.ipynb\n",
|
||
"* credit_card.csv\n",
|
||
"* restaurant_data.csv\n",
|
||
"\n",
|
||
"**Honour Code**: Note that plagiarism will not be condoned! You may discuss with your classmates and check the internet for references, but you MUST NOT submit code/report that is copied directly from other sources!"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Important\n",
|
||
"\n",
|
||
"Similar to PS0, your implementation in the following tasks **should NOT involve any iteration, including `map` and `filter`, or recursion**. Instead, you should work with the operations available in NumPy. Solutions that violate this will be penalised.\n",
|
||
"\n",
|
||
"- You are allowed to use any mathematical functions, but this does not mean that you are allowed to use any NumPy function (there are NumPy functions that aren’t mathematical functions). For example, `np.vectorize` is not allowed since it is iterative. If you are in doubt about which functions are allowed, you should ask in the forum.\n",
|
||
"\n",
|
||
"There is, however, an exception for **Tasks 3.4 and 3.5**. In the pseudo-code for the algorithm required, there is an explicit for loop. Hence, only for these task you may use **a single for/while loop** to iterate for the number of epochs required."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Introduction\n",
|
||
"\n",
|
||
"Fraudulent credit card transaction is a common phenomenon in many parts of the world and can lead to potentially large amounts of losses for both companies and customers. Therefore, we hope to help credit card companies recognize those fraudulent transactions so that customers are not charged for items that they did not purchase.\n",
|
||
"\n",
|
||
"We are given a dataset that contains transactions made by credit cards holders in `credit_card.csv`. If we think about what type of data might be included in the input variables under the given context, we might realize that those input variables are likely to include word descriptions, such as shop name or locations. In this problem set, we don't need to worry about language processing as the data are pre-processed to contain only numeric values."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Getting Started\n",
|
||
"\n",
|
||
"Take a look at the columns in the dataset `credit_card.csv`. We have V1-V20, 'Amount', and 'Time' as input features, and 'Class' as output which takes the value 1 if it's fraud and 0 otherwise. This dataset presents 492 frauds out of 284,807 transactions. That means, there are 284,808 rows (including the header) in the csv file.\n",
|
||
"\n",
|
||
"We will use this dataset to implement logistic regression using batch and stochastic gradient descent for binary classification."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Initial imports and setup\n",
|
||
"\n",
|
||
"import numpy as np\n",
|
||
"import os\n",
|
||
"import pandas as pd\n",
|
||
"\n",
|
||
"from sklearn import svm\n",
|
||
"from sklearn import model_selection\n",
|
||
"\n",
|
||
"# Read credit card data into a Pandas dataframe for large tests\n",
|
||
"\n",
|
||
"dirname = os.getcwd()\n",
|
||
"credit_card_data_filepath = os.path.join(dirname, 'credit_card.csv')\n",
|
||
"restaurant_data_filepath = os.path.join(dirname, 'restaurant_data.csv')\n",
|
||
"\n",
|
||
"credit_df = pd.read_csv(credit_card_data_filepath)\n",
|
||
"X = credit_df.values[:, :-1]\n",
|
||
"y = credit_df.values[:, -1:]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Basics of Pandas\n",
|
||
"\n",
|
||
"[Pandas](https://pandas.pydata.org/) is an open source data analysis and manipulation tool in Python. In this problem set, we read the CSV into a [Pandas dataframe](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) to provide some nifty methods that makes it easier for us to handle large amounts of data. You can think of a dataframe as a large table that stores our dataset in a neat and optimized manner, making it fast for retrieval and manipulation and data. Using Pandas, we can quickly gain an overview of the type and values of the data stored, distributions of values within the dataset, and even ways to perform sampling.\n",
|
||
"\n",
|
||
"In the new few sections, we will explore some basic functions of Pandas to help us get started. You do not need to submit any codes for this section, and can simply run the cells to follow along."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Exploring the dataset"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Using the [`head`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.head.html) method on the dataframe, we can get an overview of the data. By default, the method returns the first 5 entries in the dataframe."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 30,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Time</th>\n",
|
||
" <th>V1</th>\n",
|
||
" <th>V2</th>\n",
|
||
" <th>V3</th>\n",
|
||
" <th>V4</th>\n",
|
||
" <th>V5</th>\n",
|
||
" <th>V6</th>\n",
|
||
" <th>V7</th>\n",
|
||
" <th>V8</th>\n",
|
||
" <th>V9</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>V13</th>\n",
|
||
" <th>V14</th>\n",
|
||
" <th>V15</th>\n",
|
||
" <th>V16</th>\n",
|
||
" <th>V17</th>\n",
|
||
" <th>V18</th>\n",
|
||
" <th>V19</th>\n",
|
||
" <th>V20</th>\n",
|
||
" <th>Amount</th>\n",
|
||
" <th>Class</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>-1.359807</td>\n",
|
||
" <td>-0.072781</td>\n",
|
||
" <td>2.536347</td>\n",
|
||
" <td>1.378155</td>\n",
|
||
" <td>-0.338321</td>\n",
|
||
" <td>0.462388</td>\n",
|
||
" <td>0.239599</td>\n",
|
||
" <td>0.098698</td>\n",
|
||
" <td>0.363787</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.991390</td>\n",
|
||
" <td>-0.311169</td>\n",
|
||
" <td>1.468177</td>\n",
|
||
" <td>-0.470401</td>\n",
|
||
" <td>0.207971</td>\n",
|
||
" <td>0.025791</td>\n",
|
||
" <td>0.403993</td>\n",
|
||
" <td>0.251412</td>\n",
|
||
" <td>149.62</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>1.191857</td>\n",
|
||
" <td>0.266151</td>\n",
|
||
" <td>0.166480</td>\n",
|
||
" <td>0.448154</td>\n",
|
||
" <td>0.060018</td>\n",
|
||
" <td>-0.082361</td>\n",
|
||
" <td>-0.078803</td>\n",
|
||
" <td>0.085102</td>\n",
|
||
" <td>-0.255425</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.489095</td>\n",
|
||
" <td>-0.143772</td>\n",
|
||
" <td>0.635558</td>\n",
|
||
" <td>0.463917</td>\n",
|
||
" <td>-0.114805</td>\n",
|
||
" <td>-0.183361</td>\n",
|
||
" <td>-0.145783</td>\n",
|
||
" <td>-0.069083</td>\n",
|
||
" <td>2.69</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>1.0</td>\n",
|
||
" <td>-1.358354</td>\n",
|
||
" <td>-1.340163</td>\n",
|
||
" <td>1.773209</td>\n",
|
||
" <td>0.379780</td>\n",
|
||
" <td>-0.503198</td>\n",
|
||
" <td>1.800499</td>\n",
|
||
" <td>0.791461</td>\n",
|
||
" <td>0.247676</td>\n",
|
||
" <td>-1.514654</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.717293</td>\n",
|
||
" <td>-0.165946</td>\n",
|
||
" <td>2.345865</td>\n",
|
||
" <td>-2.890083</td>\n",
|
||
" <td>1.109969</td>\n",
|
||
" <td>-0.121359</td>\n",
|
||
" <td>-2.261857</td>\n",
|
||
" <td>0.524980</td>\n",
|
||
" <td>378.66</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>1.0</td>\n",
|
||
" <td>-0.966272</td>\n",
|
||
" <td>-0.185226</td>\n",
|
||
" <td>1.792993</td>\n",
|
||
" <td>-0.863291</td>\n",
|
||
" <td>-0.010309</td>\n",
|
||
" <td>1.247203</td>\n",
|
||
" <td>0.237609</td>\n",
|
||
" <td>0.377436</td>\n",
|
||
" <td>-1.387024</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.507757</td>\n",
|
||
" <td>-0.287924</td>\n",
|
||
" <td>-0.631418</td>\n",
|
||
" <td>-1.059647</td>\n",
|
||
" <td>-0.684093</td>\n",
|
||
" <td>1.965775</td>\n",
|
||
" <td>-1.232622</td>\n",
|
||
" <td>-0.208038</td>\n",
|
||
" <td>123.50</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>2.0</td>\n",
|
||
" <td>-1.158233</td>\n",
|
||
" <td>0.877737</td>\n",
|
||
" <td>1.548718</td>\n",
|
||
" <td>0.403034</td>\n",
|
||
" <td>-0.407193</td>\n",
|
||
" <td>0.095921</td>\n",
|
||
" <td>0.592941</td>\n",
|
||
" <td>-0.270533</td>\n",
|
||
" <td>0.817739</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>1.345852</td>\n",
|
||
" <td>-1.119670</td>\n",
|
||
" <td>0.175121</td>\n",
|
||
" <td>-0.451449</td>\n",
|
||
" <td>-0.237033</td>\n",
|
||
" <td>-0.038195</td>\n",
|
||
" <td>0.803487</td>\n",
|
||
" <td>0.408542</td>\n",
|
||
" <td>69.99</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>5 rows × 23 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Time V1 V2 V3 V4 V5 V6 V7 \\\n",
|
||
"0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 \n",
|
||
"1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 \n",
|
||
"2 1.0 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 0.791461 \n",
|
||
"3 1.0 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 1.247203 0.237609 \n",
|
||
"4 2.0 -1.158233 0.877737 1.548718 0.403034 -0.407193 0.095921 0.592941 \n",
|
||
"\n",
|
||
" V8 V9 ... V13 V14 V15 V16 V17 \\\n",
|
||
"0 0.098698 0.363787 ... -0.991390 -0.311169 1.468177 -0.470401 0.207971 \n",
|
||
"1 0.085102 -0.255425 ... 0.489095 -0.143772 0.635558 0.463917 -0.114805 \n",
|
||
"2 0.247676 -1.514654 ... 0.717293 -0.165946 2.345865 -2.890083 1.109969 \n",
|
||
"3 0.377436 -1.387024 ... 0.507757 -0.287924 -0.631418 -1.059647 -0.684093 \n",
|
||
"4 -0.270533 0.817739 ... 1.345852 -1.119670 0.175121 -0.451449 -0.237033 \n",
|
||
"\n",
|
||
" V18 V19 V20 Amount Class \n",
|
||
"0 0.025791 0.403993 0.251412 149.62 0 \n",
|
||
"1 -0.183361 -0.145783 -0.069083 2.69 0 \n",
|
||
"2 -0.121359 -2.261857 0.524980 378.66 0 \n",
|
||
"3 1.965775 -1.232622 -0.208038 123.50 0 \n",
|
||
"4 -0.038195 0.803487 0.408542 69.99 0 \n",
|
||
"\n",
|
||
"[5 rows x 23 columns]"
|
||
]
|
||
},
|
||
"execution_count": 30,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"credit_df.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Next, we can inspect the [value counts](https://pandas.pydata.org/docs/reference/api/pandas.Series.value_counts.html) of the 'Class' property in the dataframe to know the number of fraudulent and non-fraudulent transactions."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 31,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"0 284315\n",
|
||
"1 492\n",
|
||
"Name: Class, dtype: int64"
|
||
]
|
||
},
|
||
"execution_count": 31,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Inspect the number of fraudulent and non-fraudulent transactions.\n",
|
||
"credit_df['Class'].value_counts()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Indexing and selecting data"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Similar to NumPy, we can also index and select data on Pandas dataframes."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"For example, we can [select columns in the dataframe by their labels](https://pandas.pydata.org/docs/user_guide/indexing.html#basics). In the following example, we use `'Class'` to index the 'Class' column in the credit dataframe."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 32,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"0 0\n",
|
||
"1 0\n",
|
||
"2 0\n",
|
||
"3 0\n",
|
||
"4 0\n",
|
||
" ..\n",
|
||
"284802 0\n",
|
||
"284803 0\n",
|
||
"284804 0\n",
|
||
"284805 0\n",
|
||
"284806 0\n",
|
||
"Name: Class, Length: 284807, dtype: int64"
|
||
]
|
||
},
|
||
"execution_count": 32,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Select the 'Class' column in the credit dataframe\n",
|
||
"credit_df['Class']"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Or, we can use integer indexing."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 33,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Time</th>\n",
|
||
" <th>V1</th>\n",
|
||
" <th>V2</th>\n",
|
||
" <th>V3</th>\n",
|
||
" <th>V4</th>\n",
|
||
" <th>V5</th>\n",
|
||
" <th>V6</th>\n",
|
||
" <th>V7</th>\n",
|
||
" <th>V8</th>\n",
|
||
" <th>V9</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>V13</th>\n",
|
||
" <th>V14</th>\n",
|
||
" <th>V15</th>\n",
|
||
" <th>V16</th>\n",
|
||
" <th>V17</th>\n",
|
||
" <th>V18</th>\n",
|
||
" <th>V19</th>\n",
|
||
" <th>V20</th>\n",
|
||
" <th>Amount</th>\n",
|
||
" <th>Class</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>-1.359807</td>\n",
|
||
" <td>-0.072781</td>\n",
|
||
" <td>2.536347</td>\n",
|
||
" <td>1.378155</td>\n",
|
||
" <td>-0.338321</td>\n",
|
||
" <td>0.462388</td>\n",
|
||
" <td>0.239599</td>\n",
|
||
" <td>0.098698</td>\n",
|
||
" <td>0.363787</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.991390</td>\n",
|
||
" <td>-0.311169</td>\n",
|
||
" <td>1.468177</td>\n",
|
||
" <td>-0.470401</td>\n",
|
||
" <td>0.207971</td>\n",
|
||
" <td>0.025791</td>\n",
|
||
" <td>0.403993</td>\n",
|
||
" <td>0.251412</td>\n",
|
||
" <td>149.62</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>1.191857</td>\n",
|
||
" <td>0.266151</td>\n",
|
||
" <td>0.166480</td>\n",
|
||
" <td>0.448154</td>\n",
|
||
" <td>0.060018</td>\n",
|
||
" <td>-0.082361</td>\n",
|
||
" <td>-0.078803</td>\n",
|
||
" <td>0.085102</td>\n",
|
||
" <td>-0.255425</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.489095</td>\n",
|
||
" <td>-0.143772</td>\n",
|
||
" <td>0.635558</td>\n",
|
||
" <td>0.463917</td>\n",
|
||
" <td>-0.114805</td>\n",
|
||
" <td>-0.183361</td>\n",
|
||
" <td>-0.145783</td>\n",
|
||
" <td>-0.069083</td>\n",
|
||
" <td>2.69</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>2 rows × 23 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Time V1 V2 V3 V4 V5 V6 V7 \\\n",
|
||
"0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 \n",
|
||
"1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 \n",
|
||
"\n",
|
||
" V8 V9 ... V13 V14 V15 V16 V17 \\\n",
|
||
"0 0.098698 0.363787 ... -0.991390 -0.311169 1.468177 -0.470401 0.207971 \n",
|
||
"1 0.085102 -0.255425 ... 0.489095 -0.143772 0.635558 0.463917 -0.114805 \n",
|
||
"\n",
|
||
" V18 V19 V20 Amount Class \n",
|
||
"0 0.025791 0.403993 0.251412 149.62 0 \n",
|
||
"1 -0.183361 -0.145783 -0.069083 2.69 0 \n",
|
||
"\n",
|
||
"[2 rows x 23 columns]"
|
||
]
|
||
},
|
||
"execution_count": 33,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Obtain the first 2 rows\n",
|
||
"credit_df[0:2]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"We can also [select columns in the dataframe that fulfils some condition](https://pandas.pydata.org/docs/user_guide/indexing.html#boolean-indexing). In the example below, `credit_df['Class'] == 0` returns a [Pandas Series](https://pandas.pydata.org/docs/reference/api/pandas.Series.html) of length 284807, which is the size of our dataset. It contains the value `True` if and only if the `Class` value for the particular entry is of value 0, and `False` otherwise. We can use this Boolean series to index the credit dataframe to return the rows where the `Class` field is 0. Does this remind you of how NumPy arrays operate?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 34,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"0 True\n",
|
||
"1 True\n",
|
||
"2 True\n",
|
||
"3 True\n",
|
||
"4 True\n",
|
||
" ... \n",
|
||
"284802 True\n",
|
||
"284803 True\n",
|
||
"284804 True\n",
|
||
"284805 True\n",
|
||
"284806 True\n",
|
||
"Name: Class, Length: 284807, dtype: bool"
|
||
]
|
||
},
|
||
"execution_count": 34,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"credit_df['Class'] == 0"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 35,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Time</th>\n",
|
||
" <th>V1</th>\n",
|
||
" <th>V2</th>\n",
|
||
" <th>V3</th>\n",
|
||
" <th>V4</th>\n",
|
||
" <th>V5</th>\n",
|
||
" <th>V6</th>\n",
|
||
" <th>V7</th>\n",
|
||
" <th>V8</th>\n",
|
||
" <th>V9</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>V13</th>\n",
|
||
" <th>V14</th>\n",
|
||
" <th>V15</th>\n",
|
||
" <th>V16</th>\n",
|
||
" <th>V17</th>\n",
|
||
" <th>V18</th>\n",
|
||
" <th>V19</th>\n",
|
||
" <th>V20</th>\n",
|
||
" <th>Amount</th>\n",
|
||
" <th>Class</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>-1.359807</td>\n",
|
||
" <td>-0.072781</td>\n",
|
||
" <td>2.536347</td>\n",
|
||
" <td>1.378155</td>\n",
|
||
" <td>-0.338321</td>\n",
|
||
" <td>0.462388</td>\n",
|
||
" <td>0.239599</td>\n",
|
||
" <td>0.098698</td>\n",
|
||
" <td>0.363787</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.991390</td>\n",
|
||
" <td>-0.311169</td>\n",
|
||
" <td>1.468177</td>\n",
|
||
" <td>-0.470401</td>\n",
|
||
" <td>0.207971</td>\n",
|
||
" <td>0.025791</td>\n",
|
||
" <td>0.403993</td>\n",
|
||
" <td>0.251412</td>\n",
|
||
" <td>149.62</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>1.191857</td>\n",
|
||
" <td>0.266151</td>\n",
|
||
" <td>0.166480</td>\n",
|
||
" <td>0.448154</td>\n",
|
||
" <td>0.060018</td>\n",
|
||
" <td>-0.082361</td>\n",
|
||
" <td>-0.078803</td>\n",
|
||
" <td>0.085102</td>\n",
|
||
" <td>-0.255425</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.489095</td>\n",
|
||
" <td>-0.143772</td>\n",
|
||
" <td>0.635558</td>\n",
|
||
" <td>0.463917</td>\n",
|
||
" <td>-0.114805</td>\n",
|
||
" <td>-0.183361</td>\n",
|
||
" <td>-0.145783</td>\n",
|
||
" <td>-0.069083</td>\n",
|
||
" <td>2.69</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>1.0</td>\n",
|
||
" <td>-1.358354</td>\n",
|
||
" <td>-1.340163</td>\n",
|
||
" <td>1.773209</td>\n",
|
||
" <td>0.379780</td>\n",
|
||
" <td>-0.503198</td>\n",
|
||
" <td>1.800499</td>\n",
|
||
" <td>0.791461</td>\n",
|
||
" <td>0.247676</td>\n",
|
||
" <td>-1.514654</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.717293</td>\n",
|
||
" <td>-0.165946</td>\n",
|
||
" <td>2.345865</td>\n",
|
||
" <td>-2.890083</td>\n",
|
||
" <td>1.109969</td>\n",
|
||
" <td>-0.121359</td>\n",
|
||
" <td>-2.261857</td>\n",
|
||
" <td>0.524980</td>\n",
|
||
" <td>378.66</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>1.0</td>\n",
|
||
" <td>-0.966272</td>\n",
|
||
" <td>-0.185226</td>\n",
|
||
" <td>1.792993</td>\n",
|
||
" <td>-0.863291</td>\n",
|
||
" <td>-0.010309</td>\n",
|
||
" <td>1.247203</td>\n",
|
||
" <td>0.237609</td>\n",
|
||
" <td>0.377436</td>\n",
|
||
" <td>-1.387024</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.507757</td>\n",
|
||
" <td>-0.287924</td>\n",
|
||
" <td>-0.631418</td>\n",
|
||
" <td>-1.059647</td>\n",
|
||
" <td>-0.684093</td>\n",
|
||
" <td>1.965775</td>\n",
|
||
" <td>-1.232622</td>\n",
|
||
" <td>-0.208038</td>\n",
|
||
" <td>123.50</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>2.0</td>\n",
|
||
" <td>-1.158233</td>\n",
|
||
" <td>0.877737</td>\n",
|
||
" <td>1.548718</td>\n",
|
||
" <td>0.403034</td>\n",
|
||
" <td>-0.407193</td>\n",
|
||
" <td>0.095921</td>\n",
|
||
" <td>0.592941</td>\n",
|
||
" <td>-0.270533</td>\n",
|
||
" <td>0.817739</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>1.345852</td>\n",
|
||
" <td>-1.119670</td>\n",
|
||
" <td>0.175121</td>\n",
|
||
" <td>-0.451449</td>\n",
|
||
" <td>-0.237033</td>\n",
|
||
" <td>-0.038195</td>\n",
|
||
" <td>0.803487</td>\n",
|
||
" <td>0.408542</td>\n",
|
||
" <td>69.99</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>...</th>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284802</th>\n",
|
||
" <td>172786.0</td>\n",
|
||
" <td>-11.881118</td>\n",
|
||
" <td>10.071785</td>\n",
|
||
" <td>-9.834783</td>\n",
|
||
" <td>-2.066656</td>\n",
|
||
" <td>-5.364473</td>\n",
|
||
" <td>-2.606837</td>\n",
|
||
" <td>-4.918215</td>\n",
|
||
" <td>7.305334</td>\n",
|
||
" <td>1.914428</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.689256</td>\n",
|
||
" <td>4.626942</td>\n",
|
||
" <td>-0.924459</td>\n",
|
||
" <td>1.107641</td>\n",
|
||
" <td>1.991691</td>\n",
|
||
" <td>0.510632</td>\n",
|
||
" <td>-0.682920</td>\n",
|
||
" <td>1.475829</td>\n",
|
||
" <td>0.77</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284803</th>\n",
|
||
" <td>172787.0</td>\n",
|
||
" <td>-0.732789</td>\n",
|
||
" <td>-0.055080</td>\n",
|
||
" <td>2.035030</td>\n",
|
||
" <td>-0.738589</td>\n",
|
||
" <td>0.868229</td>\n",
|
||
" <td>1.058415</td>\n",
|
||
" <td>0.024330</td>\n",
|
||
" <td>0.294869</td>\n",
|
||
" <td>0.584800</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>1.214756</td>\n",
|
||
" <td>-0.675143</td>\n",
|
||
" <td>1.164931</td>\n",
|
||
" <td>-0.711757</td>\n",
|
||
" <td>-0.025693</td>\n",
|
||
" <td>-1.221179</td>\n",
|
||
" <td>-1.545556</td>\n",
|
||
" <td>0.059616</td>\n",
|
||
" <td>24.79</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284804</th>\n",
|
||
" <td>172788.0</td>\n",
|
||
" <td>1.919565</td>\n",
|
||
" <td>-0.301254</td>\n",
|
||
" <td>-3.249640</td>\n",
|
||
" <td>-0.557828</td>\n",
|
||
" <td>2.630515</td>\n",
|
||
" <td>3.031260</td>\n",
|
||
" <td>-0.296827</td>\n",
|
||
" <td>0.708417</td>\n",
|
||
" <td>0.432454</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.183699</td>\n",
|
||
" <td>-0.510602</td>\n",
|
||
" <td>1.329284</td>\n",
|
||
" <td>0.140716</td>\n",
|
||
" <td>0.313502</td>\n",
|
||
" <td>0.395652</td>\n",
|
||
" <td>-0.577252</td>\n",
|
||
" <td>0.001396</td>\n",
|
||
" <td>67.88</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284805</th>\n",
|
||
" <td>172788.0</td>\n",
|
||
" <td>-0.240440</td>\n",
|
||
" <td>0.530483</td>\n",
|
||
" <td>0.702510</td>\n",
|
||
" <td>0.689799</td>\n",
|
||
" <td>-0.377961</td>\n",
|
||
" <td>0.623708</td>\n",
|
||
" <td>-0.686180</td>\n",
|
||
" <td>0.679145</td>\n",
|
||
" <td>0.392087</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-1.042082</td>\n",
|
||
" <td>0.449624</td>\n",
|
||
" <td>1.962563</td>\n",
|
||
" <td>-0.608577</td>\n",
|
||
" <td>0.509928</td>\n",
|
||
" <td>1.113981</td>\n",
|
||
" <td>2.897849</td>\n",
|
||
" <td>0.127434</td>\n",
|
||
" <td>10.00</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284806</th>\n",
|
||
" <td>172792.0</td>\n",
|
||
" <td>-0.533413</td>\n",
|
||
" <td>-0.189733</td>\n",
|
||
" <td>0.703337</td>\n",
|
||
" <td>-0.506271</td>\n",
|
||
" <td>-0.012546</td>\n",
|
||
" <td>-0.649617</td>\n",
|
||
" <td>1.577006</td>\n",
|
||
" <td>-0.414650</td>\n",
|
||
" <td>0.486180</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.188093</td>\n",
|
||
" <td>-0.084316</td>\n",
|
||
" <td>0.041333</td>\n",
|
||
" <td>-0.302620</td>\n",
|
||
" <td>-0.660377</td>\n",
|
||
" <td>0.167430</td>\n",
|
||
" <td>-0.256117</td>\n",
|
||
" <td>0.382948</td>\n",
|
||
" <td>217.00</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>284315 rows × 23 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Time V1 V2 V3 V4 V5 \\\n",
|
||
"0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 \n",
|
||
"1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 \n",
|
||
"2 1.0 -1.358354 -1.340163 1.773209 0.379780 -0.503198 \n",
|
||
"3 1.0 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 \n",
|
||
"4 2.0 -1.158233 0.877737 1.548718 0.403034 -0.407193 \n",
|
||
"... ... ... ... ... ... ... \n",
|
||
"284802 172786.0 -11.881118 10.071785 -9.834783 -2.066656 -5.364473 \n",
|
||
"284803 172787.0 -0.732789 -0.055080 2.035030 -0.738589 0.868229 \n",
|
||
"284804 172788.0 1.919565 -0.301254 -3.249640 -0.557828 2.630515 \n",
|
||
"284805 172788.0 -0.240440 0.530483 0.702510 0.689799 -0.377961 \n",
|
||
"284806 172792.0 -0.533413 -0.189733 0.703337 -0.506271 -0.012546 \n",
|
||
"\n",
|
||
" V6 V7 V8 V9 ... V13 V14 \\\n",
|
||
"0 0.462388 0.239599 0.098698 0.363787 ... -0.991390 -0.311169 \n",
|
||
"1 -0.082361 -0.078803 0.085102 -0.255425 ... 0.489095 -0.143772 \n",
|
||
"2 1.800499 0.791461 0.247676 -1.514654 ... 0.717293 -0.165946 \n",
|
||
"3 1.247203 0.237609 0.377436 -1.387024 ... 0.507757 -0.287924 \n",
|
||
"4 0.095921 0.592941 -0.270533 0.817739 ... 1.345852 -1.119670 \n",
|
||
"... ... ... ... ... ... ... ... \n",
|
||
"284802 -2.606837 -4.918215 7.305334 1.914428 ... -0.689256 4.626942 \n",
|
||
"284803 1.058415 0.024330 0.294869 0.584800 ... 1.214756 -0.675143 \n",
|
||
"284804 3.031260 -0.296827 0.708417 0.432454 ... -0.183699 -0.510602 \n",
|
||
"284805 0.623708 -0.686180 0.679145 0.392087 ... -1.042082 0.449624 \n",
|
||
"284806 -0.649617 1.577006 -0.414650 0.486180 ... -0.188093 -0.084316 \n",
|
||
"\n",
|
||
" V15 V16 V17 V18 V19 V20 Amount \\\n",
|
||
"0 1.468177 -0.470401 0.207971 0.025791 0.403993 0.251412 149.62 \n",
|
||
"1 0.635558 0.463917 -0.114805 -0.183361 -0.145783 -0.069083 2.69 \n",
|
||
"2 2.345865 -2.890083 1.109969 -0.121359 -2.261857 0.524980 378.66 \n",
|
||
"3 -0.631418 -1.059647 -0.684093 1.965775 -1.232622 -0.208038 123.50 \n",
|
||
"4 0.175121 -0.451449 -0.237033 -0.038195 0.803487 0.408542 69.99 \n",
|
||
"... ... ... ... ... ... ... ... \n",
|
||
"284802 -0.924459 1.107641 1.991691 0.510632 -0.682920 1.475829 0.77 \n",
|
||
"284803 1.164931 -0.711757 -0.025693 -1.221179 -1.545556 0.059616 24.79 \n",
|
||
"284804 1.329284 0.140716 0.313502 0.395652 -0.577252 0.001396 67.88 \n",
|
||
"284805 1.962563 -0.608577 0.509928 1.113981 2.897849 0.127434 10.00 \n",
|
||
"284806 0.041333 -0.302620 -0.660377 0.167430 -0.256117 0.382948 217.00 \n",
|
||
"\n",
|
||
" Class \n",
|
||
"0 0 \n",
|
||
"1 0 \n",
|
||
"2 0 \n",
|
||
"3 0 \n",
|
||
"4 0 \n",
|
||
"... ... \n",
|
||
"284802 0 \n",
|
||
"284803 0 \n",
|
||
"284804 0 \n",
|
||
"284805 0 \n",
|
||
"284806 0 \n",
|
||
"\n",
|
||
"[284315 rows x 23 columns]"
|
||
]
|
||
},
|
||
"execution_count": 35,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Obtain the credit dataframe where the 'Class' field is 0\n",
|
||
"credit_df[credit_df['Class'] == 0]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"You can also [concatenate](https://pandas.pydata.org/docs/reference/api/pandas.concat.html) Pandas series or dataframes together! In this example, we explored how we can concat the first 2 rows and last 2 rows of the dataframe. Similar to NumPy, you would also need to specify the axis for concatenation."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 36,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Time</th>\n",
|
||
" <th>V1</th>\n",
|
||
" <th>V2</th>\n",
|
||
" <th>V3</th>\n",
|
||
" <th>V4</th>\n",
|
||
" <th>V5</th>\n",
|
||
" <th>V6</th>\n",
|
||
" <th>V7</th>\n",
|
||
" <th>V8</th>\n",
|
||
" <th>V9</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>V13</th>\n",
|
||
" <th>V14</th>\n",
|
||
" <th>V15</th>\n",
|
||
" <th>V16</th>\n",
|
||
" <th>V17</th>\n",
|
||
" <th>V18</th>\n",
|
||
" <th>V19</th>\n",
|
||
" <th>V20</th>\n",
|
||
" <th>Amount</th>\n",
|
||
" <th>Class</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>-1.359807</td>\n",
|
||
" <td>-0.072781</td>\n",
|
||
" <td>2.536347</td>\n",
|
||
" <td>1.378155</td>\n",
|
||
" <td>-0.338321</td>\n",
|
||
" <td>0.462388</td>\n",
|
||
" <td>0.239599</td>\n",
|
||
" <td>0.098698</td>\n",
|
||
" <td>0.363787</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.991390</td>\n",
|
||
" <td>-0.311169</td>\n",
|
||
" <td>1.468177</td>\n",
|
||
" <td>-0.470401</td>\n",
|
||
" <td>0.207971</td>\n",
|
||
" <td>0.025791</td>\n",
|
||
" <td>0.403993</td>\n",
|
||
" <td>0.251412</td>\n",
|
||
" <td>149.62</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>1.191857</td>\n",
|
||
" <td>0.266151</td>\n",
|
||
" <td>0.166480</td>\n",
|
||
" <td>0.448154</td>\n",
|
||
" <td>0.060018</td>\n",
|
||
" <td>-0.082361</td>\n",
|
||
" <td>-0.078803</td>\n",
|
||
" <td>0.085102</td>\n",
|
||
" <td>-0.255425</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.489095</td>\n",
|
||
" <td>-0.143772</td>\n",
|
||
" <td>0.635558</td>\n",
|
||
" <td>0.463917</td>\n",
|
||
" <td>-0.114805</td>\n",
|
||
" <td>-0.183361</td>\n",
|
||
" <td>-0.145783</td>\n",
|
||
" <td>-0.069083</td>\n",
|
||
" <td>2.69</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284805</th>\n",
|
||
" <td>172788.0</td>\n",
|
||
" <td>-0.240440</td>\n",
|
||
" <td>0.530483</td>\n",
|
||
" <td>0.702510</td>\n",
|
||
" <td>0.689799</td>\n",
|
||
" <td>-0.377961</td>\n",
|
||
" <td>0.623708</td>\n",
|
||
" <td>-0.686180</td>\n",
|
||
" <td>0.679145</td>\n",
|
||
" <td>0.392087</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-1.042082</td>\n",
|
||
" <td>0.449624</td>\n",
|
||
" <td>1.962563</td>\n",
|
||
" <td>-0.608577</td>\n",
|
||
" <td>0.509928</td>\n",
|
||
" <td>1.113981</td>\n",
|
||
" <td>2.897849</td>\n",
|
||
" <td>0.127434</td>\n",
|
||
" <td>10.00</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>284806</th>\n",
|
||
" <td>172792.0</td>\n",
|
||
" <td>-0.533413</td>\n",
|
||
" <td>-0.189733</td>\n",
|
||
" <td>0.703337</td>\n",
|
||
" <td>-0.506271</td>\n",
|
||
" <td>-0.012546</td>\n",
|
||
" <td>-0.649617</td>\n",
|
||
" <td>1.577006</td>\n",
|
||
" <td>-0.414650</td>\n",
|
||
" <td>0.486180</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>-0.188093</td>\n",
|
||
" <td>-0.084316</td>\n",
|
||
" <td>0.041333</td>\n",
|
||
" <td>-0.302620</td>\n",
|
||
" <td>-0.660377</td>\n",
|
||
" <td>0.167430</td>\n",
|
||
" <td>-0.256117</td>\n",
|
||
" <td>0.382948</td>\n",
|
||
" <td>217.00</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>4 rows × 23 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Time V1 V2 V3 V4 V5 V6 \\\n",
|
||
"0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 \n",
|
||
"1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 \n",
|
||
"284805 172788.0 -0.240440 0.530483 0.702510 0.689799 -0.377961 0.623708 \n",
|
||
"284806 172792.0 -0.533413 -0.189733 0.703337 -0.506271 -0.012546 -0.649617 \n",
|
||
"\n",
|
||
" V7 V8 V9 ... V13 V14 V15 \\\n",
|
||
"0 0.239599 0.098698 0.363787 ... -0.991390 -0.311169 1.468177 \n",
|
||
"1 -0.078803 0.085102 -0.255425 ... 0.489095 -0.143772 0.635558 \n",
|
||
"284805 -0.686180 0.679145 0.392087 ... -1.042082 0.449624 1.962563 \n",
|
||
"284806 1.577006 -0.414650 0.486180 ... -0.188093 -0.084316 0.041333 \n",
|
||
"\n",
|
||
" V16 V17 V18 V19 V20 Amount Class \n",
|
||
"0 -0.470401 0.207971 0.025791 0.403993 0.251412 149.62 0 \n",
|
||
"1 0.463917 -0.114805 -0.183361 -0.145783 -0.069083 2.69 0 \n",
|
||
"284805 -0.608577 0.509928 1.113981 2.897849 0.127434 10.00 0 \n",
|
||
"284806 -0.302620 -0.660377 0.167430 -0.256117 0.382948 217.00 0 \n",
|
||
"\n",
|
||
"[4 rows x 23 columns]"
|
||
]
|
||
},
|
||
"execution_count": 36,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"pd.concat([credit_df[:2], credit_df[-2:]], axis=0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Data Processing\n",
|
||
"\n",
|
||
"As you might have realised, managing Pandas is not so much different from how you would operate on NumPy or basic Python structures! We have provided some teaser on basic Pandas. Now, we will see how we can use Pandas to help us in resampling.\n",
|
||
"\n",
|
||
"In particular, you might find the [`sample` method](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.sample.html) helpful in this section."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 1.1: Problem with imbalanced data\n",
|
||
"\n",
|
||
"Our first task is to describe in one or two sentences what problem we might encounter if we directly use the given dataset without processing it.\n",
|
||
"\n",
|
||
"Hint: consider the 'Class' column, and think about how a high accuracy prediction can be achieved in a wrong way"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 1.2: Resampling methods\n",
|
||
"\n",
|
||
"When we are faced with the issue of imbalanced data, there are several ways to deal with it. A more direct way might be just to collect more data instances. We realized that in our case this doesn't work well because the events unevenly occur. We then look at how to resample the existing instances.\n",
|
||
"\n",
|
||
"In this problem set, you are introduced with three resampling methods: undersampling, oversampling, and SMOTE."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Concept 1.2.1: Undersampling\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/undersampling.png\" alt=\"visualisation of undersampling\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 1: Visualisation of undersampling.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"The figure above illustrates undersampling. In undersampling, we remove samples from the majority class. More specifically, we randomly take subsamples of the majority class such that the size of two classes is the same."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Task 1.2.1: Random undersampling in practice\n",
|
||
"\n",
|
||
"Your task is to observe the data in `credit_card.csv` and randomly remove some observations of the majority class until the two classes balance out."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 37,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def random_undersampling(df: pd.DataFrame) -> pd.DataFrame:\n",
|
||
" '''\n",
|
||
" Given credit card dataset with 0 as the majority for 'Class' column. Returns credit card data with two classes\n",
|
||
" having the same shape, given raw data read from csv.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" df: pd.DataFrame\n",
|
||
" The potentially imbalanced dataset.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" Undersampled dataset with equal number of fraudulent and legitimate data.\n",
|
||
" '''\n",
|
||
"\n",
|
||
" # TODO: add your solution here and remove `raise NotImplementedError`\n",
|
||
" ones = df[df['Class'] == 1]\n",
|
||
" return pd.concat([ones,df[df['Class'] == 0].sample(len(ones))], axis=0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 38,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# small data\n",
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [111.6, 10, 1], [111.4, 10, 0], [111.5, 10, 1], [111.6, 10, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"# checks that the randomly undersampled dataset has 3 fraudulent and 3 legitimate data points\n",
|
||
"assert np.all(np.equal(random_undersampling(df1)['Class'].value_counts(), (3, 3)))\n",
|
||
"\n",
|
||
"# credit card data\n",
|
||
"assert np.all(np.equal(random_undersampling(credit_df)['Class'].value_counts(), (492, 492)))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Now you might realize the drawback of undersampling - by removing data randomly, you might have removed some valuable information from the dataset. Is there any way to do better without losing the valuable information?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Concept 1.2.2: Oversampling\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/oversampling.png\" alt=\"visualisation of oversampling\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 2: Visualisation of oversampling.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"The figure above illustrates oversampling. In oversampling, we duplicate records of the minority class such that the size of two classes balance out."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Task 1.2.2: Oversampling in practice\n",
|
||
"\n",
|
||
"Your task is to observe the data in `credit_card.csv` and apply the oversampling technique to the dataset."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 39,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def random_oversampling(df: pd.DataFrame) -> pd.DataFrame:\n",
|
||
" '''\n",
|
||
" Given credit card dataset with 0 as the majority for 'Class' column. Returns credit card data with two classes\n",
|
||
" having the same shape, given raw data read from csv.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" df: pd.DataFrame\n",
|
||
" The potentially imbalanced dataset.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" Oversampled dataset with equal number of fraudulent and legitimate data.\n",
|
||
" '''\n",
|
||
"\n",
|
||
" # TODO: add your solution here and remove `raise NotImplementedError`\n",
|
||
" zeros = df[df['Class'] == 0]\n",
|
||
" ones = df[df['Class'] == 1]\n",
|
||
" return pd.concat([zeros, ones.sample(len(zeros), replace=True)], axis=0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 40,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# small data\n",
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], \n",
|
||
" [111.4, 10, 0], [111.5, 10, 0], [111.4, 10, 0], \n",
|
||
" [111.5, 10, 1], [111.6, 10, 1], [111.6, 10, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"assert np.all(np.equal(random_oversampling(df1)['Class'].value_counts(), (6, 6)))\n",
|
||
"\n",
|
||
"# credit card data\n",
|
||
"assert np.all(np.equal(random_oversampling(credit_df)['Class'].value_counts(), (284315, 284315)))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"You might realize the bright side of oversampling - you don't lose certain valuable information. At the same time, you also realize the drawback of oversampling - it can cause overfitting and a poor generalization of the test set. Now the question is, instead of simply duplicating records, is there any other way to increase the number of records in the minority set?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Concept 1.2.3: SMOTE (for further reading only)\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/smote.png\" alt=\"visualisation of SMOTE\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 3: Visualisation of SMOTE.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"The figure above illustrates Synthetic Minority Oversampling Technique (SMOTE).\n",
|
||
"\n",
|
||
"The SMOTE algorithm works in these four steps:\n",
|
||
"\n",
|
||
"1. Consider minority and majority instances in vector space.\n",
|
||
"1. For each minority-class instance pair, interpolate their feature values.\n",
|
||
"1. Randomly synthesize instances and label with minority class\n",
|
||
"1. More instances added to minority class\n",
|
||
"\n",
|
||
"In this case, you increase the number of minority instances without simply duplicating the values. Now your newly inserted minority record is not an exact copy of an existing data point, but it is also not too different from the known observations in your minority class. Outside this problem set, when we want to do data resampling, we can use the [Python imbalanced-learn library](https://pypi.org/project/imbalanced-learn/). We will not be implementing SMOTE algorithm for this problem set."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"#### Word of caution when resampling\n",
|
||
"\n",
|
||
"In the section above, we introduced three resampling techniques. One very important thing to note is, in practice you should first split dataset to train–test sets, then resample the train dataset before training the model. This is done to avoid data leakage (snooping).\n",
|
||
"\n",
|
||
"Data leakage can cause you to create overly optimistic if not completely invalid predictive models. It is when information from outside the training dataset is used to train the model. The additional information might allow the model to learn something that it otherwise would not know and in turn invalidate the estimated performance of the mode being constructed."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Splitting data\n",
|
||
"\n",
|
||
"Before we can start training our models, we need to **randomly** partition our dataset into training data and testing data. Remember that we are trying to make a model that can predict fraudulence of data points that the model has never seen. It would be unwise to measure the accuracy of the model using the same data it trained on."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 2.1: Splitting data\n",
|
||
"\n",
|
||
"In this task, you need to implement `train_test_split`. This function takes `X`, `y`, and `test_size` as arguments, and output `X_train`, `X_test`, `y_train`, and `y_test` you obtained."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 41,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def train_test_split(X: np.ndarray, y: np.ndarray, test_size: float=0.25):\n",
|
||
" '''\n",
|
||
" Randomly split the data into two sets.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) dataset (features).\n",
|
||
" y: np.ndarray\n",
|
||
" (m,) dataset (corresponding targets).\n",
|
||
" test_size: np.float64\n",
|
||
" fraction of the dataset to be put in the test dataset.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" A tuple of four elements (X_train, X_test, y_train, y_test):\n",
|
||
" X_train: np.ndarray\n",
|
||
" (m - k, n) training dataset (features).\n",
|
||
" X_test: np.ndarray\n",
|
||
" (k, n) testing dataset (features).\n",
|
||
" y_train: np.ndarray\n",
|
||
" (m - k,) training dataset (corresponding targets).\n",
|
||
" y_test: np.ndarray\n",
|
||
" (k,) testing dataset (corresponding targets).\n",
|
||
" '''\n",
|
||
"\n",
|
||
" m = X.shape[0]\n",
|
||
" k = int(m * test_size)\n",
|
||
" mask = np.full(m, False)\n",
|
||
" indices = np.random.choice(m, k, replace=False)\n",
|
||
" mask[indices] = True\n",
|
||
" inverse_mask = ~mask\n",
|
||
"\n",
|
||
" return X[inverse_mask], X[mask], y[inverse_mask], y[mask]\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 42,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 30, 0], \n",
|
||
" [111.4, 40, 0], [111.5, 50, 0], [111.6, 60, 1],\n",
|
||
" [111.7, 70, 0], [111.8, 80, 1], [111.9, 90, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"expected1 = [7, 2, 7, 2]\n",
|
||
"\n",
|
||
"X1_train, X1_test, y1_train, y1_test = train_test_split(X1, y1, test_size=0.25)\n",
|
||
"\n",
|
||
"assert len(X1_train) == expected1[0] and\\\n",
|
||
" len(X1_test) == expected1[1] and\\\n",
|
||
" len(y1_train) == expected1[2] and\\\n",
|
||
" len(y1_test) == expected1[3]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Logistic regression"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Gradient descent\n",
|
||
"\n",
|
||
"Recall the gradient descent method you learned and applied in the previous problem set. Gradient descent is an iterative optimization method which finds the minimum of a differentiable function. It's often used to find the coefficients that minimize the cost function. Here is a brief recap of how that's done:\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/gradient_descent.png\" alt=\"visualisation of gradient descent\" width=\"50%\">\n",
|
||
" <figcaption style=\"text-align:center\"><a href=\"https://medium.com/@divakar_239/stochastic-vs-batch-gradient-descent-8820568eada1\">Figure 4: Visualisation of gradient descent.</a></figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"We start at an initial weight vector $\\mathbf{w} = (w_0, w_1, ..., w_n)$. Then we take incremental steps along the steepest slope to get to the bottom where the minimum cost lies. Along the way, we keep updating the weight coefficients by computing the gradients using the training samples from the training dataset."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Concept: Batch gradient descent\n",
|
||
"\n",
|
||
"In batch gradient descent, **all** the training data (entire batch) is taken into consideration in a single step. For one step of gradient descent, we calculate $w_j \\leftarrow w_j - \\alpha \\frac{\\partial J(\\mathbf{w})}{\\partial w_j}$ (where $\\alpha$ is the learning rate) simultaneously for all $j$ based on all the training examples and then use that mean gradient to update our parameters.\n",
|
||
"\n",
|
||
"By continuing this for iterations until convergence, or until it's time to stop, we reach the optimal or near optimal parameters $\\mathbf{w}$ leading to minimum cost."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Concept: Stochastic gradient descent\n",
|
||
"\n",
|
||
"Think about what's a limitation of the (entire) batch gradient descent introduced earlier. What if the dataset is very large? In Stochastic Gradient Descent (SGD), we consider only the error on a single record $(x^{(*)}, y^{(*)})$ at a time. How does this work?\n",
|
||
"\n",
|
||
"During the training loop, we will randomly sample an instance in the dataset. This instance will be used to update the weights of the model $w_j \\leftarrow w_j - \\alpha \\frac{\\partial J(\\mathbf{w})}{\\partial w_j}$ (where $\\alpha$ is the learning rate) simultaneously for all $j$.\n",
|
||
"\n",
|
||
"Note that SGD does not necessarily decrease the batch loss in each iteration! However, on average, the loss will still decrease. Since we work on only a single record at any time, SGD provides the advantage of a much smaller memory requirement, and faster computation time. SGD also comes with its own set of disadvantages, for example, we lose benefits of any vectorization."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Concept: Gradient descent termination condition\n",
|
||
"\n",
|
||
"The next question is when to stop? The ideal case is to run until convergence, but convergence might be hard to obtain. Here are some criterias you can use:\n",
|
||
"\n",
|
||
"* Stop when error change is small and/or\n",
|
||
"* Stop when error is small\n",
|
||
"* Stop when maximum number of iterations is reached"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Now let's try to apply the batch gradient descent and stochastic gradient descent.\n",
|
||
"Task 3.1 to 3.4 will gradually guide you to complete the logistic regression using batch gradient descent, while task 3.5 expects you to implement the stochastic one. For the task in this problem set, you can assume that the bias column has been added for all input `X`. That means given `X` as an argument, you don't need to manually add the bias column again in your code."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.1: Cost function\n",
|
||
"\n",
|
||
"Recall that for logistic regression, we want an error function for an individual value to be:\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/piecewise_error.png\" alt=\"error function\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 5: Cost function.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"We can simplify the condition and transform it into the equivalent cost function:\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/error.png\" alt=\"error function as a line\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 6: Cost without conditions.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"In this task, you need to implement `cost_function` $E$ as mentioned above. This function takes `X`, `y`, and `weight_vector` $\\mathbf{w}$ as arguments, and returns the error $E$. Note that for this task, the $E$ should account for **all** the training data.\n",
|
||
"\n",
|
||
"Here, we are using the $\\log$ function and we need to handle the case of computing $\\log(0)$. There are many ways to handle this. In this task, we will handle $\\log(0)$ by using the machine epsilon for numpy `float64` type, and use the trick $\\log(x + eps)$ which allows $x$ to be $0$. If $x$ was any other value, $\\log(x + eps)$ would be very close to $\\log(x)$. This helps to improve numerical stability in computation."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 90,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def cost_function(X: np.ndarray, y: np.ndarray, weight_vector: np.ndarray):\n",
|
||
" '''\n",
|
||
" Cross entropy error for logistic regression\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) training dataset (features).\n",
|
||
" y: np.ndarray\n",
|
||
" (m,) training dataset (corresponding targets).\n",
|
||
" weight_vector: np.ndarray\n",
|
||
" (n,) weight parameters.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" Cost\n",
|
||
" '''\n",
|
||
" \n",
|
||
" # Machine epsilon for numpy `float64` type\n",
|
||
" eps = np.finfo(np.float64).eps\n",
|
||
"\n",
|
||
" y_predicted = 1/(1+np.exp(-X @ weight_vector))\n",
|
||
" first = -y * np.log(y_predicted+eps)\n",
|
||
" second = (1-y) * np.log(1-y_predicted+eps)\n",
|
||
"\n",
|
||
" return np.mean(first-second)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 91,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [111.6, 10, 1], [111.4, 10, 0], [111.5, 10, 1], [111.6, 10, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"w1 = np.transpose([0.002, 0.1220])\n",
|
||
"\n",
|
||
"assert np.round(cost_function(X1, y1, w1), 5) == np.round(1.29333, 5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.2: Weight update\n",
|
||
"\n",
|
||
"In this task, you need to implement `weight_update`. This function takes `X`, `y`, `alpha`, and a `weight_vector` as arguments, and output the new weight vector. Each call to the function should make one update on the weight vector."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 92,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def weight_update(X: np.ndarray, y: np.ndarray, alpha: np.float64, weight_vector: np.ndarray) -> np.ndarray:\n",
|
||
" '''\n",
|
||
" Do the weight update for one step in gradient descent\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) training dataset (features).\n",
|
||
" y: np.ndarray\n",
|
||
" (m,) training dataset (corresponding targets).\n",
|
||
" alpha: np.float64\n",
|
||
" logistic regression learning rate.1\n",
|
||
" weight_vector: np.ndarray\n",
|
||
" (n,) weight parameters.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" New weight vector after one round of update.\n",
|
||
" '''\n",
|
||
" y_predicted = 1/(1+np.exp(-X @ weight_vector))\n",
|
||
" \n",
|
||
" return weight_vector - alpha * X.T @ (y_predicted - y) / len(y)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 93,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [111.6, 10, 1],[111.4, 10, 0], [111.5, 10, 1], [111.6, 10, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"w1 = np.transpose([2.2000, 12.20000])\n",
|
||
"a1 = 1e-5\n",
|
||
"nw1 = np.array([2.199,12.2])\n",
|
||
"\n",
|
||
"assert np.array_equal(np.round(weight_update(X1, y1, a1, w1), 3), nw1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.3: Logistic regression classification\n",
|
||
"\n",
|
||
"Remember that logistic regression is used for classification even though the function gives a probability output. In this task, you classify each element in `X`, given `weight_vector` using `prob_threshold` as the threshold, and output the classification result as an `np.ndarray`.\n",
|
||
"\n",
|
||
"If the probability predicted by the `weight_vector` exceeds the `prob_threshold`, we should classify it as fraud (`y = 1`). Otherwise, we should classify it as legitimate (`y = 0`)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 94,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def logistic_regression_classification(X: np.ndarray, weight_vector: np.ndarray, prob_threshold: np.float64=0.5):\n",
|
||
" '''\n",
|
||
" Do classification task using logistic regression.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) training dataset (features).\n",
|
||
" weight_vector: np.ndarray\n",
|
||
" (n,) weight parameters.\n",
|
||
" prob_threshold: np.float64\n",
|
||
" the threshold for a prediction to be considered fraudulent.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" Classification result as an (m,) np.ndarray\n",
|
||
" '''\n",
|
||
" y_predicted = 1/(1+np.exp(-X @ weight_vector))\n",
|
||
" return (y_predicted > prob_threshold).astype(int)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 95,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [211.6, 80, 1],[111.4, 10, 0], [111.5, 80, 1], [211.6, 80, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"w1 = np.transpose([-0.000002, 0.000003])\n",
|
||
"expected1 = np.transpose([0, 0, 0, 0, 0, 0, 0, 1, 0])\n",
|
||
"result1 = logistic_regression_classification(X1, w1)\n",
|
||
"\n",
|
||
"assert result1.shape == expected1.shape and (result1 == expected1).all()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.4: Logistic regression using batch gradient descent\n",
|
||
"\n",
|
||
"In this task, you need to implement a fixed learning rate algorithm for logistic regression. This function takes `X_train`, `y_train`, `max_num_epochs`, `threshold`, and `alpha` as arguments, and output the final `weight_vector` you obtained. You can make use of the previous functions you have implemented earlier.\n",
|
||
"\n",
|
||
"Specifically, an epoch refers to one complete pass through the dataset. Hence, one iteration of gradient descent, which simultaneously updates the weight vector based on the cost function computed on the entire dataset, corresponds to one epoch. Batch gradient descent is terminated when the number of update rounds exceeds the maximum number of epochs (`max_num_epochs`) given, or when the computed error falls below the threshold value (`threshold`)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 96,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def logistic_regression_batch_gradient_descent(X_train: np.ndarray, y_train: np.ndarray, max_num_epochs: int = 250, threshold: np.float64 = 0.05, alpha: np.float64 = 1e-5):\n",
|
||
" '''\n",
|
||
" Initialize your weight to zeros. Write your terminating condition, and run the weight update for some iterations.\n",
|
||
" Get the resulting weight vector. Use that to do predictions, and output the final weight vector.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X_train: np.ndarray\n",
|
||
" (m, n) training dataset (features).\n",
|
||
" y_train: np.ndarray\n",
|
||
" (m,) training dataset (corresponding targets).\n",
|
||
" max_num_epochs: int\n",
|
||
" this should be one of the terminating conditions. \n",
|
||
" That means if you initialize num_update_rounds to 0, \n",
|
||
" then you should stop updating the weights when num_update_rounds >= max_num_epochs.\n",
|
||
" threshold: np.float64\n",
|
||
" terminating when error <= threshold value, or if you reach the max number of update rounds first.\n",
|
||
" alpha: np.float64\n",
|
||
" logistic regression learning rate.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" The final (n,) weight parameters\n",
|
||
" '''\n",
|
||
" weights = np.zeros(X_train.shape[1])\n",
|
||
" for _ in range(max_num_epochs):\n",
|
||
" weights = weight_update(X_train, y_train, alpha, weights)\n",
|
||
" if cost_function(X_train, y_train, weights) <= threshold:\n",
|
||
" break\n",
|
||
" return weights\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 97,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [211.6, 80, 1],[111.4, 10, 0], [111.5, 80, 1], [211.6, 80, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"max_num_epochs1 = 20\n",
|
||
"expected1 = np.transpose([-0.0011538251272737235, 0.0019538526326272542])\n",
|
||
"\n",
|
||
"assert np.array_equal(np.round(logistic_regression_batch_gradient_descent(X1, y1, max_num_epochs1), 5), np.round(expected1, 5))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.5: Logistic regression using stochastic gradient descent\n",
|
||
"\n",
|
||
"In this task, you need to implement logistic regression using stochastic gradient descent. This function takes `X_train`, `y_train`, `max_num_iterations`, `threshold`, and `alpha` as arguments, and output the final `weight_vector` you obtained. You can make use of the previous functions you have implemented earlier.\n",
|
||
"\n",
|
||
"For this task, you should use `np.random.choice` to randomly choose the training data for each iteration. You should also use the `seed` argument to set the random seed for reproducibility."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 98,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def weight_update_stochastic(X: np.ndarray, y: np.ndarray, alpha: np.float64, weight_vector: np.ndarray) -> np.ndarray:\n",
|
||
" y_predicted = 1/(1+np.exp(-X @ weight_vector))\n",
|
||
" grad = (X.T @ (y_predicted - y)) / X.shape[0]\n",
|
||
" return weight_vector - alpha * grad"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 99,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def logistic_regression_stochastic_gradient_descent(X_train: np.ndarray, y_train: np.ndarray, max_num_iterations: int=250, threshold: np.float64=0.05, alpha: np.float64=1e-5, seed :int=43) -> np.ndarray:\n",
|
||
" np.random.seed(seed)\n",
|
||
" n = X_train.shape[1]\n",
|
||
" weights = np.zeros(shape=(n,))\n",
|
||
" error = cost_function(X_train, y_train, weights)\n",
|
||
" for _ in range(max_num_iterations):\n",
|
||
" if error <= threshold:\n",
|
||
" return weights\n",
|
||
" index = np.random.choice(X_train.shape[0], 1)\n",
|
||
" weights = weight_update_stochastic(X_train[index], y_train[index], alpha, weights)\n",
|
||
" error = cost_function(X_train, y_train, weights)\n",
|
||
" return weights\n",
|
||
" "
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 100,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [211.6, 80, 1],[111.4, 10, 0], [111.5, 80, 1], [211.6, 80, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"expected1 = cost_function(X1, y1, np.transpose(np.zeros(X1.shape[1])))\n",
|
||
"\n",
|
||
"assert cost_function(X1, y1, logistic_regression_stochastic_gradient_descent(X1, y1)) < expected1"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.6.1: Stochastic gradient descent vs batch gradient descent\n",
|
||
"\n",
|
||
"Recall that it is ideal for gradient descent to run until convergence, but convergence might be hard to obtain.\n",
|
||
"\n",
|
||
"In this task, we will explore the relationship between the cross entropy loss vs the number of update rounds and/or the runtime it takes to run the update for both batch gradient descent (task 3.4) and stochastic gradient descent (task 3.5) using sample of `credit_card` dataset.\n",
|
||
"\n",
|
||
"To guide us in our explorations, let's start by collecting empirical results from running stochastic gradient descent and batch gradient descent. You will need to make the following 2 plots with the stated specifications:\n",
|
||
"\n",
|
||
"1. Plot of cross entropy loss against number of update rounds\n",
|
||
" - $x$-axis label: \"Number of Update Rounds\"\n",
|
||
" - $y$-axis label: \"Cross Entropy Loss\"\n",
|
||
" - Title: \"Plot of cross entropy loss against number of update rounds\"\n",
|
||
" - Legend to specify the data points corresponding to \"Batch Gradient Descent\" and \"Stochastic Gradient Descent\" respectively\n",
|
||
"2. Plot of cross entropy loss against runtime (sec)\n",
|
||
" - $x$-axis label: \"Runtime (sec)\"\n",
|
||
" - $y$-axis label: \"Cross Entropy Loss\"\n",
|
||
" - Title: \"Plot of cross entropy loss against runtime (sec)\"\n",
|
||
" - Legend to specify the data points corresponding to \"Batch Gradient Descent\" and \"Stochastic Gradient Descent\" respectively\n",
|
||
"\n",
|
||
"For both plots, put both methods (batch gradient descent and stochastic gradient descent) in one diagram. Also ensure that the runtime is the cumulative runtime not the runtime per individual update round. \n",
|
||
"\n",
|
||
"You can refer to following script for creating these plots. Feel free to modify it if necessary.\n",
|
||
"\n",
|
||
"Once you are done, paste your plots in the corresponding answer box in Coursemology."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 101,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"image/png": "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H12HqTPa+tkLIZCVlaXt8HRK02u3srLCpUuXEBMTo1gmTJgAT09PxMTEoH379pUVeoVp6+ceHR1dbaYVyJXn2jt16oS7d+/i8ePHinVXr16FgYEB6tevr9N4takiP/eTJ0/in3/+wdixY3UZYsXpZbp4LSG/NXLTpk0iNjZWTJ06VZibmytm8c+cOVOMGDFC0V5eAmDatGkiNjZWbNq0qdqXAFD32oUQIjo6WkRHRwtfX1/x+uuvi+joaHH58mV9hF8hml77jh07hJGRkVi9erXSrbHp6en6uoQK0fT6v/nmG7F//35x9epVcfXqVfHdd98JKysrMWfOHH1dQrmV5/e+sOp8d5um1/7ll1+Kffv2iatXr4q///5bzJw5UwAQe/bs0dcllJum1/7o0SNRv3598eqrr4rLly+LkydPiiZNmohx48bp6xLKrby/88OHDxft27ev7HA1xiRJx1avXi3c3d2FiYmJ8PHxESdPnlRsGzlypAgMDFRqf+LECdG2bVthYmIiGjRoINauXVvJEWuPptcOoNji7u5euUFriSbXHhgYqPLaR44cWfmBa4km179q1SrRokULYWZmJqysrETbtm3FmjVrRF5enh4irzhNf+8Lq85JkhCaXfvnn38uGjVqJGQymahTp4548cUXxcGDB/UQtXZo+nOPi4sT3bt3F6ampqJ+/fpi+vTp4unTp5UctXZoeu3p6enC1NRUbNiwoZIj1ZxEiP+fGUxERERECpyTRERERKQCkyQiIiIiFZgkEREREanAJImIiIhIBSZJRERERCowSSIiIiJSgUkSERERkQpMkoiIiIhUYJJERDXWqFGj9PqE8REjRmDx4sWVcq4ZM2Zg8uTJlXIuotqCFbeJSGdGjRqFrVu3AgAMDQ3h4uKCPn36YPHixahTp47WznPr1i14eHggOjoabdq0UazPyMiAEAI2NjZaO5e6Ll68iC5duuD27duwtLTU+flSUlLQqFEjXLx4ER4eHjo/H1FtwJ4kItKpnj17IikpCbdu3cK3336LX3/9FRMnTqyUc1tbW+slQQKAb775BoMHD66UBAkAHBwcEBQUhHXr1lXK+YhqAyZJRKRTUqkUTk5OqF+/PoKCgjBkyBCEhoYqtnfp0gVTp05V2mfgwIEYNWqU4nWDBg2wePFijBkzBpaWlnBzc8OGDRsU2+U9J23btoVEIkGXLl0AFB9u69KlC9577z1MnToVderUgaOjIzZs2IAnT55g9OjRsLS0RKNGjfDbb78pxRMbG4vevXvDwsICjo6OGDFiBFJTU0u85vz8fPz000/o37+/0vo1a9agSZMmkMlkcHR0xKuvvqrYJoTA0qVL0bBhQ5iamqJ169b4+eeflfa/fPky+vTpAysrK1haWiIgIADXr19XbO/fvz927txZYlxEpBkmSURUaW7cuIHDhw/D2NhY432XL18OPz8/REdHY+LEiXjnnXdw5coVAMCff/4JADh69CiSkpKwd+/eEo+zdetW2NnZ4c8//8R7772Hd955B4MHD0bHjh1x4cIFBAcHY8SIEXj69CkAICkpCYGBgWjTpg3Onz+Pw4cP4969e3jttddKPMfFixeRnp4OPz8/xbrz589j8uTJWLhwIeLj43H48GF07txZsX3u3LnYvHkz1q5di8uXL2PatGkYPnw4Tp48CQD4999/0blzZ8hkMhw7dgxRUVEYM2YMcnNzFcdo164dEhMTcfv2bY3fXyJSQRAR6cjIkSOFoaGhMDc3FzKZTAAQAMSKFSsUbQIDA8WUKVOU9hswYIAYOXKk4rW7u7sYPny44nV+fr5wcHAQa9euFUIIcfPmTQFAREdHFzv/gAEDlM714osvKl7n5uYKc3NzMWLECMW6pKQkAUBEREQIIYSYN2+eCAoKUjpuYmKiACDi4+NVXve+ffuEoaGhyM/PV6zbs2ePsLKyEpmZmcXaP378WMhkMnHmzBml9WPHjhXDhg0TQggxa9Ys4eHhIbKzs1WeUwghMjIyBABx4sSJEtsQkfqM9JmgEVHN17VrV6xduxZPnz7Ft99+i6tXr+K9997T+Dje3t6K/5dIJHByckJKSkqFjmNoaAhbW1u0atVKsc7R0REAFMeOiorC8ePHYWFhUexY169fR9OmTYutf/bsGaRSKSQSiWJdjx494O7ujoYNG6Jnz57o2bMnXn75ZZiZmSE2NhbPnz9Hjx49lI6TnZ2Ntm3bAgBiYmIQEBBQai+cqakpACh6wYioYpgkEZFOmZubo3HjxgCAVatWoWvXrliwYAE++eQTAICBgQFEkZtsc3Jyih2naHIgkUiQn5+vcTyqjlN4nTyxkR87Pz8f/fr1w+eff17sWM7OzirPYWdnh6dPnyI7OxsmJiYAAEtLS1y4cAEnTpxAaGgoPv74Y4SEhCAyMlJxroMHD6JevXpKx5JKpQD+S4BK8+DBAwCAvb19mW2JqGyck0RElWr+/PlYtmwZ7t69C6DgCz0pKUmxPS8vD3///bdGx5QnInl5edoL9P/5+Pjg8uXLaNCgARo3bqy0mJubq9xHXoYgNjZWab2RkRG6d++OpUuX4uLFi7h16xaOHTuG5s2bQyqVIiEhodg5XF1dART0gJ06dUplAin3999/w9jYGC1atNDOxRPVckySiKhSdenSBS1atFAUWXzppZdw8OBBHDx4EFeuXMHEiRORnp6u0TEdHBxgamqqmFSdkZGhtXgnTZqEBw8eYNiwYfjzzz9x48YNhIaGYsyYMSUmZfb29vDx8cEff/yhWHfgwAGsWrUKMTExuH37NrZt24b8/Hx4enrC0tISM2bMwLRp07B161Zcv34d0dHRWL16taLO1LvvvovMzEwMHToU58+fx7Vr17B9+3bEx8crznHq1CkEBASo1etERGVjkkRElW769OnYuHEjEhMTMWbMGIwcORJvvvkmAgMD4eHhga5du2p0PCMjI6xatQrr16+Hi4sLBgwYoLVYXVxccPr0aeTl5SE4OBgtW7bElClTYG1tDQODkv+EvvXWW/jhhx8Ur21sbLB371689NJLaNasGdatW4edO3cqen0++eQTfPzxx1iyZAmaNWuG4OBg/Prrr4ryBra2tjh27BgeP36MwMBA+Pr6YuPGjUpDhTt37sT48eO1du1EtR0rbhMR6cDz58/h6emJXbt2wd/fX+fnO3jwID744ANcvHgRRkacbkqkDexJIiLSAZlMhm3btpVadFKbnjx5gs2bNzNBItIi9iQRERERqcCeJCIiIiIVmCQRERERqcAkiYiIiEgFJklEREREKjBJIiIiIlKBSRIRERGRCkySiIiIiFRgkkRERESkApMkIiIiIhX+D3Twlel9h4h1AAAAAElFTkSuQmCC",
|
||
"text/plain": [
|
||
"<Figure size 640x480 with 1 Axes>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"import matplotlib.pyplot as plt\n",
|
||
"from time import time\n",
|
||
"\n",
|
||
"X_sample, y_sample = X[:2000], y[:2000].flatten()\n",
|
||
"num_interations = 2000\n",
|
||
"batch_times = []\n",
|
||
"batch_costs = []\n",
|
||
"\n",
|
||
"for i in range(50, num_interations + 1, 50):\n",
|
||
" start = time()\n",
|
||
" weight_vector = logistic_regression_batch_gradient_descent(X_sample, y_sample, i, 0, 1e-5)\n",
|
||
" stop = time()\n",
|
||
" batch_times.append(stop - start)\n",
|
||
" batch_costs.append(cost_function(X_sample, y_sample, weight_vector))\n",
|
||
"plt.plot(batch_times, batch_costs, 'g^', label=\"Batch Gradient Descent\")\n",
|
||
"\n",
|
||
"stochastic_times = []\n",
|
||
"stochastic_costs = []\n",
|
||
"for i in range(50, num_interations + 1, 50):\n",
|
||
" start = time()\n",
|
||
" weight_vector = logistic_regression_stochastic_gradient_descent(X_sample, y_sample, i, 0, 1e-5)\n",
|
||
" stop = time()\n",
|
||
" stochastic_times.append(stop - start)\n",
|
||
" stochastic_costs.append(cost_function(X_sample, y_sample, weight_vector))\n",
|
||
"plt.plot(stochastic_times, stochastic_costs, 'bs', label=\"Stochastic Gradient Descent\")\n",
|
||
"\n",
|
||
"plt.xlabel('Runtime (sec)')\n",
|
||
"plt.ylabel('Cross Entropy Loss')\n",
|
||
"plt.legend()\n",
|
||
"plt.title('Plot of cross entropy loss against runtime (sec)')\n",
|
||
"\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 102,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"image/png": "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2lpcXFzktddekxs3bmi2BwcHy9tvvy0TJkyQGjVqiKurq0ydOlXrmhSss7e3t946G1Nxv29D7t9sSapE1K1IuZILAMiVXLYmEVUBxm4BvnPnDr799lvUr19fa84nW1tbrF69GmfOnMEXX3yBFStW4PPPPwcADBgwAO+88w6aNGmiaZEaMGAA8vLy0L17dxw+fBjffPMNzpw5g08++URrrpl79+5hzpw5WLduHX777TckJibi3XffLbJ869evh42NDUaOHKl3e+GWnfPnz+P777/Hpk2bNN1Yd+/eRVhYGKKjo7F3716YmJjg+eefR15enmZ7z5490ahRI8TExCA8PLzYMgHAqVOnEBoaihdeeAEnT57Exo0bcfDgQYwePVor39y5cxEYGIjY2FiMHDkSb731Fs6ePQsA+OOPPwD820K0efPmYs9ZmImJCcaOHYsrV64gJiYGALBr1y689tprGDNmDM6cOYPly5dj9erV+PjjjwEAP/74Iz7//HMsX74c586dw9atW9GsWTPNMV9//XVs2LABCxYsQHx8PJYtWwYbGxsAQHJyMoKDg9GyZUscO3YMERERuH79Ol566SWtcq1ZswbW1tY4evQoZs+ejenTpyMyMhIAEB2d/11ftWoVkpOTNe+rpYqI4J4E5d2SVLgVSf1iaxJRxSivliRjtAAPHjxYTE1NxdraWqytrQWAuLu7S0xMTLH7zZ49WwICAjTv1a02Be3atUtMTEwkISFB7zHULVAFW3EWL14srq6uRZ63W7du0rx5c620uXPnaspvbW0tt27d0pTJ3Nxcq0VMn9TUVAEgp06dEhGR5cuXi6Ojo9y9e1eTZ+nSpcW2JA0aNEj+85//aB33999/FxMTE833wtvbW1577TXN9ry8PHFxcZGlS5eKSNEtRIUVly8+Pl4AyMaNG0VEpEOHDjJz5kytPOvWrRN3d3cRyb92DRs21LQKFpSQkCAAJDIyUm85pkyZIiEhIVppSUlJAkDzmQcHB8vTTz+tlad169by/vvva94DkC1bthRbZ2NiS1I1U7gVSY2tSUSVm7FagJ955hnExcUhLi4OR48eRUhICLp3744rV65o8vz44494+umn4ebmBhsbG0yZMgWJiYnFHjcuLg61a9dGw4YNi8xjZWWFevXqad67u7trloEoSuHWoqFDhyIuLg7Lly/H3bt3tR5S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",
|
||
"text/plain": [
|
||
"<Figure size 640x480 with 1 Axes>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"import matplotlib.pyplot as plt\n",
|
||
"from time import time\n",
|
||
"\n",
|
||
"X_sample, y_sample = X[:2000], y[:2000].flatten()\n",
|
||
"num_interations = 2000\n",
|
||
"batch_times = []\n",
|
||
"batch_costs = []\n",
|
||
"\n",
|
||
"for i in range(50, num_interations + 1, 50):\n",
|
||
" start = time()\n",
|
||
" weight_vector = logistic_regression_batch_gradient_descent(X_sample, y_sample, i, 0, 1e-5)\n",
|
||
" stop = time()\n",
|
||
" batch_times.append(i)\n",
|
||
" batch_costs.append(cost_function(X_sample, y_sample, weight_vector))\n",
|
||
"plt.plot(batch_times, batch_costs, 'g^', label=\"Batch Gradient Descent\")\n",
|
||
"\n",
|
||
"stochastic_times = []\n",
|
||
"stochastic_costs = []\n",
|
||
"for i in range(50, num_interations + 1, 50):\n",
|
||
" start = time()\n",
|
||
" weight_vector = logistic_regression_stochastic_gradient_descent(X_sample, y_sample, i, 0, 1e-5)\n",
|
||
" stop = time()\n",
|
||
" stochastic_times.append(i)\n",
|
||
" stochastic_costs.append(cost_function(X_sample, y_sample, weight_vector))\n",
|
||
"plt.plot(stochastic_times, stochastic_costs, 'bs', label=\"Stochastic Gradient Descent\")\n",
|
||
"\n",
|
||
"plt.xlabel('Number of Update Rounds')\n",
|
||
"plt.ylabel('Cross Entropy Loss')\n",
|
||
"plt.legend()\n",
|
||
"plt.title('Plot of cross entropy loss against number of update rounds')\n",
|
||
"\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 3.6.2: Stochastic gradient descent vs batch gradient descent\n",
|
||
"\n",
|
||
"With reference to the 2 plots that you have made in task 3.6.1, what do you observe about the relationship between the cross entropy loss vs the number of update rounds and/or the runtime it takes to run the update for both batch gradient descent (task 3.4) and stochastic gradient descent (task 3.5)? Explain your observations in terms of the effect of size of data, number of update rounds, runtime and whether the algorithm will be stuck in a local minima and so on."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Stochastic Gradient Descent runs significantly faster than batch gradient descent. Especially as the batch size increases, the speed of Stochastic over batch Gradient Descent can be seen. However, looking at the number of update rounds, batch gradient descent attains a lower CEL with lesser updates as compared to Stochastic Gradient descent. However, over time and over 2000 updates, both stochastic and batch gradient descent converges. The graph of stochastic gradient descent doesn't look as smooth and tends to 'overcorrect', when the randomly seleced variable. Since batch gradient descent considers the entire set of data, and stochastic gradient descent randomly samplies from the given dataset, this will not be stuck in a local minima. "
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Multi-class logistic regression\n",
|
||
"\n",
|
||
"Now that you have helped the credit card companies recognize fraudulent transactions, customers are less likely to be charged for items that they did not purchase. They are very satisfied with how well your model performed. They want to offer you a meal, at a restaurant of your choice. You decide to make your choice based on the occupancy of the restaurant. Luckily for you, you have some starting data `restaurant_data.csv` to work with. The dataset provides three input features \"max_capacity\", \"feedback_score\", and \"average_expense\", and an output feature \"occupancy\" that takes the value of \"none\", \"some\", or \"full\"."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 4.1: Multi-class logistic regression using batch gradient descent\n",
|
||
"\n",
|
||
"In this task, you need to implement a fixed learning rate algorithm for multi-class logistic regression. This function takes `X_train`, `y_train`, `max_num_epochs`, `threshold`, `alpha`, and `class_i` as arguments, and output the final `weight_vector` you obtained. Here, argument `class_i` is the class that you want to train in this round. You can make use of the previous functions you have implemented earlier."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 107,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def multi_class_logistic_regression_batch_gradient_descent(X_train: np.ndarray, y_train: np.ndarray, max_num_epochs: int, threshold: np.float64, alpha: np.float64, class_i: str) -> np.ndarray:\n",
|
||
" '''\n",
|
||
" Initialize your weight to zeros. Write your terminating condition, and run the weight update for some iterations.\n",
|
||
" Get the resulting weight vector. Output the resulting weight vector.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X_train: np.ndarray\n",
|
||
" (m, n) training dataset (features).\n",
|
||
" y_train: np.ndarray\n",
|
||
" (m,) training dataset (corresponding targets).\n",
|
||
" alpha: np.float64\n",
|
||
" logistic regression learning rate.\n",
|
||
" max_num_epochs: int\n",
|
||
" this should be one of the terminating conditions. \n",
|
||
" That means if you initialize num_update_rounds to 0, \n",
|
||
" then you should stop updating the weights when num_update_rounds >= max_num_epochs.\n",
|
||
" threshold: float\n",
|
||
" terminating when error <= threshold value, or if you reach the max number of update rounds first.\n",
|
||
" class_i: string\n",
|
||
" one of 'none', 'full', 'some'.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" The final (n,) weight parameters\n",
|
||
" '''\n",
|
||
" \n",
|
||
" X_sel = X_train\n",
|
||
" y_sel = (y_train == class_i).astype(int)\n",
|
||
"\n",
|
||
" weights = np.zeros(X_sel.shape[1])\n",
|
||
" for _ in range(max_num_epochs):\n",
|
||
" weights = weight_update(X_sel, y_sel, alpha, weights)\n",
|
||
" if cost_function(X_sel, y_sel, weights) <= threshold:\n",
|
||
" break\n",
|
||
" return weights\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 108,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[26, 9, 69, 'full'],\n",
|
||
" [54, 3, 16, 'some'],\n",
|
||
" [59, 7, 50, 'some' ],\n",
|
||
" [33, 0, 45, 'full']]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['max_capcity', 'feedback_score', 'average_expense', 'occupancy'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"max_num_epochs1 = 20\n",
|
||
"expected1 = np.transpose([6.75, 0.125, -6.0])\n",
|
||
"assert np.array_equal(multi_class_logistic_regression_batch_gradient_descent(X1, y1, max_num_epochs1, 0.05, 1, 'some'), expected1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 4.2: Do classification on multi-class problem\n",
|
||
"\n",
|
||
"Finally, you classify each element in `X`, given three weight vectors `weight_vector_none`, `weight_vector_some`, and `weight_vector_full`, and output a list of classification results in the form of an array (`['some', 'none', 'full', 'none', ...]`). Note that in the case of a tie, you can choose the class in the order of priority: `'none'`, `'some'`, `'full'`. You can make use of the previous functions you have implemented earlier."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 155,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def multi_class_logistic_regression_classification(X: np.ndarray, weight_vector_none: np.ndarray, weight_vector_some: np.ndarray, weight_vector_full: np.ndarray):\n",
|
||
" '''\n",
|
||
" Do classification task using logistic regression.\n",
|
||
" In the case of a tie, break the tie in the priority 'none' > 'some' > 'full'.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) training dataset (features)\n",
|
||
" weight_vector_none: np.ndarray\n",
|
||
" (n,) weight parameters for the 'none' class.\n",
|
||
" weight_vector_some: np.ndarray\n",
|
||
" (n,) weight parameters for the 'some' class.\n",
|
||
" weight_vector_full: np.ndarray\n",
|
||
" (n,) weight parameters for the 'full' class.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" Classification result as an (m,) np.ndarray. Eg ['some', 'none', 'full', ... ,'none'].\n",
|
||
" '''\n",
|
||
" m = X.shape[0]\n",
|
||
" y_none = (1/(1+np.exp(-X @ weight_vector_none))).reshape(m, 1)\n",
|
||
" y_some = (1/(1+np.exp(-X @ weight_vector_some))).reshape(m, 1)\n",
|
||
" y_full = (1/(1+np.exp(-X @ weight_vector_full))).reshape(m, 1)\n",
|
||
"\n",
|
||
" combined = np.hstack((y_none, y_some, y_full))\n",
|
||
" result = np.argmax(combined, axis=1).astype('object')\n",
|
||
" result[result == 0] = 'none'\n",
|
||
" result[result == 1] = 'some'\n",
|
||
" result[result == 2] = 'full'\n",
|
||
" return result\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 156,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"data1 = [[26, 9, 69, 'full'],\n",
|
||
" [54, 3, 16, 'some'],\n",
|
||
" [59, 7, 50, 'some' ],\n",
|
||
" [33, 0, 45, 'full']]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['max_capcity', 'feedback_score', 'average_expense', 'occupancy'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"\n",
|
||
"w11 = np.transpose([0.0013351567670329624, 2.5757816929896605e-05, -0.001189020140476165])\n",
|
||
"w12 = np.transpose([2.5757816929896605e-05, -0.001189020140476165, 0.0013351567670329624])\n",
|
||
"w13 = np.transpose([2.5757816929896605e-05, 0.0013351567670329624, -0.001189020140476165])\n",
|
||
"expected1 = np.transpose(['some', 'none', 'some', 'some'])\n",
|
||
"\n",
|
||
"result1 = multi_class_logistic_regression_classification(X1, w11, w12, w13)\n",
|
||
"\n",
|
||
"assert result1.shape == expected1.shape and (result1 == expected1).all()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Support Vector Machine\n",
|
||
"\n",
|
||
"Now let's apply the support vector machine we learnt in lecture to the credit card dataset. Here is a quick recap of what SVM is about: We are given a training dataset of $n$ points of the form $(x^{(i)}, y^{(i)})$, where the $y^{(i)}$ are either 1 or 0, each indicating the class to which the point $x^{(i)}$ belongs.\n",
|
||
"\n",
|
||
"Each $x^{(i)}$ $n$-dimensional real vector. We want to find the \"maximum-margin hyperplane\" that divides the group of points $x^{(i)}$ for which $y_{i}=1$ from the group of points for which $y_{i}=0$, which is defined so that the distance between the hyperplane and the nearest point $x^{(i)}$ from either group is maximized.\n",
|
||
"\n",
|
||
"<figure align=\"center\">\n",
|
||
"<img src=\"imgs/svm.png\" alt=\"visualisation of support vector machine\" width=\"50%\">\n",
|
||
"<figcaption style=\"text-align:center\">Figure 7: Visualisation of support vector machine.</figcaption>\n",
|
||
"</figure>\n",
|
||
"\n",
|
||
"How can we do construct such a \"maximum-margin hyperplane\"? Recall that in the lecture, we formulated SVMs as the following minimization problem:\n",
|
||
"\n",
|
||
"$$\n",
|
||
"\\min_{w} C \\left[\\sum_{i = 1}^{m} y^{(i)}\\ Cost_1(\\mathbf{w}^{T} \\mathbf{x}^{(i)}) + (1 - y^{(i)})\\ Cost_0(\\mathbf{w}^{T} \\mathbf{x}^{(i)})\\right] + \\frac{1}{2} \\sum_{i = 1}^{n} \\mathbf{w}_i^2 $$\n",
|
||
"\n",
|
||
"where $Cost_1(\\mathbf{w}^T \\mathbf{x}^{(i)}) = \\max(0, 1 - \\mathbf{w}^T \\mathbf{x}^{(i)})$ and $Cost_0(\\mathbf{w}^T \\mathbf{x}^{(i)}) = \\max(0, 1 + \\mathbf{w}^T \\mathbf{x}^{(i)})$, with the hypothesis function $h_\\mathbf{w}(\\mathbf{w}) = 1$ if $\\mathbf{w}^T \\mathbf{x} \\geq 0$, and 0 otherwise.\n",
|
||
"\n",
|
||
"In particular, the summand term is known as the *hinge loss*. Thus, when $y^{(i)} = 0$, the more negative $\\mathbf{w}^T \\mathbf{x}$ is, the more $Cost_1$ penalises the model. Similarly, $Cost_0$ penalises the model the further $\\mathbf{w}^T \\mathbf{x}$ strays from the negative region. Notice how the cost function aligns with the goal of the hypothesis function?\n",
|
||
"\n",
|
||
"Furthermore, we also add a regularization parameter $C$ to the cost function to balance the trade-off between margin maximization and loss.\n",
|
||
"\n",
|
||
"Thus, solving this minimization problem allows us to find the weight vector $\\mathbf{w}$ that provides the maximum-margin hyperplane while balancing the trade-off of overfitting.\n",
|
||
"\n",
|
||
"With this set up in mind, how can we implement this in code? Thankfully, with modern machine learning libraries, much of the tedious work of solving this minimization problem has been done. As machine learning practitioners, one popular library that we commonly use (aside from [NumPy](https://numpy.org/doc/stable/index.html)) is [scikit-learn](https://scikit-learn.org/), which is built on top of NumPy and provides simple and efficient tools to implement common machine learning algorithms.\n",
|
||
"\n",
|
||
"In particular, we will look at how we can use the [scikit-learn](https://scikit-learn.org/) library to implement SVM."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 5.1: Linear SVM\n",
|
||
"\n",
|
||
"Take a look at the Python library [scikit-learn](https://scikit-learn.org/stable/modules/svm.html). It has been imported for your use. In this task, the function takes `X`, `y`. Note that here `X` and `y` are not training set but the entire dataset, so you are asked to do a train test data split with `test_size` of 0.3, and `random_state` of 42, **using what's provided in the library** (not what you implemented in task 2.1). Then, create an instance of a Linear SVM classifier using the default parameters, train the Linear SVM classifier, and output the predictions as well as the accuracy score. Note that the default kernel in sklearn is RBF, so you need to specify the kernel type you want to use.\n",
|
||
"\n",
|
||
"**For this task, you should make full use of the library functions, instead of doing computations on your own.**\n",
|
||
"\n",
|
||
"In particular, you can look into the scikit-learn methods to perform a [train-test data split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) and employ the [SVC model](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def linear_svm(X: np.ndarray, y: np.ndarray):\n",
|
||
" '''\n",
|
||
" Do classification using linear svm. Given X and y, note that here X and y are not training sets, but rather the\n",
|
||
" entire dataset. Do a train test data split with test_size=0.3, and random_state=42.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) whole dataset (features)\n",
|
||
" y: np.ndarray\n",
|
||
" (m,) whole dataset (corresponding targets)\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" pred: np.ndarray\n",
|
||
" The predictions.\n",
|
||
" acc: np.float64\n",
|
||
" The accuracy on a scale up to 100.\n",
|
||
" '''\n",
|
||
" from sklearn.metrics import accuracy_score\n",
|
||
" X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=0.3, random_state=42)\n",
|
||
" \n",
|
||
" clf = svm.SVC(kernel='linear', random_state=42)\n",
|
||
" clf.fit(X_train, y_train)\n",
|
||
" predictions = clf.predict(X_test)\n",
|
||
" accuracy = accuracy_score(y_test, predictions) * 100\n",
|
||
"\n",
|
||
" return predictions, accuracy"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"(array([0, 0, 1]), 66.66666666666666)\n",
|
||
"(array([1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), 80.0)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# small data\n",
|
||
"data1 = [[111.1, 10, 0], [111.2, 20, 0], [111.3, 10, 0], [111.4, 10, 0], [111.5, 10, 0], [211.6, 80, 1],\n",
|
||
" [111.4, 10, 0], [111.5, 80, 1], [211.6, 80, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"expected1_y = np.transpose([0, 0, 1])\n",
|
||
"expected1_accuracy = 66.66666666666666\n",
|
||
"result1 = linear_svm(X1, y1)\n",
|
||
"print(result1)\n",
|
||
"assert (result1[0] == expected1_y).all() and (result1[0]).shape == expected1_y.shape and round(result1[1], 5) == round(expected1_accuracy, 5)\n",
|
||
"\n",
|
||
"# subset of credit card data\n",
|
||
"class_0 = credit_df[credit_df['Class'] == 0]\n",
|
||
"class_1 = credit_df[credit_df['Class'] == 1]\n",
|
||
"\n",
|
||
"data_0 = class_0.sample(n=15, random_state=42)\n",
|
||
"data_1 = class_1.sample(n=50, random_state=42)\n",
|
||
"data_100 = pd.concat([data_1, data_0], axis=0)\n",
|
||
"X = data_100.iloc[:, :-1].to_numpy()\n",
|
||
"y = data_100.iloc[:, -1].to_numpy()\n",
|
||
"\n",
|
||
"expected_pred = np.transpose([1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])\n",
|
||
"expected_accuracy = 80.0\n",
|
||
"result = linear_svm(X, y.ravel())\n",
|
||
"print(result)\n",
|
||
"assert (result[0] == expected_pred).all() and (result[0]).shape == expected_pred.shape and round(result[1], 5) == round(expected_accuracy, 5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 5.2: Gaussian Kernel SVM\n",
|
||
"\n",
|
||
"Similarly, you will use scikit-learn to implement a Gaussian Kernel SVM. In this task, the function takes `X`, `y`. Note that here `X` and `y` are not training set but the entire dataset, so you are asked to do a train test data split with `test_size` of 0.3, and `random_state` of 42, using what's provided in the library. Then, create an instance of a Gaussian Kernel SVM classifier using the default parameters, train the Gaussian Kernel SVM classifier, and output the predictions as well as the accuracy score.\n",
|
||
"\n",
|
||
"**For this task, you should make full use of the library functions, instead of doing computations on your own.**\n",
|
||
"\n",
|
||
"As before, you can look into the scikit-learn methods to perform a [train-test data split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) and employ the [SVC model](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def gaussian_kernel_svm(X: np.ndarray, y: np.ndarray):\n",
|
||
" '''\n",
|
||
" Do classification using Gaussian Kernel svm. Given X and y, note that here X and y are not training sets, but\n",
|
||
" rather the entire dataset. Do a train test data split with test_size=0.3, and random_state=42.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X: np.ndarray\n",
|
||
" (m, n) whole dataset (features)\n",
|
||
" y: np.ndarray\n",
|
||
" (m,) whole dataset (corresponding targets)\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" pred: np.ndarray\n",
|
||
" The predictions.\n",
|
||
" acc: np.float64\n",
|
||
" The accuracy on a scale up to 100.\n",
|
||
" '''\n",
|
||
" from sklearn.metrics import accuracy_score\n",
|
||
" X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=0.3, random_state=42)\n",
|
||
" \n",
|
||
" clf = svm.SVC(kernel='rbf', random_state=42)\n",
|
||
" clf.fit(X_train, y_train)\n",
|
||
" predictions = clf.predict(X_test)\n",
|
||
" accuracy = accuracy_score(y_test, predictions) * 100\n",
|
||
"\n",
|
||
" return predictions, accuracy"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"(array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), 80.0)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# small data\n",
|
||
"data1 = [[111.1, 10, -1], [111.2, 20, -1], [111.3, 10, -1], [111.4, 10, -1], [111.5, 10, -1], [211.6, 80, 1],\n",
|
||
" [111.4, 10, -1], [111.5, 80, 1], [211.6, 80, 1]]\n",
|
||
"df1 = pd.DataFrame(data1, columns = ['V1', 'V2', 'Class'])\n",
|
||
"X1 = df1.iloc[:, :-1].to_numpy()\n",
|
||
"y1 = df1.iloc[:, -1].to_numpy()\n",
|
||
"expected1_y = np.transpose([-1, -1, 1])\n",
|
||
"expected1_accuracy = 66.66666666666666\n",
|
||
"result1 = gaussian_kernel_svm(X1, y1)\n",
|
||
"assert (result1[0] == expected1_y).all() and (result1[0]).shape == expected1_y.shape and round(result1[1], 5) == round(expected1_accuracy, 5)\n",
|
||
"\n",
|
||
"\n",
|
||
"# subset of credit card data\n",
|
||
"class_0 = credit_df[credit_df['Class'] == 0]\n",
|
||
"class_1 = credit_df[credit_df['Class'] == 1]\n",
|
||
"\n",
|
||
"data_0 = class_0.sample(n=15, random_state=42)\n",
|
||
"data_1 = class_1.sample(n=50, random_state=42)\n",
|
||
"data_100 = pd.concat([data_1, data_0], axis=0)\n",
|
||
"X = data_100.iloc[:, :-1].to_numpy()\n",
|
||
"y = data_100.iloc[:, -1].to_numpy()\n",
|
||
"\n",
|
||
"expected_pred = np.transpose([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])\n",
|
||
"expected_accuracy = 80.0\n",
|
||
"result = gaussian_kernel_svm(X, y.ravel())\n",
|
||
"print(result)\n",
|
||
"assert (result[0] == expected_pred).all() and (result[0]).shape == expected_pred.shape and round(result[1], 5) == round(expected_accuracy, 5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Task 5.3: Linear SVM vs Gaussian Kernel SVM\n",
|
||
"\n",
|
||
"Based on your observations, when using support vector machines, how do you think we should choose between linear kernel vs Gaussian kernel? In other words, which kernel is better in what cases?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Submission\n",
|
||
"\n",
|
||
"Once you are done, please submit your work to Coursemology, by copying the right snippets of code into the corresponding box that says \"Your answer,\"and click \"Save.\" After you save, you can still make changes to your submission.\n",
|
||
"\n",
|
||
"Once you are satisfied with what you have uploaded, click \"Finalize submission.\" Note that once your submission is finalized, it is considered to be submitted for grading and cannot be changed. If you need to undo this action, you will have to email your assigned tutor for help. Please do not finalize your submission until you are sure that you want to submit your solutions for grading.\n",
|
||
"\n",
|
||
"*Have fun and enjoy coding.*"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3 (ipykernel)",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.9.9"
|
||
},
|
||
"vscode": {
|
||
"interpreter": {
|
||
"hash": "a665b5d41d17b532ea9890333293a1b812fa0b73c9c25c950b3cedf1bebd0438"
|
||
}
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
}
|