This is part two of a pair of live training sessions to review the concepts of statistical inference laid out in the DataCamp courses [Statistical Thinking in Python I](https://learn.datacamp.com/courses/statistical-thinking-in-python-part-1) and [Statistical Thinking in Python II](https://learn.datacamp.com/courses/statistical-thinking-in-python-part-2). The concepts of those courses were reviewed and fortified in [Case Studies in Statistical Thinking](https://learn.datacamp.com/courses/case-studies-in-statistical-thinking). You can link to those courses by clicking on the badges below.

In the Hacker Stats in Python Part 1 live training, we learned some basic ideas in nonparametric statistical inference using the plug-in principle. We applied those concepts to computing plug-in estimates and confidence intervals. Here, we will reinforce and extend those skills to:

- Construct confidence intervals and perform NHSTs comparing two treatments in a data set.
- Compute confidence intervals on two-dimensional data sets, investigating correlations and linear regressions.
- Review concepts behind null hypothesis significance tests (NHSTs).

While this live training can stand alone, I strongly encourage you to review the materials from Part 1.

## **Necessary packages**

I always import my packages at the top of a notebook or `.py` file. It is good style.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

# Seed RNG
np.random.seed(3252)

# I want crisp graphics, so we'll output SVG
%config InlineBackend.figure_format = 'svg'
```

## **The Dataset**

We will use the same dataset as in Part 1, but will explore different aspects of it. As a reminder, the data set we will be working with comes from a study by [Beattie, et al.](https://doi.org/10.1098/rsos.160321) in which they used the [Glasgow Facial Matching Test](https://en.wikipedia.org/wiki/Glasgow_Face_Matching_Test) (GFMT, [original paper](https://doi.org/10.3758/BRM.42.1.286)) to investigate how sleep deprivation affects a human subject's ability to match faces, as well as the confidence the subject has in those matches. Briefly, the test works by having subjects look at a pair of faces. Two such pairs are shown below.

For each of 40 pairs of faces, the subject gets as much time as he or she needs and then says whether or not they are the same person. The subject then rates his or her confidence in the choice.

In this study, subjects also took surveys to determine properties about their sleep. The surveys provide three different metrics of sleep quality and wakefulness.

- The Sleep Condition Indicator (SCI) is a measure of insomnia disorder over the past month. High scores indicate better sleep and scores of 16 and below indicate insomnia.
- The Pittsburgh Sleep Quality Index (PSQI) quantifies how well a subject sleeps in terms of interruptions, latency, etc. A higher score indicates poorer sleep.
- The Epworth Sleepiness Scale (ESS) assesses daytime drowsiness. Higher scores indicate greater drowsiness.

We will explore how the various sleep metrics are related to each other and how sleep disorders affect subjects' ability to discern faces and their confidence in doing so.

### Loading in the data set

We will load in the data set as in the previous live sesson on hacker stats.

```python
# Read in the dataset
df = pd.read_csv(
    "https://github.com/datacamp/Hacker-Stats-in-Python-Live-Training/blob/master/data/gfmt_sleep.csv?raw=True",
    na_values="*",
)

# Print header
df.head()
```

For this session, we are particularly interested in the following columns:

- `percent correct`: Percentage of correct responses among all trials
- `confidence when correct`: Average confidence when the subject gave a correct response for for all trials
- `confidence when incorrect`: Average confidence when the subject gave an incorrect response for for all trials
- `sci`: The subject's Sleep Condition Indicator.
- `psqi`: The subject's Pittsburgh Sleep Quality Index.
- `ess`: The subject's Epworth Sleepiness Scale.

Going forward, it will be useful to separate the subjects into two groups, insomniacs and normal sleepers. We will therefore add an `'insomnia'` column to the DataFrame with True/False entries. Recall that a person is deemed an insomniac if their SCI is 16 or below.

```python
# Add a column to the data frame for insomnia
df['insomnia'] = df['sci'] <= 16
```

With our data set in place, we can get moving with statistical inference.

## **Review of principles from last time**

In Part 1, we defined the following terms.

- **Cumulative distribution function (CDF)**: A probability distribution can be defined by its CDF, given by CDF(*x*) = probability of observing a value ≤ to *x*.
- **Generative distribution**: The unknown distribution from which the data are generated.
- **Empirical distribution**: The distribution defined entirely by observed data.
- **Empirical cumulative distribution function (ECDF)**: The cumulative distribution function (CDF) of the empirical distribution. It is defined as ECDF(*x*) = fraction of measured data points ≤ *x*.
- **Plug-in principle**: Approximate the generative distribution by the empirical distribution. That is, we make the approximation CDF(*x*) ≈ ECDF(*x*).
- **Confidence interval**: A *p*% confidence interval may be defined as follows. If an experiment is repeated over and over again, the estimate I compute will lie between the bounds of the *p*% confidence interval for *p*% of the experiments.
- **Bootstrap sample**: If I obtained *n* measurements, a bootstrap sample is constructed by drawing *n* values out of the empirical distribution. In practice, this amounts to randomly drawing data points out of the measured data set with replacement.
- **Bootstrap replicate**: A bootstrap replicate is the value of an estimate (e.g., a plug-in estimate) computed from a bootstrap sample.

We showed that we can compute many bootstrap replicates and then compute a confidence interval for an estimate by computing percentiles of the replicates.

### **Computing plug-in estimates**

The table below shows how to compute various plug-in estimates from a univariate set of measurements using NumPy.

| Property           | Plug-in estimate      |
|--------------------|-----------------------|
| mean               | `np.mean(x)`          |
| median             | `np.median(x)`        |
| variance           | `np.var(x)`           |
| standard deviation | `np.std(x)`           |
| _p_ percentile     | `np.percentile(x, p)` |

Two-dimensional distributions additionally have properties like covariances and correlations. For two-dimensional distributions, the plug-in estimates are:

| Property           | Plug-in estimate              |
|--------------------|-------------------------------|
| covariance         | `np.cov(x, y, ddof=0)[0, 1]`  |
| correlation        | `np.corrcoef(x, y)[0, 1]`     |

### **Functions from the Part 1**

We laid a lot of the groundwork we need in this live session in the previous one. We wrote functions that are useful for our hacker stats approaches to inference. First, we wrote a function to compute the *x*- and *y*-values for plotting ECDFs.

```python
def ecdf(data):
    """Compute ECDF for a one-dimensional array of measurements."""
    # Number of data points
    n = len(data)

    # x-data for the ECDF
    x = np.sort(data)

    # y-data for the ECDF
    y = np.arange(1, n + 1) / n

    return x, y
```

We also wrote a pair of functions that enable us to draw many bootstrap replicates for a plug-in estimate.

```python
def bootstrap_replicate_1d(data, func):
    """Generate bootstrap replicate of 1D data."""
    bs_sample = np.random.choice(data, len(data))

    return func(bs_sample)

def draw_bs_reps(data, func, size=1):
    """Draw `size` bootstrap replicates."""
    # Initialize array of replicates
    bs_replicates = np.empty(size)

    # Generate replicates
    for i in range(size):
        bs_replicates[i] = bootstrap_replicate_1d(data, func)

    return bs_replicates
```

### **Automating and organizing confidence intervals**

In our analysis in Part 1, we took an array of data, computed the x- and y-values for an ECDF, defined a function to compute a plug-in estimate (in our case, the mean), used the function to compute the estimate, generated bootstrap replicates of the estimate, and computed a confidence interval. We did this for two categories, normal sleepers and insomniacs.

At various points in our analyses, we might like to access these results. It is therefore useful to define a class to compute and store the results of an ECDF, plug-in estimate, and confidence interval calculation for a one-dimensional data set. Many participants in this live training may not be familiar with Python's classes, so we will instead mimic a class by writing a function that does the necessary computations and returns the results in a dictionary.

```python
def plugin_summary(
    data, func, ptiles=(2.5, 97.5), n_bs_reps=10000, label=None
):
    """Compute and store ECDF, plug-in estimate, and confidence
    intervals in a dictionary."""
    # Initialize output dictionary

    # Store data and settings

    # Compute ECDF x and y values

    # Compute plug-in estimate

    # Compute bootstrap confidence interval

    return summary
```

Importantly, whenever we instantiate this class, we get an object that has the values for plotting an ECDF, a plug-in estimate, and a bootstrap confidence interval for that measurement. For example, to repeat the analyses from last time, we can pull out the percent correct for normal sleepers and for insomniacs and instantiate the class.

```python
# Initialize list of plug-in summaries

# Iterate through groups and instantiate conf intervals

# Adjust label names to be descriptive

```

We now have a list of dictionaries that contain our analyses. Conveniently, we have the data set, what function was used to compute the plug-in estimate, the percentiles used for the confidence interval, the number of bootstrap replicates we used, which category we are considering (insomniacs vs. normal sleepers), the *x*- and *y*-values for the ECDF, the plug-in estimate for the mean, the bootstrap replicates we drew and the confidence interval. A treasure trove!

Now that we have this convenient way of storing our analysis results, we should modify our plotting function from last time to plot confidence intervals that are stored in these dictionaries.

```python
def plot_conf_ints(summaries, palette=None):
    """Plot confidence intervals with estimates."""
    # Set a nice color palette
    if palette is None:
        palette = [
            "#1f77b4",
            "#ff7f0e",
            "#2ca02c",
            "#d62728",
            "#9467bd",
            "#8c564b",
            "#e377c2",
            "#7f7f7f",
            "#bcbd22",
            "#17becf",
        ]
    elif type(palette) == str:
        palette = [palette]

    labels = [ci["label"] for ci in summaries][::-1]
    estimates = [ci["estimate"] for ci in summaries][::-1]
    conf_intervals = [ci["conf_int"] for ci in summaries][::-1]
    palette = palette[: len(labels)][::-1]

    # Set up axes for plot
    fig, ax = plt.subplots(figsize=(5, len(labels) / 2))

    # Plot estimates as dots and confidence intervals as lines
    for i, (label, est, conf_int) in enumerate(
        zip(labels, estimates, conf_intervals)
    ):
        color = palette[i % len(palette)]
        ax.plot(
            [est],
            [label],
            marker=".",
            linestyle="none",
            markersize=10,
            color=color,
        )

        ax.plot(conf_int, [label] * 2, linewidth=3, color=color)

    # Make sure margins look ok
    ax.margins(y=0.25 if len(labels) < 3 else 0.125)

    return ax
```

We can now put this class to use to make two of our plots from Part 1. We'll start with the ECDFs.

```python
# Dictionary connecting category to color
colors = {"normal": "#1f77b3", "insomniac": "#ff7f0e"}

# Plot the ECDFs
```

And now the confidence intervals.

```python
# Make plot

# Set the axis label

```

## **Our approach**

We are now all set to continue with statistical inference on this data set. In this live training, we will address the following questions (with question (1) answered in Part 1 and reviewed above).

2. How different is *confidence* in facial matching for insomniacs versus normal sleepers?
3. How are the different sleep metrics correlated?
4. How do sleep metrics influence facial matching performance?

Along the way, we will introduce the necessary new theoretical and technical concepts.

---

 Q&A 1

---

## **2. Confidence of insomniacs versus normal sleepers**

We have seen that there seems to be a difference in performance between normal sleepers and insomniacs. Now, let's compare how well the subjects *think* they are doing in their responses. We will investigate their confidence when correct and incorrect in the face matching tasks for both insomniacs and normal sleepers. We start by extracting the measurements from the DataFrame as NumPy arrays.

```python
# Extract confidences for normal sleepers
conf_corr_normal = (
    df.loc[~df["insomnia"], "confidence when correct"].dropna().values
)
conf_incorr_normal = (
    df.loc[~df["insomnia"], "confidence when incorrect"].dropna().values
)

# Extract confidences for insomniacs
conf_corr_insom = (
    df.loc[df["insomnia"], "confidence when correct"].dropna().values
)
conf_incorr_insom = (
    df.loc[df["insomnia"], "confidence when incorrect"].dropna().values
)
```

### **Performing the analysis (the automated way)**

The analysis pipeline is much the same as we did for looking at the percent correct in the first lesson. We will again compute means and confidence intervals of the mean. We can conveniently use our `plugin_summary()` functions to do all of the calculations and give us a dictionary with the results.

```python
# Put into dictionaries

```

### **EDA**

While we did just compute confidence intervals, we should always do a bit of EDA to check things out. We will plot the ECDFs of all four classes (confidence when correct and incorrect for normal sleepers and insomniacs) and overlay them on the same plot.

```python
# Dictionary of kwargs for convenience

# Plot ECDFs

# Adjust labels

```

We see a few striking trends directly from the ECDF.

1. Regardless of sleep quality, subjects are less confident in their responses when they are wrong.
2. When they are right, insomniacs and normal sleepers seem to have the same level of confidence in their responses.
3. When they are wrong, insomniacs have more confidence in their responses than normal sleepers.

This last point is really interesting. The insomniacs seem less likely to *know when they are wrong.* This kind of cognitive impairment could have real repercussions in how they go about their lives. We will give this last difference special attention.

### **Plug-in estimates and confidence intervals**

We have already computed the plug-in estimates for the mean and confidence intervals when we instantiated the classes. Let's compare them.

```python
# Make plot of confidence intervals

# Set axis label

```

The evidence from this plot is clear. Insomniacs are more confident that they have provided a correct answer when they have not. The difference is about 10% on the confidence scale, which is about 20% of the entire range of responses from subjects about their confidence. If we were to perform the experiment again, we would very likely continue to see this discrepancy.

### Defining null hypothesis significance tests

We have already drawn telling conclusions from the data from the analysis above. Considering the confidence when incorrect, the rightward shift of the ECDF for insomniacs relative to normal sleepers strongly suggests the overconfidence of insomniacs. The confidence intervals make clear that this effect is not due to the inherent variability among participants and the small sample size. We really could conclude our analysis there.

Nonetheless, we will proceed to do a null hypothesis significance test. In some fields, this is referred to as an A/B test. I do so primarily because they are widely used and are important to know how to do and to interpret. I will give further commentary about why the results of NHST should not carry as much weight as conclusions derived from graphical EDA with ECDFs and calculation of confidence intervals after we perform the analysis.

I begin by reviewing what a NHST is. A typical hypothesis test consists of these steps.

1. Clearly state the hypothesis being considered, referred to as the **null hypothesis**.
2. Define a **test statistic**, a scalar value that you can compute from data, almost always a statistical functional of the empirical distribution. Compute it directly from your measured data.
3. *Simulate* data acquisition for the scenario where the null hypothesis is true. Do this many times, computing and storing the value of the test statistic each time.
4. The fraction of simulations for which the test statistic is at least as extreme as the test statistic computed from the measured data is called the **p-value**, which is what you report.

We need to be clear on our definition here. **The p-value is the probability of observing a test statistic being at least as extreme as what was measured if the null hypothesis is true.** It is exactly that, and nothing else. It is not the probability that the null hypothesis is true. In the frequentist interpretation of probability, we cannot assign a probability to the truth of a hypothesis.

A complete definition of a hypothesis test then has the following components.

- The null hypothesis.
- The test statistic.
- What it means to be at least as extreme.

All of the named hypothesis tests you may have heard of, like the Student-t test, the Mann-Whitney U-test, Welch’s t-test, etc., describe a specific hypothesis with a specific test statistic with a specific definition of what it means to be at least as extreme (e.g., one-tailed or two-tailed). I can never remember what these are, nor do I encourage you to; you can always look them up. (I like [Kanji's *100 Statistical Tests*](https://dx.doi.org/10.4135/9781849208499) for this purpose.) Rather, you should just clearly write out what your test is in terms of the hypothesis, test statistic, and definition of extremeness.

### **Performing a NHST**

To perform a hypothesis test using hacker stats, we will follow the prescription laid out above. We of course have to start by specifying our test.

- Null hypothesis: The generative distributions for reported confidence is the same for insomniacs as it is for normal sleepers.
- Test statistic: Mean confidence of normal sleepers minus that of insomniacs.
- At least as extreme as: The test statistic is less than or equal to the observed difference of means.

The bread and butter of conducting a hypothesis test is simulating data acquisition under the null hypothesis. For this specific hypothesis, that the distributions are the same, there is a very straight-forward way of simulating it. Let `x` and `y` be our two arrays, with `m` entries in `x` and `n` entries in `y`.

1. Concatenate the two data sets into one. This is done with `np.concatenate((x, y))`.
2. Randomly scramble the order of the combined data set. This is accomplished using `np.random.permutation()`.
3. Designate the first `m` entries in this scrambled array to be "`x`" and the remaining to be "`y`."

Having simulated the data acquisition process, we then use these new "`x`" and "`y`" arrays to compute the test statistic.

This simulation is exact; it is as if the labels of the data set (in this case "normal sleeper" and "insomniac") have no meaning; hence the distributions of the two data sets are entirely equal. Note that we are *not* say what the distribution is, only that the two data sets have the *same* distribution.

A test done in this way is referred to as a **permutation test**. A **permutation sample** is akin to a bootstrap sample; it is a new pair of data sets generated after scrambling the concatenated data set. A **permutation replicate** is a value of the test statistic computed from a permutation sample, in this case the difference of means.

Let's code up the functions to do the test, starting with generating the permutation sample.

```python
def permutation_sample(data1, data2):
    """Generate a permutation sample from two data sets."""
    # Concatenate the data sets

    # Permute the concatenated array

    # Split the permuted array into two

    return perm_sample_1, perm_sample_2
```

We need to provide a function that takes in two data sets (a permutation sample or the original data sets) and computes the test statistic. In our case, this is the difference of means.

```python
def diff_of_means(data_1, data_2):
    """Difference in means of two arrays."""
```

Finally, we can write a function to generate multiple permutation replicates.

```python
def draw_perm_reps(data_1, data_2, func, size=1):
    """Generate multiple permutation replicates."""
    # Initialize array of replicates: perm_replicates

    for i in range(size):
        # Generate permutation sample

        # Compute the test statistic

    return perm_replicates
```

We'll start with the confidence when giving correct answers to the face matching test for insomniacs versus normal sleepers. We start by computing the test statistic from the actual measurements.

```python
# Compute difference of means from original data

# Take a look
```

Now, we'll draw permutation replicates.

```python
# Acquire permutation replicates

```

Now, we'll see what fraction were less than or equal to the test statistic calculated from the measured data sets.

```python
# Compute p-value

# Take a look

```

In this case, the p-value is about 0.15, which means that about 15% of the time, the null hypothesis that the confidence when correct for insomniacs and normal sleepers is the same will generate a difference of means at least as different in magnitude than what was observed. The data are then commensurate with the null hypothesis; they do not serve to rule out that hypothesis.

Now, let's do the same analysis for the case where the subjects responded incorrectly to the face matching test.

```python
# Compute difference of means from original data

# Acquire permutation replicates

# Compute p-value

# Take a look

```

This p-value is much smaller. Only 0.1 or 0.2% of the permutation samples had a difference of means less than or equal to what was observed. According to this result, it is unlikely that the observed data set could have been generated by identical distributions for normal sleepers and insomniacs. We already knew this from our analysis of the confidence intervals, and this is the icing on the cake, to borrow a phrase from [Allen Downey](http://www.allendowney.com/wp/).

### **Do not overemphasize NHSTs**

It is important to know exactly what an NHST gives you, a p-value. It is the probability of observing a test statistic being at least as extreme as what was measured if the null hypothesis were true. To put interpretation of the p-value in perspective, we can compare to the interpretation of plug-in estimates and confidence intervals. Let's consider three questions addressed by these calculations.

1. **Plug-in estimate**: How different are the means of the two samples?
2. **Confidence interval**: If we were to do the experiment again, how similar would our plug-in estimates be?
3. **NHST**: What is the probability of observing a difference in means of the two samples at least as large as the observed difference in means, if the two samples in fact have the same generative distribution?

I contend that a person thinking about their dataset might naturally ask the first two questions, but the third question is convoluted and of little practical interest.

To add further perspective, say we made trillions of measurements of two different samples and their mean differs by one part per million. This difference, though tiny, would still give a low p-value, and therefore often be deemed "statistically significant." But, ultimately, it is the size of the difference, or the **effect size**, relative to the experiment-to-experiment variation, that we care about.

Put succinctly: **Statistical significance and practical significance are not the same thing.**

---

 Q&A 2

---

## **3. Correlation of sleep metrics**

For the next study of the data set, we will explore the correlations among the three sleep/drowsiness metrics. Remember that two variables are **correlated** if one is high while the other is high. They are anticorrelated if one is high while the other is low. And they are said to be uncorrelated if the level of one is not related to the level of the other.

We can do a quick exploration of the relationship between each pair of sleep indexes by making scatter plots of each pair of indexes. This is conveniently accomplished using seaborn's `pairplot()` function.

```python
_ = sns.pairplot(df[['sci', 'psqi', 'ess']], diag_kind=None, height=2)
```

In glancing at these plots, the SCI and PSQI are clearly anticorrelated. This makes sense; they are both assess a similar feature of the subjects, the quality of nighttime sleep. Their scales are such that quality sleep is a high number in the SCI and a low number in the PSQI, so we would expect them to be anticorrelated.

However, visually at least, the ESS does not seem to correlate with SCI or PQSI. Recall that ESS is a measure of daytime drowsiness. We might expect that to correlate with sleep quality, but it does not seem to, at least graphically (though the analysis we will soon do may show that it does). Previous studies (e.g., [this one](http://dx.doi.org/10.1016/j.smrv.2009.04.002)) have shown that insomniacs experience hyperarousal, meaning that they are not drowsy as a result of their poor sleep. This would suggest that we should not be surprised by a lack of correlation between SCI and ESS.

### **Plug-in estimate for correlation**

With the graphical analysis out of the way, let's investigate the Pearson correlation between the pairs of metrics. We can compute the plug-in estimate using `np.corrcoef()`, first extracting the data as NumPy arrays. Remember that the Pearson correlation is 1 for perfect correlation, –1 for perfect anticorrelation, and zero for complete absence of correlation.

```python
# Extract all three sleep/drowsiness metrics as NumPy arrays

# Compute Pearson correlation between each pair

# Print the result
print('plug-in SCI-PSQI correlation:', rho_sci_psqi)
print('plug-in SCI-ESS correlation: ', rho_sci_ess)
print('plug-in PSQI-ESS correlation:', rho_psqi_ess)
```

Indeed, the plug-in estimates suggest that SCI and PSQI are strongly anticorrelated, but the SCI and PSQI are not really correlated with ESS.

### **Bootstrap confidence intervals for correlation**

At face, it may seem more difficult to compute bootstrap confidence intervals for correlation, we are going to have to sample out of *two* arrays. However, we can again directly apply the plug-in principle and rely on our coding skills to do it.

Consider for a moment the SCI and PSQI. Each individual subject was tested for these two sleep metrics. We cannot sample out of the array of SCI's and PSQI's independently, since they were measured *together* for each subject. Instead, we will choose a *subject* at random, and then record *both* his or her SCI and PSQI score as we build our bootstrap sample. Stated another way, we randomly choose *pairs* of scores.

This procedure is called **pairs bootstrap**. To implement pairs bootstrap on a pair of arrays of equal length, we randomly sample an *index* of the arrays, and then select the pair of values that share that index.

Let's write a function to draw bootstrap pairs. As with our function for drawing bootstrap replicates for one-dimensional data, we will supply a function to compute the estimate as an argument. It must have the call signature `func(x, y)`, taking two arrays as arguments and returning a scalar result.

```python
def draw_bs_pairs(x, y, func, size=1):
    """Perform pairs bootstrap for single statistic."""
    # Set up array of indices to sample from: inds

    # Initialize replicates: bs_replicates

    # Generate replicates

    return bs_replicates
```

Now that we have this function in place, we can write our function to compute the Pearson correlation.

```python
def pearson_r(x, y):
    """Compute Pearson correlation coefficient between two arrays."""
    # Compute correlation matrix

    return corr_mat[0,1]
```

Ok, let's draw our bootstrap replicates! We will do it for each pair of sleep indexes, SCI–PSQI, SCI–ESS, and PSQI–ESS.

```python
# Pairs bootstrap replicates of correlation

```

Now that we have our replicates, we can compute the confidence intervals, again by taking percentiles as we have done in the one-dimensional case.

```python
# Pairs bootstrap confidence intervals

```

To plot the results, we would like to use our `plot_conf_ints()` function. It expects a dictionary has `label`, `estimate`, and `conf_int` keys.

```python
sci_psqi = dict(
    label="SCI–PSQI", estimate=rho_sci_psqi, conf_int=sci_psqi_conf_int
)
sci_ess = dict(
    label="SCI–ESS", estimate=rho_sci_ess, conf_int=sci_ess_conf_int,
)
psqi_ess = dict(
    label="PSQI–ESS", estimate=rho_psqi_ess, conf_int=psqi_ess_conf_int,
)
```

Now that we have them in convenient dictionaries, we can make our plot of the confidence intervals of the correlations.

```python
# Make plot

```

There is very clearly strong correlation between SCI and PSQI, but no real correlation between those metrics and the ESS metric for daytime drowsiness.

---

 Q&A 3

---

## **4. Effect of sleep quality on performance**

We have just investigate how the different sleep metrics are correlated. Now, we turn our attention to how the sleep metrics affect performance in the face matching test. We will start by considering the SCI. As usual, we'll do a little EDA and make a scatter plot of the percent correct versus SCI.

```python
_ = plt.plot(
    df["sci"], df["percent correct"], marker=".", linestyle="none", alpha=0.5,
)
_ = plt.xlabel("SCI")
_ = plt.ylabel("percent correct")
```

If we squint, we can see there may be better scores for higher SCI. We will attempt to quantify this.

We could compute the correlation between SCI and percent correct in the test, similarly to how we computed correlation between SCI and PSQI. But this is not really what we want to do. We want to know how SCI *explains* performance in the test. Contrast that with the analysis we just did in computing correlation of SCI and PSQI. We did not try to assert that one influences the other, but that the same thing, namely nighttime sleep quality, may influence them both, and hence make them correlated. In that case, a correlation was appropriate, but now, we are interested in how SCI *influences* performance. So here, a linear regression is appropriate.

This subtle difference may be explained by investigating the mathematical expressions for correlation and for the slope computed from a linear regression by least squares.

\begin{align}
\text{Pearson correlation} = \frac{\sigma_{xy}}{\sigma_x\,\sigma_y}, \\[1em]
\text{slope of lin. reg.} = \frac{\sigma_{xy}}{\sigma_x^2}.
\end{align}

Here, $\sigma_{xy}$ is the covariance of $x$ and $y$, $\sigma_x^2$ is the variance of $x$, and $\sigma_y^2$ is the variance of $y$. The Pearson correlation is a comparison of covariance to the geometric mean of the variances of the individual variables. The slope computed from linear regression by least squares is a comparison of the covariance to the variance in the x-variable, the variable that may explain the variation in y-variable. The slope of the linear regression is thus asymmetric; we are looking at the effects of *x* on *y*, in this case the effects of SCI on percent correct.

### **Performing a linear regression**

When we perform a linear regression, we seek to find the line that is closest to the data in a least squares sense. This means that we want to find the line that passes through the data such that the sum of the square of the distance between the line and each data point is as small as possible.

This calculation involves some beautiful mathematics, and the result is given above. The slope of the line is given by the ratio of the covariance to the variance in x. The intercept is given by the mean of the y-values minus the slope times the mean of the x-values. We can directly compute the slope and intercept using these formulas.

```python
# Extract SCI and percent correct from data frame
sci = df['sci'].dropna().values
pcorr = df['percent correct'].dropna().values

# Compute slope

# Compute intecept

# Print results
print("slope:     ", slope)
print("intercept: ", intercept)
```

Indeed, we see what our squinting suggested; there is a slight positive slope. For every unit increase in SCI, users get an improvement of 0.32% in their performance on the facial matching test.

I pause to note here that the slope (which is of primary interest) and the intercept are numbers that are computed directly from the data. *They are therefore plug-in estimates.* All of the same principles we have been using still apply.

I wanted to show you the respective formulas for the slope and intercept to make that point clear; we are "just" computing variances and means directly from the data and taking some ratios. In practice, we can more conveniently compute the slope and intercepts for a linear regression using the `np.polyfit()` function. It fits (*x*, *y*) data with a polynomial of arbitrary degree. A line is a polynomial of degree 1, so we can use it!

```python
# Use polyfit to compute slope and intercept

# Show that it's the same as our formulas
print("slope:     ", slope)
print("intercept: ", intercept)
```

### **Bootstrap replicates of linear regression parameters**

Since the linear regression parameters are computed directly from data, we can again apply pairs bootstrap and compute the parameters. Since we might also be interested in the intercept, we can adapt our `draw_bs_pairs()` function to return two arrays of bootstrap replicates (one for slope and one for intercepts).

```python
def draw_bs_pairs_linreg(x, y, size=1):
    """Perform pairs bootstrap for linear regression."""
    # Set up array of indices to sample from: inds

    # Initialize replicates: bs_slope_reps, bs_intercept_reps

    # Generate replicates

    return bs_slope_reps, bs_intercept_reps
```

Let's use this function to get some bootstrap replicates for the slope and intercepts!

```python
bs_slope_reps, bs_intercept_reps = draw_bs_pairs_linreg(sci, pcorr, size=10000)
```

As before, we can compute the confidence intervals using `np.percentile()`. The slope is of primary interest, so we can compute its confidence interval.

```python
# Compute confidence interval from percentiles

# Take a look

```

The confidence interval juuuust crosses zero, suggesting that most likely there is a real positive slope, meaning that SCI does inform performance on the test.

I will visualize the linear regression on the scatter plot. To do so, I like to plot a bunch of the regression lines on the same plot as the scatter plot so I can see how the line might change if we were to do the experiment again.

```python
# x-values for lines

# Plot 300 lines (thin with a bit of transparency)

# Plot data

# Label axes

```

Indeed in this graphic, we see that on rare occasion the slope is close to zero or slightly negative, but most of the lines show positive slope.

## A NHST for a linear regression

The above analysis, in which we computed a plug-in estimate for the slope, its confidence interval, and created a visualization, is sufficient, in my opinion, for this analysis. Nonetheless, we might want to perform a null hypothesis test. Remember, we have to specify three things; here is a reasonable test.

1. Null hypothesis: The SCI is completely inconsequential when it comes to performance on the facial matching test.
2. Test statistic: The slope of the linear regression line.
3. At least as extreme as: The slope is greater than or equal to what was acquired from the measured data.

Again, the trick is in simulating the null hypothesis. If the SCI has no bearing on the performance of the facial matching test, we can scramble the subjects' SCI scores, thereby decoupling them from the test scores. We can then compute the slope from the scrambled dataset. This is another version of a permutation test.

Let's code up our function to draw permutation replicates of the slope.

```python
def draw_perm_reps_slope(x, y, size=1):
    """Draw permuation replicates of the slope from lin. reg."""
    # Initialize array of replicates: perm_replicates

    for i in range(size):
        # Scramble x-values

        # Compute the slope

    return perm_replicates
```

Now we can draw our replicates (we'll take 10,000 of them) and compute the p-value.

```python
# Draw permutation replicates
perm_reps = draw_perm_reps_slope(sci, pcorr, size=10000)

# p-value

```

Our p-value is about 0.02, suggesting that the measured data may not be commensurate with the null hypothesis being true. This again confirms what we already saw in our analysis of the plug-in estimate with confidence interval.

---

 Q&A 4

---

## **Conclusions**

We have seen that even when we have multidimensional data, the principles we apply to do nonparametric statistical inference are the same: We directly apply the plug-in principle and the frequentist interpretation of probability. We sometimes have to do bootstrapping with pairs of data, but the central ideas are the same.

Null hypothesis significance testing is also possible with hacker stats. We simply directly apply the plug-in principle to simulate data acquisition under the null hypothesis. While we can perform NHSTs, they often add little insight, and we should be very careful to not over-interpret them.

## **Take-home questions**

There is much to still explore in this dataset, and I encourage you to do so. In particular, you might want to investigate the following:

- Do the other sleep metrics, particularly the ESS which measured drowsiness, inform performance?
- Does performance in the tests correlate with age? How about confidence?

Finally, I'll note that the [dc_stat_think package](https://github.com/justinbois/dc_stat_think) contains all of the functions that learners wrote in the Statistical Thinking courses (including Case Studies in Statistical Thinking). It is important that you know how to write all of your own functions, but this package can help you apply hacker stats in your analyses.


```python
%run common-imports.ipynb
```

<link href="https://fonts.googleapis.com/css?family=Lora:400,700|Montserrat:300" rel="stylesheet">

<link href="https://fonts.googleapis.com/css2?family=Crimson+Pro&family=Literata&display=swap" rel="stylesheet">

#  Dataset-4

## Prerequisites

prior notebooks:

* Notebook 1: `univariate-1.ipynb`
* Notebook 2: `dataset-2.ipynb`
* Notebook 3: `dataset-3.ipynb`
* Notebook 4: `univariate-2.ipynb`
* Notebook 5: `univariate-3.ipynb`

## Lab Goals

* will explore this data, observe its statistical characteristics, visualize it.
* Next, we will take a systematic approach to build  linear and polynomial regression models to make prediction on the data.

## Outcome

This lab should have given you some more fluency in dealing with polynomial regression, and under its limitations when dealing with data more appropriately modeled with transcendental functions.

```python
#source = 'https://raw.githubusercontent.com/'
data = pd.read_csv("../datasets/dataset-4.csv")
```

```python
data.sample(5)
```

      x
      y

      62
      -4.405331
      41.710608

      40
      -9.303292
      135.382126

      95
      0.568865
      -5.558952

      18
      -1.059046
      5.130926

      97
      -5.119469
      48.733724

#### Descriptive statistics

```python
data.describe(include="all").transpose()
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      x
      99.0
      0.025950
      5.795069
      -9.792249
      -4.489466
      -0.653896
      5.263204
      9.953913

      y
      99.0
      43.817392
      38.695960
      -5.558952
      14.393938
      32.658142
      66.336876
      151.376254

#### Missing Values Analysis

```python
data.isnull().sum()
```

    x    0
    y    0
    dtype: int64

 From the above results, it appears that there are no missing values at all. Therefore, we don't need to worry about addressing this issue.

#### Pandas Profiling

```python
# data.profile_report()
```

## Data Visualization

### Plotting using matplotlib

Observe that the data distinctly exhibits nonlinearity of relationship between $x$ and $y$. **what the correlation is between the variables?**

```python
plt.scatter(data['x'], data['y'], alpha=0.5, s=150, color='salmon')
plt.title(r'\textbf{Scatter-plot of $y$ vs $x$}')
plt.xlabel(r'$x\longrightarrow$');
plt.ylabel(r'$y\longrightarrow$');
```

# Regression

From the figure above, it should be apparent that a simple linear regression model is unlikely to work. However, let us first build a simple linear regression model for this dataset, in order to get a baseline performance.

As usual, we will first separate out the predictor from the target, and then split the data into a training and test set.

```python
X, y = data[['x']], data['y']
```

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42,train_size=0.5)
```

## Build a linear regression model

Let's build a regression model, and fit it to the dataset. For this, we instantiate a `LinearRegression` object named `model` using the constructor. Then we fit the `model` to the available training dataset.

```python
model = LinearRegression();
model.fit(X_train, y_train);
```

Recall that a linear regression model is given by the equation:

\begin{equation} y =  \beta_0 + \beta_1 x + \epsilon\end{equation}

where:
$\epsilon$ is the irreducible error term, so that the model is essentially:

\begin{equation} \hat{y} =  \beta_0 + \beta_1 x \end{equation}

What values of $\beta_0$ (the intercept) and $\beta_1$ (the slope) is this model predicting? We can inspect this as follows:

```python
print (f'Intercept: {model.intercept_}, Slope: {model.coef_}')
```

    Intercept: 40.857817915455506, Slope: [-2.24854015]

Look back at the data visualization, and see if this agrees with your own estimates.

## Predictions from the model

Now, let's use the model to make predictions on the **test** data, something the model has not seen so far. By comparing the predictions to the actual values, we will get a sense of how well the model has learned to generalize from the data.

```python
yhat = model.predict(X_test)
print("Mean Squared Error: %.2f"
      % mean_squared_error(y_test, yhat))
