# Introduction to Machine Learning

This notebook provides an introduction to machine learning and walks through how to develop workflows for training machine learning models.

This lesson is prepared by:
- [Kevin Chai](http://computation.curtin.edu.au/about/computational-specialists/health-sciences/)
- [Rebecca Lange](http://computation.curtin.edu.au/about/computational-specialists/humanities/)

from the [Curtin Institute for Computation](http://computation.curtin.edu.au) at Curtin University in Perth, Australia.

Some of the materials in this notebook have been referenced and adapted from:
- [Randal Olsen's Data Science Notebook](https://github.com/rhiever/Data-Analysis-and-Machine-Learning-Projects/tree/master/example-data-science-notebook)
- [Sebastian Raschka's Python Machine Learning Notebooks](https://github.com/rasbt/python-machine-learning-book)
- [Kevin Markham's Scikit Learn Notebooks](https://github.com/justmarkham/scikit-learn-videos)

Make sure to open this notebook in the root directory of the code repository.

This work is made available under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).

## Table of contents

1. [Introduction](#1.-Introduction)
2. [Problem definition](#2.-Problem definition)
3. [Data cleaning](#3.-Data-cleaning)
4. [Exploratory analysis](#4.-Exploratory-analysis)
5. [Data preparation](#5.-Data-preparation)
6. [Classification](#6.-Classification)
7. [Regression](#7.-Regression)
8. [Clustering](#8.-Clustering)
9. [Dimensionality reduction](#9.-Dimensionality-reduction)
10. [Reproducibility](#10.-Reproducibility)
11. [Conclusions](#11.-Conclusions)
12. [Further reading](#12.-Further-reading)

## 1. Introduction

### Required libraries

[[ go back to the top ]](#Table-of-contents)

This notebook uses several Python packages that come standard with the [Anaconda Python distribution](http://continuum.io/downloads). The primary libraries that we'll be using are:

* **NumPy**: a fast numerical array structure and helper functions.
* **pandas**: a DataFrame structure to store data in memory and work with it easily and efficiently.
* **scikit-learn**: a machine learning package.
* **matplotlib**: a basic plotting library; most other plotting libraries are built on top of it.
* **seaborn**: a advanced statistical plotting library.

To make sure you have all of the packages you need, install them with `conda`:

    conda install numpy pandas scikit-learn matplotlib seaborn

`conda` may ask you to update some of the packages if you don't have the most recent version. Allow it to do so.

Alternatively, if you can install the packages with [pip](https://pip.pypa.io/en/stable/installing/) (a Python package manager):

    pip install numpy pandas scikit-learn matplotlib seaborn

### Machine learning

[[ go back to the top ]](#Table-of-contents)

Machine learning refers to algorithms that learn from examples and experiences (data). These algorithms often perform better than explicitly hard coded rules for complex tasks. i.e. where it is difficult to describe precise rules.

We will explore two types of machine learning algorithms in this notebook:

**1. Supervised Learning**
* make predictions using data
* There is an outcome we are trying to predict
* Example: Is an e-mail spam or ham?

Figure: Classification example

The workflow for developing and deploying a supervised machine learning model is shown below.

Figure: Supervised learning workflow

**2. Unsupervised Learning**
* Extract structure from data
* There is no "right answer"
* Example: Segment grocery store shoppers into clusters that exhibit similar behaviours

Figure: Clustering example

The unsupervised learning workflow is illustrated in  the figure below. Observe that no labels are used for training.

## 2. Problem definition

### Domain

[[ go back to the top ]](#Table-of-contents)

We have been tasked to develop a machine learning model to classify galaxies by their morphology (appearance) from a dataset containing measurements such as:

- redshift
- magnitude in _ugriz_ bands
- exponential scale radius and ellipticity
- de Vaucouleurs scale radius and ellipticity
- stellar mass
- ...
- etc.

Galaxies in the dataset have labels taken from [Galaxy Zoo](https://www.galaxyzoo.org/) DR1 - Table 2. Galaxy Zoo is described in Lintott et al. 2008, MNRAS, 389, 1179 and the data release is described in Lintott et al. 2011, 410, 166.

We use the final debiased labels to categorise a galaxy as:

- spiral
- elliptical

Figure: A spiral galaxy (left) and elliptical galaxy (right)

Our goal is to train a model that can accurately classify galaxies. We want our model to generalise well. i.e. it can correctly classify unseen galaxies (i.e. galaxies not in our training dataset).

### Definition

[[ go back to the top ]](#Table-of-contents)

The first step of developing a machine learning model is to clearly understand and define the problem we want to solve. Here are some useful questions to ask.

#### Does the dataset contain labels?

Yes, the dataset contains galaxy morphology labels (spiral and elliptical). Therefore, we can use supervised learning methods to utilise the labelled data.

#### What is the type of problem?

We want to classify galaxies by their morphology so this is a classification problem.

#### What metric can be used to evaluate the model?

Since we are performing classification, we can use a classification metric such as [accuracy](https://en.wikipedia.org/wiki/Accuracy_and_precision) to evaluate and quantify the performance of our model. Accuracy is the percentage of correctly classified galaxies. As a challenge, we have been asked to develop a model that achieves ≥ 70% accuracy. Is this feasible? How can we find out?

#### Can the question actually be answered with the available data?

Yes, the dataset contains labels of the galaxy types we want to classify. i.e. `{spiral, elliptical}`. If we were asked to develop a model to detect other types of galaxies then we would need to collect data and labels for these other types.

#### Are there problem and application requirements we need to consider?

How will the model be used after it has been developed? Are there requirements for the model to be used on specific devices? e.g. personal computers, smart phones, embedded systems and/or servers. For the purpose of this lesson, we can assume that it is fine to train and run the model on our personal computers. However, this may not always be the case and it is something to consider if we are required to deploy our models to systems with limited compute and memory resources (e.g. embedded systems). In these instances, we may be restricted to build small, less complex and fast models at the cost of reduced performance (e.g. accuracy).

**Thinking about and documenting the problem we're working on is an important step to performing effective machine learning (and data analysis in general) that sometimes gets overlooked.**

## 3. Data cleaning

[[ go back to the top ]](#Table-of-contents)

#### Dataset

For this lesson, we will be using data from [Galaxy Zoo DR1](https://www.galaxyzoo.org/) and the Sloan Digital Sky Survey (SDSS) ([using the DR9 SQL search](http://skyserver.sdss.org/dr9/en/tools/search/sql.asp)).

The data dictionary for this dataset is presented in Table 1. This dataset is limited to the first 5,000 Galaxy Zoo classified galaxies which have spectra in the SDSS database. The debiased fraction of the votes in elliptical and spiral categories is given, along with columns identifying systems classified as spiral, elliptical or uncertain.

Table 1: Data dictionary

| Column           | Description                                                            |
|:-----------------|:-----------------------------------------------------------------------|
| id               | unique SDSS ID composed of [skyVersion, rerun, run, camcol, field, obj]|
| ra               | right ascension  (HMS)                                                 |
| dec              | declination (DMS)                                                      |
| redshift         | redshift                                                               |
| mag_u            | magnitude _u_ band                                                     |
| mag_g            | magnitude _g_ band                                                     |
| mag_r            | magnitude _r_ band                                                     |
| mag_i            | magnitude _i_ band                                                     |
| mag_z            | magnitude _z_ band                                                     |
| deVRad_r         | de Vaucouleurs scale radius fit in _r_ band                            |
| deVAB_r          | ellipticity from de Vaucouleurs fit in _r_ band                        |
| expRad_r         | exponential scale radius fit in _r_ band                               |
| expAB_r          | ellipticity from exponential fit in _r_ band                           |
| stellar_mass     | log galaxy mass (in units of solar mass)                               |
| votes            | number of Galaxy Zoo annotators                                        |
| p_el_debiased    | debiased labelling probability the galaxy is elliptical                |
| p_cs_debiased    | debiased labelling probability the galaxy is spiral                    |
| spiral           | label = spiral galaxy {0=False, 1=True}                                |
| elliptical       | label = elliptical galaxy {0=False, 1=True}                            |
| uncertain        | label = uncertain {0=False, 1=True}                                    |

#### Understand and clean the data

The next step is to explore and familiarise ourselves with the data. Datasets can have errors, and it's important to identify and resolve these errors before rushing in to develop our models. Errors in the dataset that aren't resolved will propogate through the machine learning pipeline.

Generally, we're looking to answer the following questions:

* Is there anything wrong with the data?
* Are there any quirks with the data?
* Do I need to clean or remove some of the data?

One of the best options for working with tabular data in Python is to use the
[Python Data Analysis Library](http://pandas.pydata.org/) (a.k.a. Pandas). The
Pandas library provides data structures, produces high quality plots with
[matplotlib](http://matplotlib.org/) and integrates nicely with other libraries
that use [NumPy](http://www.numpy.org/) arrays.
If you want to learn more about pandas you can find an introductory notebook from a session previously run by [ADACS](https://github.com/ADACS-Australia/Face-to-Face).

Let's start by loading `pandas` and checking our version, we will need v0.19 or higher.

```python
# Import pandas
import pandas as pd

#check your version, we need v0.19 or higher
pd.__version__
```

Next we will read our data into a DataFrame and inspect the first few rows.

#### What is a DataFrame?
A DataFrame is a 2-dimensional data structure that can store data of different
types (including characters, integers, floating point values, factors and more)
in columns. It is similar to a spreadsheet or an SQL table or the `data.frame` in
R. The DataFrame has a 0-based index as is common in Python.

```python
# Read the data file
data = pd.read_csv('data/galaxies.csv')

# Display the first few rows using the .head() method

```

### Missing Data Values - NaN

Dealing with missing data values is always a challenge, think about:
* Why values are missing - was it because of a data entry error? Or are they data that someone was unable to collect?
* How are the missing values represented? Are they empty cells in a spreadsheet, NaN or NA, or otherwise defined?
* How should you treat the NaN? Should the value be 0 or the mean of all measurements, should they be ignored?

Note: empty cells in a spreadhseet are automatically replaced with NaN by pandas when the data is read into a DataFrame.

 So, one of the first things we should do is to look for is missing data. We can use `pandas` to  display summary statistics for each (numerical) column as well as inspect rows with missing data.

```python
# The .describe() method returns summary statistics for each (numerical) column

```

```python
# Get records containing any missing values using the .isnull() method

```

Here we see a number of rows that contain missing values (`NaN`). We also saw that a few of the columns have minima of -9999 a default value often used in astronomy (and remote sensing) to flag missing values, either because the data was not collected or there was an error in calculating the final value.

There are many [methods for handling and imputing missing data](http://machinelearningmastery.com/handle-missing-data-python/). However, from our results we see that the galaxies with NaN values have been labelled as `uncertain` so we will simply remove these records.

```python
# Drop the rows containing any missing values

```

Let's double check to make sure the records have been removed.

```python
# Should return 0 rows

```

As for the data with -9999 values, from our domain knowledge we know that this is a flag to signify NaN. So we can treat these as missing values / errors and remove them from our dataset as well.
Also, having galaxies with masses or magnitudes less than 0 does not make sense and we should remove these records, too.

#### Slicing Subsets of Rows and Columns in Pandas

We can select specific ranges of our data in both the row and column directions
using either label or integer-based indexing.

- `iloc`: indexing via *integers*
- `loc`: indexing via *labels* (which can be integers)

To select a subset of rows AND columns from our DataFrame, we can use the `iloc`
method. For example, we can select ra, dec and redshift (columns 2, 3 and 4 if we
start counting at 1), like this:

```python
data.iloc[0:3, 1:4]
```

```python

```

Similarly, if we wanted to select the same subset using the labels we can use .loc like this:

```python
data.loc[[0,1,2],["ra","dec","redshift"]]
```

```python

```

Now, let's look at and remove our data with values <0.

Be careful, some data columns can be smaller than zero and still be valid measurements.

Make sure to check any metadata given with your data to figure out what is a bad measurements.

```python
# Get records with ≤ 0 values
# Exclude the first  4 columns (id, ra, dec, redshift) and last 5 columns (galaxy labels and probabilities)

```

```python
# Only retain records with ≥ 0 values in our dataset

# Let's check the summary stats again
data.describe()
```

It is important to construct a good training dataset for developing a robust model. In machine learning, we also refer to the training dataset as the ground truth. Any uncertainities or errors in the ground truth can confuse and reduce the performance of our models.

In order to build a good training dataset, we only want to keep records where we are confident there is agreement between the Galaxy Zoo annotators in labelling galaxies. From the dataset, it appears we can achieved this by:

- Keeping records where the uncertain label column is equal to False (0)

Let's try this method and perform some sanity checking to see if the resulting data subset has debiased probabilities for ellipital and spiral labels ≥ 0.5.

```python
# 1. Filter records using the uncertain column

f1=

print('# records after removing uncertain rows: %d' % (len(f1)))

# 2. Filter records using the debiased probabilities ≥ 0.5

f2=

print('# records with ≥ 0.5 debiased probability: %d' % (len(f2)))
```

There appears to be a discrepency of 5 records between the two data subsets. Let's investigate further by inspecting these records.

```python
# Display filter #1 rows that don't appear in the filter #2 dataset

```

These records have been labelled as spiral galaxies but the debiased probabilities (confidence) for these labels are below the 0.5 confidence threshold. i.e. `p_cs_debiased < 0.5`. Additionally, for the last 2 records, we see that debiased probabilities for labelling the galaxy as elliptical is actually larger than the spiral probabilities which suggests these might be due to labelling errors.

It appears we can't rely on filtering the dataset on the `uncertain` column alone so we will apply both filters to clean the dataset. Sanity checking your dataset is a step that is regularly overlooked but dedicating time to explore and resolve these errors can (sometimes dramatically) improve the performance of our machine learning models.

We will make a copy of the filtered dataset in the variable `df` (abbreviation for DataFrame) for our clean dataset.

```python
# use .copy to create and independent copy of the dataframe

```

Let's clean and prepare our dataset by:

- creating a class column to represent the galaxy type label `{0=elliptical, 1=spiral}`
- removing unwanted columns that won't be used in our analysis

```python
# Create the class column - do this by renaming the 'spiral' column to 'class' with .rename()

# define unwanted columns
unwanted_columns = []

# remove unwanted columns using .frop

# Inspect the first few rows of our prepared dataset
df.head()
```

Next, it's always a good idea to look to have a closer look at our data — especially for outliers. Let's start by printing out some summary statistics about the data set.

```python
# Display dataset summary statistics
df.describe()
```

The summary table provides some useful information:

- there are 1,797 galaxies in our dataset
- the mean of the `class` column indicates that ~75.3% of the dataset are spiral galaxies
- min values for the radius columns range from 0.31 to 0.38
- min value for the `stellar_mass` = 8.096
- min value for the `mag` ranging from 15.59 to 18.5

Our cleaned dataset contains 1,797 galaxies and 75.3% of the records are spiral galaxies.

Tables like this are useful when we know that our data should fall in a particular range. e.g. It did not make sense to have zero or negative values for some of our columns. However, it is usually better to visualize the data in some way. Visualization makes outliers and errors immediately stand out, whereas they might go unnoticed in a large table of numbers.

Since we know we're going to be plotting in this section, let's set up the notebook so we can plot inside of it.

```python
# Command to show plots inside of the notebook
%matplotlib inline

import matplotlib.pyplot as plt
import seaborn as sb
```

Next, let's create a **scatterplot matrix**. Scatterplot matrices plot the distribution of each column along the diagonal and a scatterplot matrix for the combination of each variable. They make for an efficient tool to look for errors in our data.

Since we have quite a few columns, let's generate two scatterplots for data containing:

1. magnitudes
2. measurements

```python
# Magnitudes scatterplot
magnitudes = ['mag_u', 'mag_g', 'mag_r', 'mag_i', 'mag_z']
selected_columns = magnitudes + ['class']
sb.pairplot(df[selected_columns], hue='class', plot_kws={'alpha': 0.3})
```

From the magnitude scatterplot matrix, we see a potential outlier: the `mag_u` value of a `elliptical` galaxy falls outside its normal range (a value of ~24) and there seems to be an outlier for the spirals as well with values >18 for all(?) magnitudes.

```python
# Inspect possible outliers
df[(df.loc[:, df.columns.str.startswith('mag')]>18).all(axis=1) | (df['mag_u'] > 24)]
```

There is nothing obvious that leads us to believe the records are erroneous. Let's keep them in our dataset.

Next we have a look at the distribution of th eother measurements in our dataset:

```python
# Measurements scatterplot
measurements = ['redshift', 'stellar_mass', 'deVRad_r', 'deVAB_r', 'expRad_r', 'expAB_r']
selected_columns = measurements + ['class']
sb.pairplot(df[selected_columns], hue='class', plot_kws={'alpha': 0.3})
```

From the scatterplot matrix, we see potential outliers:

- the `stellar_mass` value of a `spiral` galaxy falls outside its normal range (a value of ~8).
- two spiral galaxies with `expRad_r` ≥ 17

Let's inspect the outlier records to determine whether if we should keep them in our training dataset.

```python
# Inspect possible outliers
df[(df['stellar_mass'] < 8.1) | (df['expRad_r'] >= 17) ]
```

Again, there is nothing obvious that leads us to believe the records are erroneous, so we'll keep them in our dataset.

Fixing outliers can be difficult. It's often unclear whether the outlier was caused by measurement error, recording the data in improper units, or if the outlier is a real anomaly. For that reason, we should be careful when working with outliers. If we decide to exclude any data, we should document what data was excluded and provide reasons for excluding that data. i.e. The data didn't fit my hypothesis will not stand peer review.

After all this hard work, we don't want to repeat the data cleaning process every time we work with the dataset. Let's save the cleaned dataset as a separate file and work directly with that data file from now on.

```python
# Save the clean dataset to a file
df.to_csv('data/galaxies-clean.csv', index=False)
```

## 4. Exploratory analysis

[[ go back to the top ]](#Table-of-contents)

Exploratory analysis is the step where we start delving deeper into the data set beyond the outliers and errors. We'll be looking to answer questions such as:

- How is my data distributed?
- Are there any correlations in my data?
- Are there any confounding factors that explain these correlations?

This is the stage where we plot the data in as many ways as possible. Create many charts, but don't bother making them pretty as these charts are for internal use.

For completeness, let's generate a scatterplot for all features in one figure.

```python
# Plot scatterplot matrix with all features
selected_columns = measurements + magnitudes + ['class']
sb.pairplot(df[selected_columns], hue='class', plot_kws={'alpha': 0.3})
```

We can make a number of observations from this plot:

1. `reshift`, `stellar_mass`, `mag_u` and `mag_g` follow a normal distribution. This is something to keep in mind if we use modelling methods that assume the data is normally distributed.

2. There is a strong positive correlation between the `expAB_r` and `deVAB_r` which indicates we might not need to use both of these features in our model.

3. There is a strong positive correlation between the `mag_r`, `mag_i` and `mag_z`.

4. There is a positive correlation between `mag_u` and `mag_g` with the other magnitude bands but not as strong as in observation #3. This suggests these features might provide added information not present in the `mag_r`, `mag_i` and `mag_z` bands.

5. There are no clear pair-wise combinations of features that can easily seperate the two classes.

Distnguishing between `spiral` and `elliptical` galaxies might be difficult given how much these features interrelate.

We can also make **violin plots** of the data to compare the measurement distributions of the classes. Violin plots contain the same information as [box plots](https://en.wikipedia.org/wiki/Box_plot), but also scales the box according to the density of the data.

```python
# General violin plots of all features
selected_columns = measurements + magnitudes + ['class']
plt.figure(figsize=(13, 13))
for column_index, column in enumerate(selected_columns):
    if column == 'class':
        continue
    plt.subplot(3, 4, column_index + 1)
    sb.violinplot(x='class', y=column, data=df[selected_columns])
```

There are numerous errors and possible scenarios you may face while exploring and cleaning your data.

The general takeaways here should be:

- Handle missing data: replace it if you can or drop it
- Ensure your data is encoded properly
- Check if your data falls within the expected range and use domain knowledge whenever possible to define that expected range
- Avoid tidying your data manually because that is not easily reproducible
- Use code as a record of how you tidied your data
- Plot everything you can about the data at this stage of the analysis so you can visually confirm everything looks correct

## 5. Data preparation

[[ go back to the top ]](#Table-of-contents)

Cleaning and exploring the data is a important component to any machine learning project. If we had jumped straight in to modelling, we would have trained a model with errors in the dataset. Bad data leads to bad models.

Now it's a good time to introduce some machine learning terminology.

### Terminology

- Each row is an `example` (also known as (aka): observation, sample, instance, record)
- Each column is a `feature` (aka: predictor, attribute, dimension, independent variable)
- The value we are predicting is the `label` (aka: target, outcome, dependent variable)

In classification, the label is also referred to as the class or category.

### Notation

Let:
- `m` = the number of examples in our dataset
- `n` = the number of features in our dataset

The dataset can be represented by two variables as shown in Table 2:

1. A matrix `X` containing the examples and features of size `m x n`
2. A vector `y` containing the labels of size `m`

Table 2: Dataset notations

            X
            y

            feature 1 ($x_1$)
            feature 2 ($x_2$)
            feature 3 ($x_3$)
            label

            5
            12
            3
            0

            1
            3
            8
            1

            9
            3
            2
            1

            5
            8
            7
            0

            2
            7
            8
            0

This is the standard notation used in machine learning. Following this convention makes it easier for other machine learning practitioners to understand your scripts. This notation also allows us to concisely define supervised machine learning and classification described in the [Introduction](#1.-Introduction) section.

#### Supervised learning
Learn a function `f` that maps features to labels

$f(X) \rightarrow y$

\begin{align}
y &= \begin{bmatrix}
    y_1 \\
    \vdots \\
    y_i \\
    \vdots \\
    y_{m}
    \end{bmatrix}
\end{align}

With classification, $y_i = \{c_1, ..., c_k\},where\ k = number\ of\ classes$

We only have two classes in our dataset so $y_i \in \{0=elliptical, 1=spiral\}$

### Scikit Learn
We will be using the [scikit-learn](http://scikit-learn.org/) machine learning library for developing our models. As such, we need to prepare our dataset into a format that scikit-learn expects:

1. Features and labels are **separate objects**
2. Features and labels should be **numeric**
3. Features and labels should be **`numpy` arrays**
4. Features and labels should have **specific shapes**

**Note**: `pandas` is built on top of `numpy` so we can create the features matrix as a pandas `DataFrame` and labels vector as a pandas `Series`.

Let's prepare the dataset for scikit-learn.

```python
# Import the required libraries

# Set a random seed number to repoduce our results
seed = 19

# Load the clean dataset
# df =

# Selected columns for modelling
measurements = ['redshift', 'stellar_mass', 'deVRad_r', 'deVAB_r', 'expRad_r', 'expAB_r']
magnitudes = ['mag_u', 'mag_g', 'mag_r', 'mag_i', 'mag_z']
# features =

# features matrix
# X =

# labels vector
# y =
```

Let's verify our features matrix and labels vector

```python
# Features matrix
# expected output: pandas DataFrame, (number of rows, number of features)
print('X: %s, %s' % (type(X), X.shape))

# Labels vector
# expected output: pandas Series, (number of rows,)
print('y: %s, %s' % (type(y), y.shape))
```

That looks great. Now we can start modelling.

## 6. Classification

[[ go back to the top ]](#Table-of-contents)

### Splitting the data

It's time to make the next big step in our analysis: splitting the data into training and test sets.

- A **training set** is a random subset of the data that we use to train our models.
- A **test set** is a random subset of the data (mutually exclusive from the training set) that we use to test our models.

In machine learning we are always concerned that our models will **overfit** the data. i.e. The model learns the training set so well that it won't be able to handle examples it's never seen before. This is why it's important for us to build the model with the training set, but score it with a separate unseen testing set.

Once we split the data into a training and test set, we should treat the test set like it no longer exists. We cannot use any information from the testing set to build our model or else we're cheating. The training dataset can also be split again to create a validation set to tune our models. It is important that the validation set is separate from the holdout test dataset.

This process is illustrated in the figure below.

Figure: Holdout set method to split the dataset

Now let's split the dataset using the `train_test_split` function.

```python
# Import the train_test_split function

# Split the dataset into 80% training, 20% testing
# X_train, X_test, y_train, y_test =
```

With the dataset split, we can start fitting models to our data. We have heard from our colleagues that they have had success using decision tree classifiers in their projects so let's start with those.

Decision tree classifiers are incredibly simple in theory. In their simplest form, decision tree classifiers ask a series of Yes/No questions about the data — each time getting closer to finding out the class of each example — until they either classify the data set perfectly or simply can't differentiate a set of examples.

An example decision tree classifier for approving loan applications is shown in the following figure.

Figure: Decision tree example

Notice how the classifier asks yes/no questions about the data. e.g. whether the applicant owns a house so it can differentiate the records.

Decision tree classifiers are *scale-invariant*, i.e. the scale of the features does not affect their performance unlike many machine learning models. In other words, it doesn't matter if our features range from 0 to 1 or 0 to 1,000; decision tree classifiers will work with them just the same.

There are several [parameters](http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html) that we can tune for decision tree classifiers and different [metrics](https://en.wikipedia.org/wiki/Evaluation_of_binary_classifiers) we can use to evaluate its performance. For now let's use a basic decision tree and the accuracy performance metric.

\begin{align}
accuracy &= \dfrac{correct\ classifications}{total\ number\ of\ classifications}
\end{align}

Scikit learn provides a 4 step modelling pattern which makes it easy to switch in different models / algorithms for your dataset. This pattern is described in the code and comments below.

```python
# Step 1: Import the model

# Step 2: Instantiate the model
# decision_tree_classifier =

# Step 3: Fit the model on data (i.e. train the model)

# Step 4: Generate predictions / scores

```

That's great! Our model achieves 91% accuracy without much effort. We have already beaten the challenge to build a model with ≥ 70% accuracy.

However, there's a catch. Depending on how our training and testing set is sampled, our model can achieve anywhere from 87% to 96% accuracy.

```python
model_accuracies = []

# Split the dataset differently and fit a model on this split, 1,000 times
for i in range(1000):
    X2_train, X2_test, y2_train, y2_test = train_test_split(X, y, train_size=0.8, random_state=i)

    decision_tree_classifier = DecisionTreeClassifier(random_state=seed)
    decision_tree_classifier.fit(X2_train, y2_train)
    classifier_accuracy = decision_tree_classifier.score(X2_test, y2_test)
    model_accuracies.append(classifier_accuracy)

sb.distplot(model_accuracies)
```

This is a problem! The model performance varies a lot depending on the subset of the data it's trained on. This means the model is **overfitting**: the model learns to classify the training set so well that it doesn't generalize and perform well on data it hasn't seen before (i.e. the test set).

### Cross-validation

[[ go back to the top ]](#Table-of-contents)

Cross validation is a method of splitting the dataset to help address this problem. For this lesson, we will use the ***k*-fold cross-validation** method. This involves splitting the original data set into *k* subsets, use one of the subsets as the testing set and, the rest of the subsets are used as the training set. This process is repeated *k* times such that each subset is used as the testing set exactly once. This process is illustrated in Figure X.

Figure: k-fold cross-validation

10-fold cross-validation is the most common choice so let's use that here. Performing 10-fold cross-validation on our data set looks something like this on a subset of 100 examples in our dataset.

```python
def plot_cv(cv, n_samples):
    masks = []
    for train, test in cv:
        mask = np.zeros(n_samples, dtype=bool)
        mask[test] = 1
        masks.append(mask)

    plt.figure(figsize=(15, 15))
    plt.imshow(masks, interpolation='none')
    plt.ylabel('Fold')
    plt.xlabel('Row #')

# Import the function
from sklearn.cross_validation import StratifiedKFold

# Use a subset of 100 examples
# subset =

# Plot the data split
plot_cv(StratifiedKFold(subset, n_folds=10), len(subset))
```

Each square in the plot represents an example in our dataset.

You'll notice that we used the `StratifiedKFold` function in the code above. Stratified means we keep the class percentage the same across all of the folds (~75% spiral), which is important for maintaining a representative subset of our dataset. i.e. we don't want to end up having 100% spiral galaxies in one of the folds.

We can fit a decision tree classifier using 10-fold cross-validation with the `cross_val_score` function.

```python
# Import the cross_val_score function

# Create a decision tree classifier
# decision_tree_classifier =

# cross_val_score returns a list of the scores, which we can visualize
# to get a reasonable estimate of our classifier's performance
# cv_scores =

# Plot the results
sb.distplot(cv_scores)
plt.title('Average score: {}'.format(np.mean(cv_scores)))
```

That looks much better. We have a more consistent score (less variance) of our model's classification accuracy.

### Learning curves

[[ go back to the top ]](#Table-of-contents)

Learning curves allow us to evaluate the performance of our models as they are provided more training examples. i.e. They show us how or if our model learns. Plotting these curves also allows us to diagnose our models in order to gain insights on how we can improve them. More specifically, learning curves allows us to identify:

- underfitting (high bias): model performs poorly on training and validation
- overfitting (high variance): model performance is good on training but much poorer on validation

Ideally, we want to develop a model that achieves similar performance on both the training and validation sets (good bias and variance trade off) as shown in the figure below.

Figure: Learning curves

Let's plot the learning curve for our decision tree classifier using the `learning_curve` function.

```python
# Import the learning_curve function

# Create a decision tree classifier
# decision_tree_classifier =

# Generate the learning curve scores
# train_sizes, train_scores, val_scores =

# Calculate the result averages and standard deviation
# train_mean =
# train_std =
# val_mean =
# val_std =

# Plot the training learning curve
plt.plot(train_sizes, train_mean,
         color='green', marker='o',
         markersize=5, label='training accuracy')

plt.fill_between(train_sizes,
                 train_mean + train_std,
                 train_mean - train_std,
                 alpha=0.15, color='green')

# Plot the validation learning curve
plt.plot(train_sizes, val_mean,
         color='blue', linestyle='--',
         marker='s', markersize=5,
         label='validation accuracy')

plt.fill_between(train_sizes,
                 val_mean + val_std,
                 val_mean - val_std,
                 alpha=0.15, color='blue')

# Plot settings
plt.xlabel('Number of training samples')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8, 1])
plt.tight_layout()
# plt.savefig('images/learning_curve.png', dpi=300)
plt.show()
```

From the learning curve we can see that the decision tree classifier overfits the training dataset. The training accuracy remains constant at 100% (fits the data perfectly) while the average validation scores steadily increase from 87% to 93% as the number of training samples increases.

Now that we know our decision tree classifier overfits the dataset, let's do something about it!

### Parameter tuning

[[ go back to the top ]](#Table-of-contents)

Every machine learning model comes with a variety of parameters to tune, and these parameters can be vitally important to the performance of our classifier. For example, using the default parameters for decision trees has resulted in our classifier overfitting the training dataset.

As an extreme example, let's limit the depth of our decision tree classifier to a `max_depth = 1`:

```python
# Set the max_depth of the decision tree to 1
# decision_tree_classifier =

# Calculate the cross validation score
# cv_scores =

# Plot the results
sb.distplot(cv_scores, kde=False)
plt.title('Average score: {}'.format(np.mean(cv_scores)))
plt.show()
```

The average classification accuracy has dropped to 86%.

We need to use a systematic method to discover the best parameters for our model and dataset.

#### Grid search

A common method for model parameter (hyperparameter) tuning is grid search. The idea behind grid search is to explore a range of parameters and find the best-performing parameter combination. Focus your search on the best range of parameters, then repeat this process several times until the best parameters are discovered.

Let's tune our decision tree classifier using the `GridSearchCV` function. We'll stick to only two parameters for now (`max_depth` and `max_features`), but it's possible to simultaneously explore dozens of parameters if we want.

```python
# Import the GridSearchCV function

# Create a decision tree classifier
# decision_tree_classifier =

# Parameter values to explore
parameter_grid = {'max_depth': [1, 2, 3, 4, 5, 6],
                  'max_features': [7, 8, 9, 10, 11]}

# Create a cross validation object using StratifiedKFold
# cross_validation =

# Instantiate the grid search using GridSearchCV
# grid_search =

# Fit the decision tree using grid search

# Display the results
print('Best score: {}'.format(grid_search.best_score_))
print('Best parameters: {}'.format(grid_search.best_params_))
```

We can also visualize the grid search to see how the parameters interact.

```python
grid_visualization = []

for grid_pair in grid_search.grid_scores_:
    grid_visualization.append(grid_pair.mean_validation_score)

grid_visualization = np.array(grid_visualization)
grid_visualization.shape = (6, 5)
sb.heatmap(grid_visualization, cmap='Blues')
plt.xticks(np.arange(5) + 0.5, grid_search.param_grid['max_features'])
plt.yticks(np.arange(6) + 0.5, grid_search.param_grid['max_depth'][::-1])
plt.xlabel('max_features')
plt.ylabel('max_depth')
```

Now we have a better sense of the parameter space: a `max_depth > 4` is needed for the decision tree perform reasonably well. Tuning the `max_features` doesn't seem to make as much difference as we achieved the best performance with using `9` out of the 11 features.

An alternative way to visualise the performance of your parameter tuning results is to plot [validation curves](http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.validation_curve.html).

Let's go ahead and use a broader grid search to find the best settings for more parameters.

```python
# Create a decision tree classifier

# Hyperparameter values to explore
parameter_grid = {'criterion': ['gini', 'entropy'],
                  'splitter': ['best', 'random'],
                  'max_depth': [4, 5, 6],
                  'max_features': [7, 8, 9, 10, 11],
                  'min_samples_split': [2, 4, 6, 8]}

# Create a cross validation object using StratifiedKFold
# cross_validation =

# Instantiate the grid search using GridSearchCV
# grid_search =

# Perform the grid search

# Display the best model scores and parameters
print('Best score: {}'.format(grid_search.best_score_))
print('Best parameters: {}'.format(grid_search.best_params_))
```

Save the best classifier from the grid search.

```python
decision_tree_classifier = grid_search.best_estimator_
# Examine the model
decision_tree_classifier
```

Now plot the learning curve for the best classifier for evaluation.

```python
# Generate the learning curve scores
# train_sizes, train_scores, val_scores =

# Calculate the result averages and standard deviation
# train_mean =
# train_std =
# val_mean =
# val_std =

# Plot the training learning curve
plt.plot(train_sizes, train_mean,
         color='green', marker='o',
         markersize=5, label='training accuracy')

plt.fill_between(train_sizes,
                 train_mean + train_std,
                 train_mean - train_std,
                 alpha=0.15, color='green')

# Plot the validation learning curve
plt.plot(train_sizes, val_mean,
         color='blue', linestyle='--',
         marker='s', markersize=5,
         label='validation accuracy')

plt.fill_between(train_sizes,
                 val_mean + val_std,
                 val_mean - val_std,
                 alpha=0.15, color='blue')

# Plot settings
plt.xlabel('Number of training samples')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8, 1])
plt.tight_layout()
# plt.savefig('images/learning_curve.png', dpi=300)
plt.show()
```

This learning curve looks better. The gap between the training and validation accuracy has reduced with training score of 95% and a validation score of 93% which suggests the model is no longer overfitting.

### Reporting
[[ go back to the top ]](#Table-of-contents)

Score the classifier on the test dataset.

```python
# Get the best estimator from the grid search
# decision_tree_classifier =

# Score the estimator on the test dataset

```

Our best classifier achieves a test accuracy score of 92.7% which is almost on par with our validation score of 93%. This gives us confidence that our experiment design and trained model is providing realistic results.

We can generate a confusion matrix using the `confusion_matrix` function to get more detailed classification results.

```python
# Import the confusion_matrix function

# Generate the predictions
# y_pred =

# Calculate the confusion matrix
# cm =

# Create a DataFrame for plotting
labels = ['elliptical', 'spiral']
df_cm = pd.DataFrame(cm, columns=labels, index=labels)

# Plot the confusion matrix
sb.heatmap(df_cm, annot=True, fmt='g', cmap='Blues', annot_kws={"size": 15})
plt.title('Confusion Matrix', fontsize=20)
plt.xlabel('Actual', fontsize=15)
plt.xticks(fontsize=13)
plt.ylabel('Predicted', fontsize=15)
plt.yticks(fontsize=13)
plt.show()
```

The confusion matrix provides more detailed information of where the classifier made mistakes (i.e. where it was confused). Here we can see that the model predicted 12 galaxies as spiral that were actually elliptical and 14 elliptical galaxies that were actually spiral. Overall, it looks like the decision tree classifier has performed quite well on test dataset.

We can also visualize the decision tree with [GraphViz](http://www.graphviz.org/) to see how it's making the classifications:

```python
import sklearn.tree as tree
from sklearn.externals.six import StringIO

with open('images/galaxy_tree.dot', 'w') as out_file:
    out_file = tree.export_graphviz(decision_tree_classifier, out_file=out_file)
```

```python
# The exported decision tree graph displays features by their index. e.g. X[0], ... X[10]
# This snippet will replace the index names with the actual feature names
replacements = {}
for index, feature in enumerate(features):
    key = 'X[%d]' % (index)
    replacements[key] = feature

with open('images/galaxy_tree.dot') as infile, open('images/galaxy_tree_names.dot', 'w') as outfile:
    for line in infile:
        for src, target in replacements.items():
            line = line.replace(src, target)
        outfile.write(line)
```

If you have [GraphViz](http://www.graphviz.org/) installed then you can generate a image of the decision tree with the following command:

    dot -Tpng images/galaxy_tree_names.dot -o images/galaxy_tree_names.png

Alright! We finally have our demo classifier. Let's create some more visuals of its performance so we have something to put in our report.

```python
dt_scores = cross_val_score(decision_tree_classifier, X, y, cv=10)
sb.boxplot(dt_scores)
```

This plot is not very useful with just one model so let's train another.

A common problem with decision trees is that they're prone to overfitting. They can add complex rules to the point that they classify the training set near-perfectly (as we saw early with our learning curves), but fail to generalise well to data they have not seen before.

**Random Forest classifiers** work around this limitation by creating many decision trees (i.e. a forest), each trained on random subsets of training samples and features. These decision trees are then combined to make a more accurate classification. Let's see if a random forest classifier works better for our datatset.

The great part about scikit-learn is that the modelling pattern of training, testing, parameter tuning, etc. process is the same for all models. We only need to plug in the new classifier.

```python
# Step 1: Import the class

# Step 2: Instantiate the estimator
# random_forest_classifier =

# Hyperparameter values to explore
parameter_grid = {'n_estimators': [5, 10, 25, 50],
                  'criterion': ['gini', 'entropy'],
                  'max_features': [1, 2, 3, 4],
                  'warm_start': [True, False]}

# Create a cross validation object using StratifiedKFold
# cross_validation =

# Instantiate the grid search using GridSearchCV
grid_search =

# Step 3 & 4: Fit the estimator on data (i.e. train the model) and generate predictions

# Display the best model scores and parameters
print('Best score: {}'.format(grid_search.best_score_))
print('Best parameters: {}'.format(grid_search.best_params_))

grid_search.best_estimator_
```

The random forest classifier achieved a 95% cross validation accuracy score. Find out how well it performs on the test dataset.

```python
# Get the best estimator from the grid search
# random_forest_classifier =

# Score the estimator on the test dataset

```

Random forests performs better than the decision tree classifier on the test dataset:

- decision trees ~ 92%
- random forests ~ 96%

We can also plot and compare their cross validation performance.

```python
rf_df = pd.DataFrame({'accuracy': cross_val_score(random_forest_classifier, X_train, y_train, cv=10),
                       'classifier': ['Random Forest'] * 10})
dt_df = pd.DataFrame({'accuracy': cross_val_score(decision_tree_classifier, X_train, y_train, cv=10),
                      'classifier': ['Decision Tree'] * 10})
both_df = rf_df.append(dt_df)

sb.boxplot(x='classifier', y='accuracy', data=both_df)
```

Random forests also performs better on average than decisions trees with cross validation. Random forest classifiers can perform particularly well when there's hundreds of possible features to model (we only have 11 features in dataset). In other words, there was not much room for improvement for random forests over decision trees with this dataset.

To report more detailed information of our model, let's see what features were considered the most important for both classifiers.

```python
# Decision tree features
sorted(zip(decision_tree_classifier.feature_importances_, features), reverse=True)
```

```python
# Random forests features
sorted(zip(random_forest_classifier.feature_importances_, features), reverse=True)
```

Both classifiers rank the importance of the features in a similar order with the exception of the magnitudes. The stellar mass, ellipticity, radius and redshift of the galaxies were the most important features.

If you want to explore this further you could experiment with removing the `mag_u`, `mag_i`, `mag_g` features as they scored a feature importance of `0` for the decision tree classifier to see how it affects classification performance. Additionally, you can experiment with various [feature selection methods](http://scikit-learn.org/stable/modules/feature_selection.html) for systemtically selecting a reduced feature set.

#### Other algorithms

There are many [classification algorithms](http://scikit-learn.org/stable/supervised_learning.html#supervised-learning) we can experiment with on our dataset. Try fitting a [Logistic Regression](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) and [Multi Layer Perceptron (MLP - Neural Network)](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html) model on the dataset. It is up to you whether you want to perform parameter tuning on these models.

Generate boxplots so we can compare the performance with the decision tree and random forest models.

```python
# Import the required models

# Initialise the models
# logreg_classifier =
# mlp_classifier =

# Calculate the 10-fold cross validation scores
# logreg_cv =
# mlp_cv =

# Create a DataFrame of the results
logreg_df = pd.DataFrame({'accuracy': logreg_cv, 'classifier': ['Logistic Regression'] * 10})
mlp_df = pd.DataFrame({'accuracy': mlp_cv, 'classifier': ['MLP'] * 10})

# Merge all classifier results
classifiers_df = rf_df.append([dt_df, logreg_df, mlp_df])

# Generate a box plot comparing the different algorithms
sb.boxplot(x='classifier', y='accuracy', data=classifiers_df)
```

### Challenge

Pick your favourite model and use it to predict the morphology of galaxies that have been labelled as  uncertain in the dataset.

- How many spiral vs. elliptical galaxies does the model predict?
- How do the model predictions compare with the debiased labelled probabilities?
- How could you use this model and results in practice?

```python

```

## 7. Regression

[[ go back to the top ]](#Table-of-contents)

Your colleague has heard about your recent success in applying machine learning for galaxy classification. They mention they have a dataset containing _ugriz_ photomerty of galaxies but no redshift measurements. They would like you to develop a machine learning model to estimate the redshift values.

This is a regression problem. Regression is a supervised learning approach but instead of predicting the category of an example, we are predicting a continuous value. i.e. $y_i \in\ \mathbb{R}$. For example, the predicted redshift of galaxy `i` is 0.2312.

Developing a regression model follows the same process as building a classification model with the exception of using a different metric for evaluating the model performance. i.e. we can't use classification accuracy for evaluating our regression model.

###  Prepare the dataset

Load and prepare the dataset for using the scikit-learn library.

```python
import numpy as np
import pandas as pd

# Set a random seed number to reproduce our results
seed = 11

# Clean dataset file
clean_dataset = 'data/galaxies-clean.csv'

# 1. Load the dataset using pandas read_csv function
# df =

# 2. Select the columns of interest for modelling
# features =

# 3. Create the features matrix as X
# X =

# 4. Create the labels vector as y
# y =
```

###  Split the dataset

Create a training and test dataset using the `train_test_split` function.

```python
# Import the function

# Split the dataset into X_train, X_test, y_train, y_test
# Use a training dataset size of 80%
# X_train, X_test, y_train, y_test =
```

### Train models

We will fit the regression models without any parameter tuning using the following three models:

- [Linear Regression](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)
- [RandomForestRegressor](http://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html)
- [MLPRegressor](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPRegressor.html)

We will measure model performance using the root mean squared error (RMSE) metric. This can be calculated by using the NumPy function `np.sqrt` and the scikit-learn `mean_squared_error` function.

```python
# Step 1: Import the classes

# Step 2: Instantiate the estimators
# lr =
# rf =
# mlp =

# Step 3: Fit the estimators on data (i.e. train the models)

# Step 4: Generate predictions
# y_pred_m1 =
# y_pred_m2 =
# y_pred_m3 =

# Calculate the Root Mean Squared Error (RMSE)
# np.sqrt(mean_squared_error(...))
# m1_score =
# m2_score =
# m3_score =

# Display the model scores
print('Linear regression: %.3f' % (m1_score))
print('Random forest regressor: %.3f' % (m2_score))
print('Multi layer perceptron: %.3f' % (m3_score))
```

The linear regression model is off by an average of 0.022 redshift while the random forest regression has an RMSE of 0.026. The multi layer perceptron (without any parameter tuning) achieves a poorer RMSE of 0.079.

Let's examine the correlation between the predicted and actual redshift values for both models.

```python
# calculate the correlation between the labels and predictions
# m1_corr =
# m2_corr =
# m3_corr =

print('Linear regression: %.3f' % (m1_corr))
print('Random forest regressor: %.3f' % (m2_corr))
print('Multi layer perceptron: %.3f' % (m3_corr))
```

That's impressive. The linear regression model has a correlation value of 0.889 while random forests achieves 0.834. However, the multi layer perceptron performs poorly with a correlation of -0.183.

Let's look at the generated coefficients and intercept values for the linear regression model.

```python
print(lr.intercept_)
print(lr.coef_)
```

```python
# pair coefficients with feature names
list(zip(features, lr.coef_))
```

This shows that the linear regression model for estimating the redshift of a galaxy is:

$redshift = -0.027(mag_u) + 0.255(mag_g) - 0.138 (mag_r) - 0.373(mag_i) + 0.302(mag_z) - 0.294$

### Feature normalisation

An important data preparation step for many machine learning models is to [normalise / standarised the feature values](http://scikit-learn.org/stable/modules/preprocessing.html) in our dataset. e.g. we can scale the magnitude values to a smaller and standardised range of [0, 1]. Note: this is not required for decision trees and random forests as those models are scale invariant.

We can normalise our features to a range of [0, 1] using the scikit-learn `MinMaxScaler()` function.

```python
# Import the MinMaxScaler function

# Fit the scaler on the training dataset ONLY
# scaler =

# Transform both the training and test datasets
# X_train_scaled =
# X_test_scaled =

# Print out the new scaled values
print('Original scale:')
print('  X_train %.3f - %.3f' % (np.min(np.min(X_train)), np.max(np.max(X_train))))
print('  X_test %.3f - %.3f' % (np.min(np.min(X_test)), np.max(np.max(X_test))))
print('Transformed scale:')
print('  X_train %.3f - %.3f' % (np.min(X_train_scaled), np.max(X_train_scaled)))
print('  X_test %.3f - %.3f' % (np.min(X_test_scaled), np.max(X_test_scaled)))
```

Now fit a linear regression and multi layer perceptron model on the scaled dataset.

```python
# Instantiate the estimators
# lr_scaled =
# mlp_scaled =

# Fit the estimators

# Generate predictions
# y_pred_m1 =
# y_pred_m2 =

# Calculate the Root Mean Squared Error (RMSE)
# np.sqrt(mean_squared_error(...))
# m1_score =
# m2_score =

# calculate the correlation coefficient
# m1_corr =
# m2_corr =

# print the results
print('Linear regresssion scaled')
print('  RMSE: %.3f' % (m1_score))
print('  correlation: %.3f' % (m1_corr))
print('Multi layer perception scaled')
print('  RMSE: %.3f' % (m2_score))
print('  correlation: %.3f' % (m2_corr))
```

The linear regression model achieved the same performance on the scaled and unscaled dataset (0.022 RMSE). However, the multi layer perceptron model has an improved RMSE of 0.040 (previously 0.079) and correlation coefficient of 0.554 (previously -0.183) on the scaled dataset.

Normalisation might not always improve performance but it is common practice to do so before training certain types of models. You should also think about your dataset and what features you are normalising. e.g. are the features measured on a linear scale? Should magnitude be normalised or should the raw fluxes be normalised instead?

### Feature engineering

Another approach to improve the performance of our models is to try new features based on our knowledge of the domain. This is known as feature engineering.

We are currently using the raw magnitude bands as features to estimate a galaxy's redshift. However, it might make more sense to use colour features that measure the ratio of flux in neighbouring filters. This is equivalent to subtracting the magnitudes of the neighbouring filters. The key to photometric red shift is that a red shifted galaxy will have different observed colors to what it would have at red shift zero. i.e. galaxies at higher redshift tend to be redder in colour.

With this knowledge, let's build a model with 4 engineered features by subtracting neighbouring bands in the `ugriz` magnitude channels:

- $mag_u - mag_g$
- $mag_g - mag_r$
- $mag_r - mag_i$
- $mag_i - mag_z$

```python
# Make a copy of the features matrix
X_new = X.copy()

# Create new features
# subtract neighbouring magnitudes
# X_new['u-g'] =
# X_new['g-r'] =
# X_new['r-i'] =
# X_new['i-z'] =

# Remove the old columns
X_new.drop(['mag_u', 'mag_g', 'mag_r', 'mag_i', 'mag_z'], axis=1, inplace=True)
```

Fit a linear regression and a multi layer perceptron model with the new features.

```python
# Split the dataset into X_train, X_test, y_train, y_test
# X_train_new, X_test_new, y_train_new, y_test_new =

# Instantiate the estimators
# lr_new =
# mlp_new =

# Fit the estimators

# Generate predictions
# m1_pred =
# m2_pred =

# Calculate the RMSE
# m1_score =
# m2_score =

# Calculate the correlation coefficient
# m1_corr =
# m2_corr =

# Print the result
print('Linear regresssion new features')
print('  RMSE: %.3f' % (m1_score))
print('  correlation: %.3f' % (m1_corr))
print('Multi layer perceptron new features')
print('  RMSE: %.3f' % (m2_score))
print('  correlation: %.3f' % (m2_corr))
```

The linear regression model achieved a poorer RMSE of 0.025 (previously 0.022) and correlation coefficient of 0.846 (previously 0.888). However, the multi layer perceptron model achieved improved performance with a RMSE of 0.031 (previously on the scaled dataset 0.040) and a correlation coefficient of 0.753 (previously 0.554) with the new features.

It is up to you to decide on how long you want to spend coming up with and experiment with new features. The amount of time you spend on feature engineering is often based on your desired model performance and project requirements (e.g. deadlines).

### Challenge

Apply the steps used in the [Step 5: Classification](#Step-5:-Classification) section for cross-validation, generating learning curves, parameter tuning and reporting on your regression models.

```python

```

Are there any ways you can improve the performance? Can you come up with better features or try other [regression algorithms](http://scikit-learn.org/stable/supervised_learning.html#supervised-learning) for this dataset?

## 8. Clustering

[[ go back to the top ]](#Table-of-contents)

Clustering is an unsupervised learning algorithm that groups similar objects together. How these objects are grouped is based upon what features are provided to the algorithm. Recall that unsupervised learning algorithms model the underlying structure of the data `X`. No corresponding labels `y` are available / provided.

### K-means

K-means is a simple and popular clustering algorithm. The `K` in its name refers to the number of clusters we want to generate from the dataset. For example, `K = 3` means we want to generate 3 clusters.

The K-means algorithm:

1. randomly place `K` points into the feature space. These serve as the initial centroids.
2. Assign each example to the group with the closest centroid (Euclidian distance).
3. When all examples have been assigned, recalculate the positions of the `K` centroids as the mean of all examples in the cluster.
4. Repeat steps 2 and 3 until the centroids no longer change.

An example output of generating 3 clusters for a dataset is shown in figure below. The `x` markers show the location of the final centroids used to group examples.

Figure: K-means clustering example

**Note:** K-means does not work for categorical features. If you want to cluster categorical features then consider other methods such as [k-modes](https://pypi.python.org/pypi/kmodes/).

Let's try clustering galaxies based on the `u-r` and `g-i` colour features using the K-means algorithm.

First, let's load and prepare our dataset again for this section of the notebook.

```python
import numpy as np
import pandas as pd

# Set a random seed number to repoduce our results
seed = 19

# Clean dataset file
clean_dataset = 'data/galaxies-clean.csv'

# Load the dataset
df = pd.read_csv(clean_dataset)

# Calculate the new features
# df['u-r'] =
# df['g-i'] =

# Select columns of interest

# Create the features matrix
# X =

# Create the labels vector
# We're not using labels in modelling but rather for plotting
# y =
```

Split the dataset. Remember, we need to place the test dataset aside and treat it as it doesn't exist. We will be training a classification model based on insights gained from unsupervised learning later in the notebook.

```python
# Import the train_test_split function
from sklearn.model_selection import train_test_split

# Split the dataset into 80% training, 20% testing
# X_train, X_test, y_train, y_test =
```

Apply the K-means algorithm on the dataset.

```python
# Import the KMeans and StandardScaler libraries
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# Specify the number of clusters
K = 2

# Instantiate the algorithm
# kmeans =

# It is important to normalise the data for K-means to have zero mean
# To do this we will use the StandardScaler function
# X_train_scaled =

# Fit the algorithm

# predict the cluster / group for each example
# y_pred =

# Generate plots
plt.figure(figsize=(13, 5))

# Predicted clusters
plt.subplot(1, 2, 1)
plt.xlim(-3, 3)
plt.title('Predicted clusters')
plt.scatter(X_train_scaled[:,0], X_train_scaled[:,1], alpha=0.15, c=y_pred, cmap=plt.cm.Paired)
# Plot the centroids
plt.scatter(kmeans.cluster_centers_[:,0], kmeans.cluster_centers_[:,1], marker='X', s=500, c='black')

# Real labels
plt.subplot(1, 2, 2)
plt.xlim(-3, 3)
plt.title('Labels')
plt.scatter(X_train_scaled[:,0], X_train_scaled[:,1], alpha=0.15, c=y_train, cmap=plt.cm.Paired)
plt.show()

```

The generated K-means clusters don't match up well with the galaxy labels. That's fine because we aren't explicitly training a model to group galaxies by their shape. We're asking the model to group galaxies based on the features provided (i.e. colour bands).

We can also experiment different values for K and see how the clusters change.

```python
# Specify a list of cluster numbers
K_values = [2, 3, 4]

# Initialise plots
plt.figure(figsize=(13, 4))

for index, K in enumerate(K_values):
    kmeans = KMeans(n_clusters=K, random_state=seed)

    # Fit the algorithm
    kmeans.fit(X_train_scaled)

    # predict the cluster / group for each example
    y_pred = kmeans.predict(X_train_scaled)

    # plot results
    plt.subplot(1, 3, index+1)
    plt.xlim(-3, 3)
    plt.title('K = %d' % (K))
    plt.scatter(X_train_scaled[:,0], X_train_scaled[:,1], alpha=0.15, c=y_pred, cmap=plt.cm.Paired)
    # Plot the centroids
    plt.scatter(kmeans.cluster_centers_[:,0], kmeans.cluster_centers_[:,1], marker='X', s=500, c='black')

plt.show()
```

So what `K` value is best suited for our dataset? Since we have 2 classes in our dataset, we naturally want to group the data into 2 clusters. However, if we are open to consider different numbers of clusters, then it might actually make more sense to use `K=3` to generate 3 clusters, where the additional / middle cluster represents the 'green valley' (a transformative phase) where galaxies are not quite elliptical or spiral but a bit of both.

If we had no labels, then we don't know how many clusters would best represent our dataset beforehand. Whether these clusters make sense or not will be based on trial and error. We would use our domain knowledge and inspect objects in these clusters to identify their similarities.

However, a more systematic way to valiadate the number of clusters is to use the [elbow method](https://bl.ocks.org/rpgove/0060ff3b656618e9136b). This involves calculating the sum of squared errors (distance) between each data point and their assigned cluster centroid. The distance scores are plotted for different values of `K`. If the plot (aka scree plot) looks like an arm then the **elbow on the arm** is the value of `K` that is chosen as shown in the figure below.

Figure: Elbow method to choose the number of clusters

So far we have only clustered our dataset using two features: `u-r` and `g-i`. Let's try it with all the original features and with `K = 2`.

```python
# Create the features matrix
# Be sure to exclude the id and class columns
# X =

# Split the dataset into 80% training, 20% testing
# X_train, X_test, y_train, y_test

# Specify the number of clusters
# K =

# Instantiate the KMeans algorithm
# kmeans =

# Normalise the dataset using StandardScaler
# X_train_scaled =
# X_test_scaled =

# Fit the algorithm

# predict the cluster / group for each example
# y_pred =

# Print the number of objects grouped in each cluster
print('Cluster 0: %d' % (np.sum(y_pred == 0)))
print('Cluster 1: %d' % (np.sum(y_pred == 1)))
```

It looks like we have more galaxies grouped into Cluster 1. Let's examine a sample of records from each cluster.

```python
# display the first 10 galaxies in cluster 0
X.iloc[X_train[y_pred == 0].index].head(5)
```

```python
# display the first 10 galaxies in cluster 1
X.iloc[X_train[y_pred == 1].index].head(5)
```

We have generated 2 clusters using the K-means algorithm using 11 features (dimensions) in our dataset. But how can we visualise a model with more than 3 dimensions? We can explore dimensionality reduction methods to help us with this problem.

### Challenge

Scikit-learn offers many [clustering algorithms](http://scikit-learn.org/stable/modules/clustering.html) that you can easily and quickly experiment with on your datasets as shown below. Experiment with some of these algorithms and compare the generated clusters with the K-means algorithm.

```python

```

## 9. Dimensionality reduction

Dimensionality reduction is the process of reducing the number of features (dimensions) while trying to preserve as much structure in the dataset as possible. This is a useful method:

- for visualising data with ≥ 3 dimensions
- as a pre-processing step for other machine learning algorithms such as regression and clustering

There are two approaches to performing dimensionality reduction:

1. [Feature selection](http://scikit-learn.org/stable/modules/feature_selection.html): choose a subset of the original feature set
2. Feature extraction: construct a new and smaller feature set. e.g. from a combination of the original features.

For this notebook, we will experiment with a method called [Principal Components Analysis (PCA)](http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html). Scikit-learn provides feature extraction methods in two modules:

- [Feature extraction](http://scikit-learn.org/stable/modules/feature_extraction.html) - methods targetted towards extracting features from text and images
- [Matrix decomposition](http://scikit-learn.org/stable/modules/classes.html#module-sklearn.decomposition) - dimensionality reduction techniques such as PCA, factor analysis etc.

### Principal Components Analysis (PCA)

Principal Components Analysis (PCA) is a popular and commonly used dimensionality reduction method. The objective of PCA is to reduce a dataset to it's basic (principal) components stripping away unnecessary parts. These components represent the underlying structure of the dataset, indicating the direction of most variance (the direction where data is most spread out).

Here is a excellent [blog article explaining the intuition and inner workings of PCA](https://georgemdallas.wordpress.com/2013/10/30/principal-component-analysis-4-dummies-eigenvectors-eigenvalues-and-dimension-reduction/).

The PCA algorithm:

1. Normalise the dataset to have zero mean
2. Calculate the co-variance matrix
3. Calculate the eigenvalues and eigenvectors
4. Choose the top `k` components to form the new feature set
5. Transform / project the original dataset based on these `k` eigenvectors

An example of performing PCA on a 2-dimensional dataset is shown in the figure below.

Figure: PCA example - 1st component (red) and 2nd component (green)

A commonly used heurestic is to choose the `top k` components that can represent (explain) 90%, 95% or 99% of the variance of the dataset. In the above example, the 1st principal component (red) explains 99.6% of the variance so we could just use the `top` component and discard the 2nd. We can then use this principal component to transform (project) our original 2D dataset into a 1D dataset.

### Visualisation

First, let's use PCA to visualise our K-means model by only using the top 2 components.

```python
# Import the PCA algorithm

# Number of PCA components
components = 2

# Number of clusters
K = 2

# Initialise the PCA algorithm
# pca =

# Fit and transform the PCA on the dataset

# Initialise the K-means algorithm
# kmeans =

# Fit the on the transformed dataset

# Predict / assign the examples to a cluster
# y_pred =

# Generate plots
plt.figure(figsize=(13, 5))
plt.suptitle('%d components: explained variance = %.3f' % (components, np.sum(pca.explained_variance_ratio_)))

# Predicted clusters
plt.subplot(1, 2, 1)
plt.title('Predicted clusters')
plt.scatter(X_train_pca[:,0], X_train_pca[:,1], alpha=0.8, c=y_pred, cmap=plt.cm.Paired)
# Plot the centroids
plt.scatter(kmeans.cluster_centers_[:,0], kmeans.cluster_centers_[:,1], marker='X', s=500, c='black')

# Real labels
plt.subplot(1, 2, 2)
plt.title('Labels')
plt.scatter(X_train_pca[:,0], X_train_pca[:,1], alpha=0.8, c=y_train, cmap=plt.cm.Paired)
plt.show()
```

Similar to before, the generated clusters doesn't really match up with our labelled data. That's fine but let's examine how much variance is explained by the two principal components.

```python
print(pca.explained_variance_ratio_)
print(np.sum(pca.explained_variance_ratio_))
```

The components are able to explain 52% and 27% of the variance in the dataset respectively. The first two components explain a total of 79% variance of the dataset.

We can also evaluate the weights of the principal components with our features. Here, we are interested in the absolute values of the weights. Let's calculate percentages so it is easier to see which features contribute the most for each component.

```python
# Total sum of each component
component_totals = np.sum(np.abs(pca.components_), axis=1)

# Calculate the percentage for each component and feature
component_percentage = np.abs(pca.components_) / component_totals.reshape(2, 1) * 100
components_df = pd.DataFrame(component_percentage, columns=features).round(2)

components_df
```

We see that the magnitudes contribute the most for the first principal component. Their values range from 12.62% to 13.96% for a total of 66.26%. The second component has the most contribution from the `deVAB_r` (16.73%), `expAB_r` (16.77%) and `stellar_mass` (14.41%) features.

This table is a bit difficult to read so let's create a heatmap plot based on its values.

```python
# Transpose the dataframe to make it easier to read the feature names
sb.heatmap(components_df.T, cmap='Blues')
plt.ylabel('Feature')
plt.xlabel('Principal component')
plt.show()
```

That's much easier to identify the contribution of the features for each of the components. Now, let's find out how many components is required to explain 99% of the variance in the dataset.

```python
# Initialise the algorithm
# PCA keeps all components by default if you don't pass the argument n_components
# pca =

# Fit the PCA on the dataset

# Calculate the cumulative sum of the explained variance of each component
print(pca.explained_variance_ratio_.cumsum())

# Plot the chart showing the explained variance ratio
plt.ylim(0, 1.1)
plt.xlabel('Principal component', fontsize=15)
plt.ylabel('Explained variance', fontsize=15)
plt.plot(pca.explained_variance_ratio_.cumsum(), '--o')
plt.show()
```

We are able to explain 99% of the variance in the dataset we reach the 6th principal component.

Based on this result, we could transform our feature set using these 6 principal components without losing much information. This is particularly useful when we are imposed with engineering requirements where we may be required to reduce the training / prediction time of our models and the storage of our datasets.

### Preprocessing
Let's use PCA as a preprocessing step for training a Logistic Regression model. Recall that PCA should only be fitted on the training dataset (`X_train`). The fitted model however will be used to transform both training and testing datasets. We can also experiment with the number of principal components used for transforming the datasets.

```python
# Import LogisticRegression and the accuracy_score functions
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# Initialise the PCA algorithm
pca = PCA()

# Fit PCA model on the scaled training dataset
pca.fit(X_train_scaled)

# Use PCA to transform the scaled training and test datasets
X_train_pca = pca.transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled)

# Principal components list
# 1 to 11
top_k_values = range(1, len(features) + 1)

train_scores = []
test_scores = []

for top_k in top_k_values:
    # Build the feature sets based on the top_k components
    X_train_features = X_train_pca[:, 0:top_k]
    X_test_features = X_test_pca[:, 0:top_k]

    # Initialise our model
    logreg_pca = LogisticRegression(random_state=seed)

    # Fit the model
    logreg_pca.fit(X_train_features, y_train)

    # Generate predictions
    y_train_pred = logreg_pca.predict(X_train_features)
    y_test_pred = logreg_pca.predict(X_test_features)

    # Calculate the scores
    train_scores.append(accuracy_score(y_train, y_train_pred))
    test_scores.append(accuracy_score(y_test, y_test_pred))

# Plot the chart showing the explained variance ratio
plt.xlabel('Principal components', fontsize=15)
plt.ylabel('Accuracy', fontsize=15)
plt.ylim(0, 1)
plt.plot(train_scores, '--o', label='Training accuracy')
plt.plot(test_scores, '--o', label='Test accuracy')
plt.legend(fontsize=13)
plt.show()

# Print results to two decimal places
print('Train: %s' % (['%.3f' % x for x in train_scores]))
print('Test: %s' % (['%.3f' % x for x in test_scores]))
```

How does the performance of using the full feature set compare with using the PCA components? What is the number of components achieved the best performance in your experiments?

```python
# Initialise the model
# logreg =

# Fit the model using the scaled training dataset

# Generate predictions
# y_train_pred =
# y_test_pred =

# Calculate scores
print('Training accuracy: %.2f' % (accuracy_score(y_train, y_train_pred)))
print('Test accuracy: %.2f' % (accuracy_score(y_test, y_test_pred)))
```

## 10. Reproducibility

[[ go back to the top ]](#Table-of-contents)

Ensuring that your work is reproducible is an important step in any analysis. **As a rule, we shouldn't place much weight on a discovery that can't be reproduced**.

Notebooks like this one go a long way toward making our work reproducible. Since we documented every step as we moved along, we have a written record of what we did and why we did it — both in text and code.

## 11. Conclusions

[[ go back to the top ]](#Table-of-contents)

We hope you found this example notebook useful for your own work and learned at least one new trick by reading through it.

If you've spotted any errors or would like to contribute to this notebook, please don't hestitate to get in touch. We can be reached in the following ways:

* E-mail us

* [Submit an issue](https://github.com/CurtinIC/cic-ml-workshop/issues) on GitHub

* Fork the [notebook repository](https://github.com/CurtinIC/cic-ml-workshop), make the fix/addition yourself, then send over a pull request

## 12. Further reading

[[ go back to the top ]](#Table-of-contents)

This notebook covers a broad variety of topics but skips over many of the specifics. If you're looking to dive deeper into a particular topic, here's some recommended reading.

**Data Science**: William Chen compiled a [list of free books](http://www.wzchen.com/data-science-books/) for newcomers to Data Science, ranging from the basics of R & Python to Machine Learning to interviews and advice from prominent data scientists.

**Machine Learning**: /r/MachineLearning has a useful [Wiki page](https://www.reddit.com/r/MachineLearning/wiki/index) containing links to online courses, books, data sets, etc. for Machine Learning. There's also a [curated list](https://github.com/josephmisiti/awesome-machine-learning) of Machine Learning frameworks, libraries, and software sorted by language.

**scikit-learn** has a [bunch of tutorials](http://scikit-learn.org/stable/tutorial/index.html) for those looking to learn Machine Learning in Python. Andreas Mueller's [scikit-learn workshop materials](https://github.com/amueller/scipy_2015_sklearn_tutorial) are top-notch and freely available.

**pandas** has [several tutorials](http://pandas.pydata.org/pandas-docs/stable/tutorials.html) covering its myriad features.

**matplotlib** has many [books, videos, and tutorials](http://matplotlib.org/resources/index.html) to teach plotting in Python.

**Seaborn** has a [basic tutorial](http://stanford.edu/~mwaskom/software/seaborn/tutorial.html) covering most of the statistical plotting features.

```python

```


```python
import qutip
from qutip import Bloch as b
import matplotlib.pyplot as plt
import numpy as np
```

```python
from mpmath import limit
from mpmath import *
import numpy as np

#mp.dps = 20
```

### q_exp

$$ exp_{q}(u) = \lim_{a \to q}{(1+u(1-a))^{\frac{1}{1-a}}} $$
$$$$
com isso, vemos que
$$ exp_{1}(u) = \lim_{a \to 1}{(1+u(1-a))^{\frac{1}{1-a}}} $$

que igualando  $ n = {\frac{1}{1-a}} $ vemos que $(1-a) = \frac{1}{n}$, e $n \to \infty$
com o $ \lim_{a \to 1}{\frac{1}{1-a}}$ da equção acima, vemos que podemos verificar que
$$ exp_{1}(u) = \lim_{n \to \infty}{{ \left(1+\frac{u}{n} \right)}^{n}}$$
$$$$

### q_ln
$$ ln_{q}(u) = \lim_{a \to q}{\left(\frac{u^{1-a}-1}{1-a}\right)} $$

que pode ser reescrita em
$$ \lim_{a \to q}{(u^{1-a}-1)\left(\frac{1}{1-a}\right)} $$

igualando  $ h = {1-a} $, vemos que $h \to 0$
com o $ \lim_{a \to 1}{{1-a}}$ da equção acima. Então podemos verificar que

$$ln_{1}(u) = \lim_{h \to 0}\frac{u^h-1}{h}$$

```python
np.double(2)
```

    2.0

```python
from sympy import *
```

```python
x = symbols('x')
f = sin(x)/x
limit(f,x,2)
```

```python
from yaraah.tools import puts
```

```python
#transfromador de iamginário para real da biblioteca mpmath
def real_mp(number):
    return number.real()
float(mpf('1').real)

############# function base q_expotion ###############

def __q_exp__(u:float,q:float) -> np.double :

    __q__ = lambda _q_: 1/(1-_q_)
    param_q = limit(__q__,q)

    A = lambda w: 1/(w-1)
    #parametro limite de A
    param_A = np.double(limit(A,q).real)

    if(q > 1.9 and t >= param_A): return np.nan

     #function base q_expotion
    __e__ = lambda q_: np.power((1+u*(1-q_)),param_q)
    #try:
    return np.double(limit(__e__,q).real) if((1+u*(1-q))>=0) else 0
    #except(ZeroDivisionError):

'''
def q_exp(t,q=1):
    if(type(t) == np.ndarray or type(t) == list):
        res = []
        for i in t:
            res.append(__q_exp__(i,q))
            print(res)
        return np.array(res)
    else:
        return __q_exp__(t,q)
'''
q_exp = __q_exp__
```

```python
t = np.linspace(-2,2,100)
for i in t:
    print(q_exp(i,q=1))
type(t) == np.ndarray
```

```python
plt.plot(y,t)
```

```python
b = b()
b.show()
np.double(2222)
```

```python
from qutip import *
import numpy as np
import matplotlib.pyplot as plt

n=2#Atomic number
j = n//2
psi0 = spin_coherent(j, np.pi/3, 0)#Set the initial state of the system to spin coherent state

Jp=destroy(2*j+1).dag()#Up Operator
J_=destroy(2*j+1)#Descending operator
Jz=(Jp*J_-J_*Jp)/2#Jz

H=Jz**2#System's Hamiltonian

tlist=np.linspace(0,3,100)#Time list
result=mesolve(H,psi0,tlist)#State evolution over time

theta=np.linspace(0, np.pi, 50)
phi=np.linspace(0, 2*np.pi, 50)

#Calculate the husimi q function in the four states separately
Q1, THETA1, PHI1 = spin_q_function(result.states[0], theta, phi)
Q2, THETA2, PHI2 = spin_q_function(result.states[30], theta, phi)
Q3, THETA3, PHI3 = spin_q_function(result.states[60], theta, phi)
Q4, THETA4, PHI4 = spin_q_function(result.states[90], theta, phi)

#Draw the husimi q function in the four states in the four subgraphs
fig = plt.figure(dpi=150,constrained_layout=1)
ax1 = fig.add_subplot(221,projection='3d')
ax2 = fig.add_subplot(222,projection='3d')
ax3 = fig.add_subplot(223,projection='3d')
ax4 = fig.add_subplot(224,projection='3d')

plot_spin_distribution_3d(Q1, THETA1, PHI1,fig=fig,ax=ax1)
plot_spin_distribution_3d(Q2, THETA2, PHI2,fig=fig,ax=ax2)
plot_spin_distribution_3d(Q3, THETA3, PHI3,fig=fig,ax=ax3)
plot_spin_distribution_3d(Q4, THETA4, PHI4,fig=fig,ax=ax4)

for ax in [ax1,ax2,ax3,ax4]:
    ax.view_init(0.5*np.pi, 0)
    ax.axis('off')#Do not display the axis

fig.show()

```

```python
from qutip import *
import numpy as np
import matplotlib.pyplot as plt

alpha=1#Coherent light parameter alpha
n=2#Atomic number
j = n/2

psi0 = tensor(coherent(10,alpha),spin_coherent(j, 0, 0))#Set the initial state of the system

a=destroy(10)#Light field annihilation operator
a_plus=a.dag()#Light field generation operator
Jp=destroy(n+1).dag()#Atomic ascending operator
J_=destroy(n+1)#Atomic descending operator
Jx=(Jp+J_)/2#Atomic Jx operator
Jy=(Jp-J_)/(2j)#Atomic Jy operator, where j is the imaginary unit
Jz=(Jp*J_-J_*Jp)/2#Atomic Jz operator

H=tensor(a,Jp)+tensor(a_plus,J_)#System's Hamiltonian
tlist=np.linspace(0,10,1000)#Time list
result=mesolve(H,psi0,tlist)#State evolution over time

fig=plt.figure()
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222)
ax3 = fig.add_subplot(223)
ax4 = fig.add_subplot(224)

ax1.plot(tlist,expect(tensor(qeye(10),Jx),result.states))#Jx average value over time graph
ax2.plot(tlist,expect(tensor(qeye(10),Jy),result.states))#Jy's average over time graph
ax3.plot(tlist,expect(tensor(qeye(10),Jz),result.states))#Jz average change over time graph
ax4.plot(tlist,expect(tensor(qeye(10),Jx**2+Jy**2+Jz*2),result.states))#J squared average over time graph

fig.subplots_adjust(top=None,bottom=None,left=None,right=None,wspace=0.4,hspace=0.4)#Set the sub-picture spacing
fig.show()

```

```python

```