---
title: "Supervised Learning"
subtitle: "Nonparametric regression"
author: "Ron Yurko"
date: "07/06/2020"
---

```{r setup, echo = FALSE}
knitr::opts_chunk$set(fig.retina = 3, warning = FALSE, message = FALSE)
```


## Model flexibility vs interpretability

[Figure 2.7, Introduction to Statistical Learning with Applications in R (ISLR)](http://faculty.marshall.usc.edu/gareth-james/ISL/)

```{r out.width='50%', echo = FALSE, fig.align='center'}
knitr::include_graphics("http://www.stat.cmu.edu/~pfreeman/flexibility.png")
```

__Tradeoff__ between a model's _flexibility_ (i.e. how "curvy" it is) and how __interpretable__ it is

- Simpler the parametric form of the model $\Rightarrow$ the easier it is to interpret

- Hence why __linear regression__ is popular in practice

---

## Model flexibility vs interpretability

```{r out.width='50%', echo = FALSE, fig.align='center'}
knitr::include_graphics("http://www.stat.cmu.edu/~pfreeman/flexibility.png")
```

- __Parametric__ models, for which we can write down a mathematical expression for $f(X)$ __before observing the data__, _a priori_ (e.g. linear regression), __are inherently less flexible__


--
- __Nonparametric__ models, in which $f(X)$ is __estimated from the data__ (e.g. kernel regression)

---

## K Nearest Neighbors (KNN)

__In words:__ KNN examines the $k$ data points closest to a location $x$ and uses just those data to generate predictions. The optimal value of $k$ is that which minimizes validation-set MSE (regression) or, e.g., MCR (classification).

KNN straddles the boundary between fully parameterized models like linear regression and fully data-driven models like random forests. A KNN model is data-driven, but one *can* actually write down a compact parametric form for the model *a priori*:

--

- For regression:
$$
{\hat Y} \vert X = \frac{1}{k} \sum_{i=1}^k Y_i \,,
$$
- For classification:
$$
P[Y = j \vert X] = \frac{1}{k} \sum_{i=1}^k I(Y_i = j) \,,
$$
where $I(\cdot)$ is the indicator function: it returns 0 if the argument is false, and 1 otherwise. The summation yields the proportion of neighbors that are of class $j$.

---

## Finding the optimal number of neighbors $k$

__The number of neighbors $k$ is a tuning parameter__ (like $\lambda$ is for ridge / lasso)

--

As is the case elsewhere in statistical learning, determining the optimal value of $k$ requires balancing bias and variance:

- If $k$ is too small, the resulting model is *too flexible*,

  - low bias (it is right on average...if we apply KNN to an infinite number of datasets sampled from the same parent population) 
  
  - high variance (the predictions have a large spread in values when we apply KNN to our infinite data). See the panels to the left on the next slide.

--

- If $k$ is too large, the resulting model is *not flexible enough*, 

  - high bias (wrong on average) and 
  
  - low variance (nearly same predictions, every time). See the panels to the right on the next slide.

---

## Finding the optimal number of neighbors $k$

```{r out.width='40%', echo = FALSE, fig.align='center'}
knitr::include_graphics("http://www.stat.cmu.edu/~pfreeman/Fig_3.16.png")
```

```{r out.width='40%', echo = FALSE, fig.align='center'}
knitr::include_graphics("http://www.stat.cmu.edu/~pfreeman/Fig_2.16.png")
```


(Figures 3.16 [top] and 2.16 [bottom], *Introduction to Statistical Learning* by James et al.)

---

## KNN in context

Here are two quotes from ISLR to keep in mind when thinking about KNN:

- "As a general rule, parametric methods [like linear regression] will tend to outperform non-parametric approaches [like KNN] when there is a small number of observations per predictor." This is the *curse of dimensionality*: for data-driven models, the amount of data you need to get similar model performance goes up exponentially with $p$.

--

$\Rightarrow$ KNN might not be a good model to learn when the number of predictor variables is very large.

--

- "Even in problems in which the dimension is small, we might prefer linear regression to KNN from an interpretability standpoint. If the test MSE of KNN is only slightly lower than that of linear regression, we might be willing to forego a little bit of prediction accuracy for the sake of a simple model..."

--

$\Rightarrow$ KNN is not the best model to learn if inference is the goal of an analysis.

---

## KNN: two critical points to remember

1. To determine which neighbors are the nearest neighbors, pairwise Euclidean distances are computed...so we may need to scale (or standardize) the individual predictor variables so that the distances are not skewed by that one predictor that has the largest variance.

--

2. Don't blindly compute a pairwise distance matrix! For instance, if $n$ = 100,000, then your pairwise distance matrix will have $10^{10}$ elements, each of which uses 8 bytes in memory...resulting in a memory usage of 80 GB! Your laptop cannot handle this. It can barely handle 1-2 GB at this point. If $n$ is large, you have three options:
    a. subsample your data, limiting $n$ to be $\lesssim$ 15,000-20,000;
    b. use a variant of KNN that works with sparse matrices (matrices that can be compressed since most values are zero); or
    c. make use of a "kd tree" to more effectively (but only approximately) identify nearest neighbors.
  
The [`FNN` package in `R`](https://daviddalpiaz.github.io/r4sl/knn-reg.html) has an option to search for neighbors via the use of a kd tree.


--
But instead we will use the [`caret`](http://topepo.github.io/caret/index.html) package...

---

## Example data: MLB 2019 batting statistics

Downloaded MLB 2019 batting statistics leaderboard from [Fangraphs](https://www.fangraphs.com/leaders.aspx?pos=all&stats=bat&lg=all&qual=y&type=8&season=2019&month=0&season1=2019&ind=0)

```{r load-data, warning = FALSE, message = FALSE}
library(tidyverse)
mlb_data <- read_csv("http://www.stat.cmu.edu/cmsac/sure/materials/data/fg_batting_2019.csv")
head(mlb_data)
```


---

## Data cleaning

- [`janitor`](http://sfirke.github.io/janitor/) package has convenient functions for data cleaning like `clean_names()`

- `parse_number()` function provides easy way to convert character to numeric columns

```{r clean-data}
library(janitor)
mlb_data_clean <- clean_names(mlb_data)
mlb_data_clean <- mlb_data_clean %>%
  mutate_at(vars(bb_percent:k_percent), parse_number)
head(mlb_data_clean)
```

---

## KNN example

`caret` is a package of functions designed to simplify training, tuning, and testing statistical learning methods

- first create partitions for training and test data using `createDataPartition()`

```{r split-data}
set.seed(1960)
train_i <- createDataPartition(y = mlb_data_clean$w_oba, p = 0.7, list = FALSE) %>%
  as.numeric()
train_mlb_data <- mlb_data_clean[train_i,]
test_mlb_data <- mlb_data_clean[-train_i,]
```

--

- next [`train()`](http://topepo.github.io/caret/model-training-and-tuning.html) to find the optimal `k` on the training data with cross-validation

```{r train-knn}
set.seed(1971)
init_knn_mlb_train <- train(w_oba ~ bb_percent + k_percent + iso, 
                            data = train_mlb_data, method = "knn",
                            trControl = trainControl("cv", number = 10),
                            preProcess = c("center", "scale"),
                            tuneLength = 10)
```

---

## KNN example

```{r plot-knn, fig.align ='center', fig.height=5}
plot(init_knn_mlb_train)
```


---

## KNN example

Can manually create a __tuning grid__ to search over for the tuning parameter `k`

```{r tune-k}
set.seed(1979)
tune_knn_mlb_train <- train(w_oba ~ bb_percent + k_percent + iso, 
                            data = train_mlb_data, method = "knn",
                            trControl = trainControl("cv", number = 10),
                            preProcess = c("center", "scale"),
                            tuneGrid = expand.grid(k = 2:20)) #<<
tune_knn_mlb_train$results
```

---

## KNN example

```{r plot-knn-tune, fig.align ='center', fig.height=5}
plot(tune_knn_mlb_train)
```

---

## KNN example

```{r test-preds}
tune_knn_mlb_train$bestTune
test_preds <- predict(tune_knn_mlb_train, test_mlb_data)
head(test_preds)
RMSE(test_preds, test_mlb_data$w_oba)
```

---

## What does KNN remind you of?...

```{r out.width='40%', echo = FALSE, fig.align='center'}
knitr::include_graphics("https://media1.giphy.com/media/12Gyz2J1b9SjD2/200w.gif")
```

---

## Kernels

A kernel $K(x)$ is a weighting function used in estimators. Full stop. A kernel technically has only one required property:

- $K(x) \geq 0$ for all $x$.

However, in the manner that kernels are used in statistics, there are two other properties that are usually satisfied:

- $\int_{-\infty}^\infty K(x) dx = 1$; and 

- $K(-x) = K(x)$ for all $x$.

In short: a kernel is a symmetric pdf!

---

## Kernel density estimation

__Goal__: estimate the PDF $f(x)$ for all possible values (assuming it is continuous / smooth)

--

$$
\text{Kernel density estimate: } \hat{f}(x) = \frac{1}{n} \sum_{i=1}^n \frac{1}{h} K_h(x - x_i)
$$

--
- $n =$ sample size, $x =$ new point to estimate $f(x)$ (does NOT have to be in dataset!)


--
- $h =$ __bandwidth__, analogous to histogram bin width, ensures $\hat{f}(x)$ integrates to 1

- $x_i =$ $i$th observation in dataset


--
- $K_h(x - x_i)$ is the __Kernel__ function, creates __weight__ given distance of $i$th observation from new point 
  - as $|x - x_i| \rightarrow \infty$ then $K_h(x - x_i) \rightarrow 0$, i.e. further apart $i$th row is from $x$, smaller the weight
  
  - as __bandwidth__ $h \uparrow$ weights are more evenly spread out (as $h \downarrow$ more concentrated around $x$) 

  - typically use [__Gaussian__ / Normal](https://en.wikipedia.org/wiki/Normal_distribution) kernel: $\propto e^{-(x - x_i)^2 / 2h^2}$
  
  - $K_h(x - x_i)$ is large when $x_i$ is close to $x$
  

---

## Commonly Used Kernels

```{r out.width='40%', echo = FALSE, fig.align='center'}
knitr::include_graphics("http://www.stat.cmu.edu/~pfreeman/kernels.png")
```


A general rule of thumb: the choice of kernel will have little effect on estimation, particularly if the sample size is large! The Gaussian kernel (i.e., a normal pdf) is by far the most common choice, and is the default for `R` functions that utilize kernels.


---

## Kernel regression

As a final note, realize that one can apply kernels in the regression setting as well as in the density estimation setting.

The classic kernel regression estimator is the __Nadaraya-Watson__ estimator:

$$\hat{y}_h(x) = \sum_{i=1}^n l_i(x) Y_i \,,$$

where
$$l_i(x) = \frac{K\left(\frac{x-X_i}{h}\right)}{\sum_{j=1}^n K\left(\frac{x-X_j}{h}\right)} \,.$$

Basically, the regression estimate is the average of all the *weighted* observed response values; the farther $x$ is from an observation, the less weight that observation has in determining the regression estimate at $x$.

The workhorse function for kernel regression in `R` is `ksmooth()` from the base `stats` package. Tuning to find the optimal value of $h$ is not necessarily simple; you can use $n^{-1/5}$ as an initial estimate.