r2 = r2_score(y_test, yhat)
print(rf"Coefficient of Determination (R^2):{r2}")
```

    Mean Squared Error: 1456.24
    Coefficient of Determination (R^2):0.0491860854062125

The coefficient of determination, $R^2$ indicates a dismal model! Should we plod on to the next step of model verification: namely the residual analysis.

## Residual Analysis

will start by plotting the residuals from the predictions. Recall that the residual from the prediction $\hat{y}_i$ on a particular datum $(x_i, y_i)$ is defined as:

\begin{equation}\mathbf{ r_i = \hat{y}_i  - y_i }\end{equation}

will use the `yellowbrick` library for looking at the model characteristics.

```python
from yellowbrick.regressor import residuals_plot
viz = residuals_plot(model, X_train, y_train, X_test, y_test,
                     train_color='orange', test_color='IndianRed', train_alpha=0.3, test_alpha=0.5)
```

The residuals display a striking pattern! The presence of a pattern in the residuals in a clear indication that the model has failed to capture some essential characteristics of the relationship between $x$ and $y$.

## Visualization of the model predictions

As a final step, let us visualize the predictions of the model, and superimpose it on the actual data. This should give us a sense of how well the model is working.

```python

X = pd.DataFrame(data={'x': np.linspace(data.x.min(), data.x.max(), 1000)})
yhat = model.predict(X)
```

```python
fig, ax = plt.subplots(figsize=(20,10))
ax.scatter(data['x'], data['y'], alpha=0.5, s=150, color='salmon')
ax.plot(X.x, yhat, 'r--.', label="Model Predictions")
ax.legend(loc='best');
```

This simple linear model has, as one would have expected by now, failed terribly!

##### Polynomial Regression

Let's add polynomial features to the dataset, and then performing the regression can be an effective tool.

merged the steps of:
* create polynomial degrees of the input
* fit a linear model to the data
* make predictions on test data
* print model diagnostics

```python
degree     = 2
polynomial = PolynomialFeatures(degree)
X_poly = polynomial.fit_transform(X_train)
model = LinearRegression()
# Now, train the model
model.fit(X_poly, y_train)
print ("The cofficients: {}".format(model.coef_))
X_poly_test = polynomial.fit_transform(X_test)
yhat = model.predict(X_poly_test)
print("Mean squared error: %.2f"
      % mean_squared_error(y_test, yhat))
r2 = r2_score(y_test, yhat)
print(rf"Coefficient of Determination (R^2):{r2}")
```

    The cofficients: [ 0.         -2.52019403  1.29303536]
    Mean squared error: 10.86
    Coefficient of Determination (R^2):0.9929123351880997

The coefficient of determination seems quite encouraging. Let us now proceed to the residual analysis.

## Residual Analysis

will start by plotting the residuals from the predictions. Recall that the residual from the prediction $\hat{y}_i$ on a particular datum $(x_i, y_i)$ is defined as:

\begin{equation}\mathbf{ r_i = \hat{y}_i  - y_i }\end{equation}

We will use the `yellowbrick` library for looking at the model characteristics.

```python
from yellowbrick.regressor import residuals_plot
viz = residuals_plot(model, X_poly, y_train, X_poly_test, y_test,
                     train_color='orange', test_color='IndianRed', train_alpha=0.3, test_alpha=0.5)
```

There is a homoscedasticity of the residuals, and no patterns are present in the residuals. This too is an encouraging sign.

## Visualization of the model predictions

As a final step, let us visualize the predictions of the model, and superimpose it on the actual data. This should give us a sense of how well the model is working.

```python

X = pd.DataFrame(data={'x': np.linspace(data.x.min(), data.x.max(), 1000)})
yhat = model.predict(polynomial.transform(X))

fig, ax = plt.subplots(figsize=(20,10))
ax.scatter(data['x'], data['y'], alpha=0.5, s=150, color='salmon')
ax.plot(X.x, yhat, 'r--.', label="Model Predictions")
ax.legend(loc='best');
```

A careful observation of the prediction curve shows that it has fit the data with high fidelity.

# Conclusion

For this dataset, polynomial regression has proved effective.


# Multi-label COCO

```python
#@title

! pip -qq install tensorflow-addons
! pip install -qq -U tensorflow_datasets

IS_COLAB = 'google.colab' in str(get_ipython())

if IS_COLAB:
  from google.colab import drive
  drive.mount('/content/drive')

import tensorflow as tf

for d in tf.config.list_physical_devices('GPU'):
  print(d)
  print(f'Setting device {d} to memory-growth mode.')
  try:
    tf.config.experimental.set_memory_growth(d, True)
  except Exception as e:
    print(e)
```

```python
if IS_COLAB:
  base_dir = '/content/drive/MyDrive'
  data_dir = '/root/tensorflow_datasets'
else:
  base_dir = '/home/ldavid/Workspace'
  data_dir = '/home/ldavid/Workspace/datasets'

class Config:
  class data:
    data_dir = '/home/ldavid/Workspace/datasets'
    size = (512, 512)
    shape = (*size, 3)
    batch_size = 32
    shuffle_buffer_size = 8 * batch_size
    prefetch_buffer_size = tf.data.experimental.AUTOTUNE
    train_shuffle_seed = 2142
    shuffle = True

    preprocess = tf.keras.applications.resnet_v2.preprocess_input
    deprocess = lambda x: tf.cast(tf.clip_by_value((x+1)*127.5, 0, 255), tf.uint8)

  class aug:
    brightness_delta =  .2
    saturation_lower =  .2
    saturation_upper = 1.0
    contrast_lower   =  .5
    contrast_upper   = 1.5
    hue_delta        =  .0

  class model:
    pool_layer = 'avg_pool'
    backbone = tf.keras.applications.ResNet101V2

    ae_latent_dim = 128
    ae_intermediate_dim = 512

  class training:
    epochs = 80
    learning_rate = 1.0
    lr_first_decay_steps = 50

    fine_tune_lr = 0.01
    fine_tune_epochs = 80
    fine_tune_layers = .6
    freeze_batch_norm = False

    fine_tune_early_reduce_lr_patience = 3
    fine_tune_early_stopping_patience = 20

  class experiment:
    seed = 218402
    override = True
    logs              = f'{base_dir}/logs/coco/resnet101-sw-1-ce/'
    fine_tune_logs    = f'{base_dir}/logs/coco/resnet101-sw-1-ce-fine-tune/'

    training_weights  = f'{base_dir}/models/coco/resnet101-sw-1-ce/weights.h5'
    fine_tune_weights = f'{base_dir}/models/coco/resnet101-sw-1-ce-fine-tune/weights.h5'
```

## Setup

```python
import os
import shutil
from math import ceil

import numpy as np
import pandas as pd
import tensorflow_addons as tfa
import tensorflow_datasets as tfds
import matplotlib.pyplot as plt
import seaborn as sns

from tensorflow.keras import callbacks
```

```python
for d in tf.config.list_physical_devices('GPU'):
  print(d)
  print(f'Setting device {d} to memory-growth mode.')
  try:
    tf.config.experimental.set_memory_growth(d, True)
  except Exception as e:
    print(e)
```

```python
R = tf.random.Generator.from_seed(Config.experiment.seed, alg='philox')
C = np.asarray(sns.color_palette("Set1", 21))
CMAP = sns.color_palette("Set1", 21, as_cmap=True)

sns.set_style("whitegrid", {'axes.grid' : False})
```

```python
def normalize(x, reduce_min=True, reduce_max=True):
  if reduce_min: x -= tf.reduce_min(x, axis=(-3, -2), keepdims=True)
  if reduce_max: x = tf.math.divide_no_nan(x, tf.reduce_max(x, axis=(-3, -2), keepdims=True))

  return x

def visualize(
    image,
    title=None,
    rows=2,
    cols=None,
    figsize=(16, 7.2),
    cmap=None
):
  if image is not None:
    if isinstance(image, (list, tuple)) or len(image.shape) > 3:  # many images
      plt.figure(figsize=figsize)
      cols = cols or ceil(len(image) / rows)
      for ix in range(len(image)):
        plt.subplot(rows, cols, ix+1)
        visualize(image[ix],
                 cmap=cmap,
                 title=title[ix] if title is not None and len(title) > ix else None)
      plt.tight_layout()
      return

    if isinstance(image, tf.Tensor): image = image.numpy()
    if image.shape[-1] == 1: image = image[..., 0]
    plt.imshow(image, cmap=cmap)

  if title is not None: plt.title(title)
  plt.axis('off')
```

## Dataset

```bash
%%bash

if [ ! -d "/root/tensorflow_datasets/coco" ]; then
  mkdir -p /root/tensorflow_datasets/coco/2017/1.1.0

  cp /content/drive/MyDrive/datasets/coco/2017/1.1.0/*train* \
     /content/drive/MyDrive/datasets/coco/2017/1.1.0/*validation* \
     /content/drive/MyDrive/datasets/coco/2017/1.1.0/*.json       \
     /content/drive/MyDrive/datasets/coco/2017/1.1.0/*.txt        \
     /root/tensorflow_datasets/coco/2017/1.1.0
else
  echo "Data found at coco/2017 - skipping"
fi
```

```python
def default_policy_fn(image):
  image = tf.image.resize_with_crop_or_pad(image, *Config.data.size)
  # mask = tf.image.resize_with_crop_or_pad(mask, *Config.data.size)

  return image

def augment_policy_fn(image):
  seeds = R.make_seeds(6)

  image = tf.image.resize_with_crop_or_pad(image, *Config.data.size)
  # image = tf.image.stateless_random_crop(image, [*Config.data.size, 3], seed=seeds[:, 0])
  # mask = tf.image.stateless_random_crop(mask, [*Config.data.size, 1], seed=seeds[:, 0])

  image = tf.image.stateless_random_flip_left_right(image, seed=seeds[:, 0])
  # mask = tf.image.stateless_random_flip_left_right(mask, seed=seeds[:, 0])

  image = tf.image.stateless_random_flip_up_down(image, seed=seeds[:, 1])
  # mask = tf.image.stateless_random_flip_up_down(mask, seed=seeds[:, 1])

  image = tf.image.stateless_random_hue(image, Config.aug.hue_delta, seed=seeds[:, 2])
  image = tf.image.stateless_random_brightness(image, Config.aug.brightness_delta, seed=seeds[:, 3])
  image = tf.image.stateless_random_contrast(image, Config.aug.contrast_lower, Config.aug.contrast_upper, seed=seeds[:, 4])
  image = tf.image.stateless_random_saturation(image, Config.aug.saturation_lower, Config.aug.saturation_upper, seed=seeds[:, 5])

  return image
```

```python
(train_dataset, val_dataset), info = tfds.load(
  'coco/2017',
  split=('train', 'validation'),
  with_info=True,
  shuffle_files=False,
)
```

```python
CLASSES = np.asarray(info.features['objects']['label']._int2str)
int2str = info.features['objects']['label'].int2str
```

```python
from functools import partial

@tf.function
def load_fn(d, augment=False):
  image = d['image']
  labels = d['objects']['label']

  image = tf.cast(image, tf.float32)

  image, _ = adjust_resolution(image)
  image = (augment_policy_fn(image)
           if augment
           else default_policy_fn(image))

  image = Config.data.preprocess(image)

  return image, labels_to_one_hot(labels)

def adjust_resolution(image):
  es = tf.constant(Config.data.size, tf.float32)
  xs = tf.cast(tf.shape(image)[:2], tf.float32)

  ratio = tf.reduce_min(es / xs)
  xsn = tf.cast(tf.math.ceil(ratio * xs), tf.int32)

  image = tf.image.resize(image, xsn, preserve_aspect_ratio=True, method='nearest')

  return image, ratio

def labels_to_one_hot(labels):
  return tf.maximum(tf.reduce_max(tf.one_hot(labels, depth=CLASSES.shape[0]), axis=0), 0)

def prepare(ds, batch_size, cache=False, shuffle=False, augment=False):
  if cache: ds = ds.cache()
  if shuffle: ds = ds.shuffle(Config.data.shuffle_buffer_size, reshuffle_each_iteration=True, seed=Config.data.train_shuffle_seed)

  return (ds.map(partial(load_fn, augment=augment), num_parallel_calls=tf.data.AUTOTUNE)
            .batch(batch_size, drop_remainder=True)
            .prefetch(Config.data.prefetch_buffer_size))
```

## Network

```python
print(f'Loading {Config.model.backbone.__name__}')

backbone = Config.model.backbone(
  classifier_activation=None,
  include_top=False,
  input_shape=Config.data.shape
)

backbone.trainable = False
```

### Softmax weights

Dense With Softmax Weights Layer:

\begin{align}
l_c &= X \cdot [W \circ \text{softmax}_c(W - max_c(W))] + b\\
y_c &= \sigma(l)_c
\end{align}

```python
from tensorflow.keras.layers import Conv2D, Dense

class DenseKur(Dense):
  """Dense with Softmax Weights.
  """
  def call(self, inputs):
    kernel = self.kernel
    ag = kernel  # ag = tf.abs(kernel)
    ag = ag - tf.reduce_max(ag, axis=-1, keepdims=True)
    ag = tf.nn.softmax(ag)

    outputs = inputs @ (ag * kernel)

    if self.use_bias:
      outputs = tf.nn.bias_add(outputs, self.bias)

    if self.activation is not None:
      outputs = self.activation(outputs)

    return outputs
```

### Classification Head

```python
from tensorflow.keras.layers import Conv2D, GlobalAveragePooling2D, Dense

def build_classifier():
  x = tf.keras.Input(Config.data.shape, name='images')
  y = backbone(x)
  y = GlobalAveragePooling2D(name='avg_pool')(y)
  y = DenseKur(len(CLASSES), name='predictions')(y)

  return tf.keras.Model(
    inputs=x,
    outputs=y,
    name=f'clf_{Config.model.backbone.__name__}_coco')
```

```python
nn = build_classifier()
```

```python
tf.keras.utils.plot_model(nn, show_shapes=True)
```

## Training

### Loss, Metrics and Model Compilation

```python
class FromLogitsMixin:
  def __init__(self, from_logits=False, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.from_logits = from_logits

  def update_state(self, y_true, y_pred, sample_weight=None):
    if self.from_logits:
      y_pred = tf.nn.sigmoid(y_pred)
    return super().update_state(y_true, y_pred, sample_weight)

class BinaryAccuracy(FromLogitsMixin, tf.metrics.BinaryAccuracy):
  ...

class TruePositives(FromLogitsMixin, tf.metrics.TruePositives):
  ...

class FalsePositives(FromLogitsMixin, tf.metrics.FalsePositives):
  ...

class TrueNegatives(FromLogitsMixin, tf.metrics.TrueNegatives):
  ...

class FalseNegatives(FromLogitsMixin, tf.metrics.FalseNegatives):
  ...

class Precision(FromLogitsMixin, tf.metrics.Precision):
  ...

class Recall(FromLogitsMixin, tf.metrics.Recall):
  ...

class F1Score(FromLogitsMixin, tfa.metrics.F1Score):
  ...
```

```python
from tensorflow.python.keras.callbacks import ReduceLROnPlateau
from tensorflow.python.platform import tf_logging

class ReduceLRBacktrack(ReduceLROnPlateau):
    """
    Reduce Learning Rate on Plateau and restore weights.

    Ref: https://stackoverflow.com/a/55228619/2429640
    """
    def __init__(self, best_path, *args, **kwargs):
        super(ReduceLRBacktrack, self).__init__(*args, **kwargs)
        self.best_path = best_path

    def on_epoch_end(self, epoch, logs=None):
        current = logs.get(self.monitor)
        if current is None:
            tf_logging.warning('Reduce LR on plateau conditioned on metric `%s` '
                               'which is not available. Available metrics are: %s',
                               self.monitor, ','.join(list(logs.keys())))
        if not self.monitor_op(current, self.best):
            if not self.in_cooldown():
                if self.wait+1 >= self.patience:
                    print("Backtracking to best model before reducting LR")
                    self.model.load_weights(self.best_path)

        super().on_epoch_end(epoch, logs)
```

### Top Classifier Training

```python
train = prepare(train_dataset, 2*Config.data.batch_size, shuffle=True, augment=True)
valid = prepare(val_dataset, 2*Config.data.batch_size)
```

```python
nn.compile(
    optimizer=tf.optimizers.SGD(learning_rate=Config.training.learning_rate, momentum=0.9, nesterov=True),
    loss=tf.losses.BinaryCrossentropy(from_logits=True),
    metrics=[
      F1Score(num_classes=len(CLASSES), from_logits=True, average='macro'),
      Precision(from_logits=True),
      Recall(from_logits=True),
      tf.keras.metrics.AUC(multi_label=True, from_logits=True),
    ])
```

```python
def train_fn(initial_epoch):
  try:
    os.makedirs(os.path.dirname(Config.experiment.training_weights), exist_ok=True)

    if os.path.exists(Config.experiment.logs) and initial_epoch == 0:
      if not Config.experiment.override:
        raise ValueError(f'A training was found in {Config.experiment.logs}. '
                        f'Either move it or set experiment.override to True.')

      print(f'Overriding previous training at {Config.experiment.logs}.')
      shutil.rmtree(Config.experiment.logs)

    nn.fit(
      train,
      validation_data=valid,
      epochs=Config.training.epochs,
      initial_epoch=initial_epoch,
      callbacks=[
        callbacks.TerminateOnNaN(),
        callbacks.ModelCheckpoint(Config.experiment.training_weights, save_best_only=True, save_weights_only=True, verbose=1),
        callbacks.TensorBoard(Config.experiment.logs, profile_batch=False),
        callbacks.EarlyStopping(patience=20, verbose=1),
        ReduceLRBacktrack(Config.experiment.training_weights, factor=0.5, patience=3, min_lr=0.001, verbose=1),
      ]);

  except KeyboardInterrupt: print('\ninterrupted')
  else: print('\ndone')
```

```python
train_fn(initial_epoch=0)
```

```python
nn.load_weights(Config.experiment.training_weights)

train_fn(initial_epoch=21)
```

### Fine-Tuning

```python
train = prepare(train_dataset, Config.data.batch_size, shuffle=True, augment=True)
valid = prepare(val_dataset, Config.data.batch_size)
```

```python
nn.load_weights(Config.experiment.training_weights)
```

```python
if Config.training.fine_tune_epochs:
  backbone.trainable = True

  frozen_layer_ix = int((1-Config.training.fine_tune_layers) * len(backbone.layers))
  for ix, l in enumerate(backbone.layers):
    l.trainable = (ix > frozen_layer_ix and
                   (not isinstance(l, tf.keras.layers.BatchNormalization) or
                    not Config.training.freeze_batch_norm))

  nn.compile(
    optimizer=tf.optimizers.SGD(learning_rate=Config.training.fine_tune_lr,
                                momentum=0.9,
                                nesterov=True),
    loss=tf.losses.BinaryCrossentropy(from_logits=True),
    metrics=[
      F1Score(num_classes=len(CLASSES), from_logits=True, average='macro'),
      Precision(from_logits=True),
      Recall(from_logits=True),
      tf.keras.metrics.AUC(multi_label=True, from_logits=True),
    ])
```

```python
trained_epochs = 28
```

```python
if Config.training.fine_tune_epochs:
  os.makedirs(os.path.dirname(Config.experiment.fine_tune_weights), exist_ok=True)

  print(f'Fine tuning params:')
  print(f'  epochs:          {Config.training.fine_tune_epochs}')
  print(f'  learning rate:   {Config.training.fine_tune_lr}')
  print(f'  layers unfrozen: {frozen_layer_ix} to {len(backbone.layers)}')

  try:
    history = nn.fit(
      train,
      validation_data=valid,
      epochs=trained_epochs + Config.training.fine_tune_epochs,
      callbacks=[
        callbacks.TerminateOnNaN(),
        callbacks.ModelCheckpoint(Config.experiment.fine_tune_weights, save_best_only=True, save_weights_only=True, verbose=1),
        callbacks.TensorBoard(Config.experiment.fine_tune_logs),
        callbacks.EarlyStopping(patience=20, verbose=1),
        ReduceLRBacktrack(Config.experiment.fine_tune_weights, factor=0.5, patience=3, min_lr=0.001, verbose=1),
      ],
      initial_epoch=trained_epochs,
    );

  except KeyboardInterrupt: print('\ninterrupted')
  else: print('\ndone')
```

## Evaluation

```python
#@title

def labels_and_probs(nn, dataset):
  labels_ = []
  probs_ = []

  for images, labels in dataset:
    y = nn(images, training=False)
    y = tf.nn.sigmoid(y)

    labels_.append(labels)
    probs_.append(y)

    print('.', end='')

  return (tf.concat(labels_, axis=0),
          tf.concat(probs_, axis=0))

def evaluate(l, p):
  acc, tpr, fpr, tnr, fnr, auc, mcm = metrics_per_label(l, p)

  return pd.DataFrame({
    'accuracy': acc,
    'true positive r': tpr,
    'true negative r': tnr,
    'false positive r': fpr,
    'false negative r': fnr,
    'roc auc score': auc,
    'support': tf.cast(tf.reduce_sum(l, axis=0), tf.int32),
    'label': CLASSES
  })
```

```python
if Config.training.fine_tune_epochs:
  backbone.trainable = True

  frozen_layer_ix = int((1-Config.training.fine_tune_layers) * len(backbone.layers))

  for ix, l in enumerate(backbone.layers):
    l.trainable = (ix > frozen_layer_ix and
                   (not isinstance(l, tf.keras.layers.BatchNormalization) or
                    not Config.training.freeze_batch_norm))
```

```python
nn.load_weights('/home/ldavid/Workspace/models/coco/resnet101-ce-fine-tune/weights.h5')
```

```python
l, p = labels_and_probs(nn, valid)
```

```python
from sklearn import metrics as skmetrics

def metrics_per_label(gt, probs, threshold=0.5):
  p_pred = tf.cast(probs > threshold, probs.dtype).numpy()

  tru_ = tf.reduce_sum(gt, axis=0)
  neg_ = tf.reduce_sum(1- gt, axis=0)

  tpr = tf.reduce_sum(p_pred*gt, axis=0) / tru_
  fpr = tf.reduce_sum(p_pred*(1-gt), axis=0) / neg_
  tnr = tf.reduce_sum((1-p_pred)*(1-gt), axis=0) / neg_
  fnr = tf.reduce_sum((1-p_pred)*gt, axis=0) / tru_

  f2_score = skmetrics.fbeta_score(gt, p_pred, beta=2, average=None)
  precision, recall, f1_score, support = skmetrics.precision_recall_fscore_support(
    gt, p_pred, average=None)

  mcm = skmetrics.multilabel_confusion_matrix(gt, p_pred)

  skmetrics.accuracy_score(gt, p_pred)

  return pd.DataFrame({
    'true positive r': tpr,
    'true negative r': tnr,
    'false positive r': fpr,
    'false negative r': fnr,
    'precision': precision,
    'recall': recall,
    'auc_score': skmetrics.roc_auc_score(gt, probs, average=None),
    'f1_score': f1_score,
    'f2_score': f2_score,
    'support': support,
    'label': CLASSES
  }), mcm

test_report, test_mcm = metrics_per_label(l, p)
```

```python
with pd.option_context('display.max_rows', 100):
  display(test_report.round(4))
```

```python
co_occurrence = tf.transpose(l) @ l
occurrence = tf.reshape(np.diag(co_occurrence), (-1, 1))

co_occurrence_rate = tf.math.divide_no_nan(co_occurrence, occurrence)
```

```python
#@title Labels Occurrence Matrix

plt.figure(figsize=(16, 12))
sns.heatmap(
  co_occurrence_rate.numpy(),
  annot=False,
  fmt='.0%',
  xticklabels=CLASSES,
  yticklabels=CLASSES,
  cmap="RdPu",
  cbar=False
)
plt.tight_layout();
```

```python
d = tf.cast(p > 0.5, tf.float32)
co_occurrence = tf.transpose(d) @ d
occurrence = tf.reshape(np.diag(co_occurrence), (-1, 1))

co_occurrence_rate = tf.math.divide_no_nan(co_occurrence, occurrence)
```

```python
#@title Predictions Relation Matrix

plt.figure(figsize=(16, 12))
sns.heatmap(
  co_occurrence_rate.numpy(),
  annot=False,
  fmt='.0%',
  xticklabels=CLASSES,
  yticklabels=CLASSES,
  cmap="RdPu",
  cbar=False
)
plt.tight_layout();
```