```python
%matplotlib inline

import matplotlib
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (12, 9)
plt.rcParams["font.size"] = 18
```

# In Core Fuel Management

In core fuel management focuses on the study of requirements and operational considerations impacting fuel performance in the reactor core, power history, core loading patterns, and refuelling activities.

## Learning Objectives

At the end of this lesson, you will be equipped to:

- List safety constraints driving in core fuel management decisions.
- Calculate capacity and availability factors.
- Calculate the mass required for each reactor year of operation.
- Calculate core and assembly discharge burnup based on power output.
- Analyze the reactivity evolution of a core based on burnup.
- Apply burnup calculations to multiple batch cores.
- Recognize the relationship between the number of batches and the final burnup.
- Understand the goals driving choices in various fuel loading patterns.
- Apply these lessons to pebble-fuelled and liquid-fueled advanced reactor designs.
- Recognize the impact of extended burnup on fuel utilization, SWU utilization, and fuel cycle cost.
- Understand how isotopic activities can be used to determine fuel burnup.
- Calculate burnup based on key activity ratios.

## Safety Constraints

-  $\frac{P_{peak}}{P_{avg}}$, peak to average power ratio.
- $T_{max}$, maximimum core temperature.
- Departure from Nucleate Boiling Ratio (DNBR)
- $\rho$, reactivity in the core.
- $\alpha_T$, temperature coefficient of reactivity

Primarily, there is a loss of coolant accident (LOCA) peak clad temp (PCT) limit of 1205 $^\circ C$, which limits the maximum pellet linear power density to approx 48 kW/m at Hot Full Power(HFP).

- Critical Heat Flux (CHF), which denotes departure from nuclear boiling (DNB) for a PWR and Dryout for a BWR, not being exceeded during anticipated transients, which limits the maximum average fuel pin linear power density to approximately 29 kW/m at HFP.
- Fuel cladding strain limit not exceeded during anticipated transients

### Safety Variables

- Fuel enrichment
- Re-load batch size & number of assemblies
- Fuel loading pattern of fresh and partially spent fuel assemblies
- Control mechanisms

## Mass Required

The simplest possible representation of the mass of fuel that must be added into a reactor is:

\begin{align}
M(t) &= \frac{Q}{BU}
\end{align}

where
\begin{align}
M &= \mbox{mass of heavy metal (e.g., uranium) in the core }[MTHM/yr]\\
Q &= \mbox{annual thermal energy output }[GWd/yr]\\
BU &= \mbox{burnup }[GWd/MTIHM]
\end{align}

But, Q itself typically needs to be back-calculated from energy produced.

\begin{align}
Q &= \frac{P_0\cdot CF\cdot T}{\eta_{th}}
\end{align}

where
\begin{align}
P_0 &= \mbox{installed electric capacity }[GWe]\\
CF &= \mbox{capacity factor }[-]\\
T &= \mbox{time in core } [days]\\
\eta_{th} &= \mbox{thermal efficiency }[GWe/GWth]\\
\end{align}

```python
def m(q, bu):
    return q/bu

def q(p0, cf, t, eta_th):
    return p0*cf*t/eta_th

p0 = 1.500 # installed electric capacity MWe
cf = 0.9 # capacity factor
t = 365 # days per year
eta_th = 0.33 # thermal efficiency MWe/MWth
bu = 50 # burnup GWd/MTIHM

print(m(q(p0, cf, t, eta_th), bu))

```

    29.863636363636363

## Capacity and Availability Factors

The capacity factor is representative of the plant's tendency to acheive its rated power capacity.

\begin{align}
CF &= \frac{\mbox{actual power generated over time T}}{\mbox{rated power potential over time T}}\\
  &=\frac{\int_0^T P(t)dt}{P_0T}\\
P(t) &= \mbox{ thermal power at time t during period T}
\end{align}

The capacity factor, integrated over time, gives Effective Full Power Days (EFPD), the equivalent number of days at full power.

\begin{align}
EFPD &= \int_0^TCF(t)dt\\
     &= \int_0^T \frac{\int_0^T P(t)dt}{P_0T}\\
\end{align}

The availability factor is always greater than the capacity factor.

\begin{align}
AF &= \frac{\mbox{time during which the reactor was operational during time period T}}{T}
\end{align}

```python
# The reactor shuts down:
# for a few days during the 10th month
# for one month during month 18
shutdowns = {10:10.1,
             18.5:19.5}

import numpy as np
def A(t, shutdowns):
    to_ret = 1.0*(t > 0)
    for start,stop in shutdowns.items():
        if start < t and t < stop:
            to_ret = 0
    return to_ret

times = np.arange(0.0, 20.0, 0.01)
hist = np.arange(0.0, 20.0, 0.01)
cf = np.arange(0.0, 20.0, 0.01)

for i in range(0, times.size):
    hist[i] = A(times[i], shutdowns)
    cf[i] = A(times[i], shutdowns)*(1.-0.01*np.random.random())

plt.plot(times, hist, label='Availability')
plt.plot(times, cf, label='Capacity')

plt.ylim([-0.5, 1.5])
plt.title('Capacity and Availabilty')
plt.xlabel('Time (months)')
plt.ylabel('Factor [-]')
plt.legend()

```

We can do a quick numeric integral to get each factor as an integral over the 20 month cycle.

\begin{align}
AF &= \frac{\int_0^{20}A(t)dt}{T}\\
CF &= \frac{\int_0^{20}P(t)dt}{P_0T}\\
\end{align}

```python
print("Availability Factor = ", hist.sum()/hist.shape[0])
print("Capacity Factor = ", cf.sum()/cf.shape[0])
```

    Availability Factor =  0.9455
    Capacity Factor =  0.9408645280541623

## Simple Reactivity Model
- On each cycle (1/n)th of the fuel is replaced
- Each fuel batch experiences a discharge burnup of Bd
- Each fuel batch on each cycle experiences a burnup of Bd/n
- $k_{reactor}$ is the uncontrolled multiplication factor (excess reactivity)
- $k_i$ is the infinite multiplication factor of a fuel batch (excess reactivity)

Each batch of fuel will have a different burn-up and $k_i(B)$ since each batch has been in the reactor a different length of time. The reactivity of the reactor is found by summing over the reactivities of all the batches of fuel, for n batches:

\begin{align}
k_{reactor} = \frac{1}{n}\sum_{i=1}^{n}k_i(B)
\end{align}

\begin{align}
k_i(B) = k_0 - \alpha B_n
\end{align}

- $k_0$ is the uncontrolled infinite multiplication factor of the fuel batch when it is fresh.
- $B_n$ is the burnup of the batch in a single cycle. The n refers to the number of batches that the reload scheme includes.
- $\alpha$ is a constant of proportionality with units of 1/Bn. Uniform linear depletion.
- $k_F$ is the uncontrolled infinite multiplication factor necessary to sustain a chain reaction at the end of an operating cycle

```python

def ki(k0, alpha, b):
    return k0 - alpha*b

def k(ki, n):
    return (1/n)*np.sum(ki)

n=0
k0 =4.5
alpha = (k0 - 1)/20000
bu = np.arange(0, 50000., 1000.)
plt.plot(bu, ki(k0, alpha, bu))
plt.plot(bu, np.zeros(bu.shape), color='r')
plt.ylabel(r'$k_i(B)$')
plt.xlabel(r'$B$')
plt.title('Excess Reactivity Using Linear Depletion Model')
```

This approximation is somewhat accurate and gives an intuition for the impact of reloading on excess reactivity in the core.

## Single Cycle Refuelling

\begin{align}
k_{reactor} = k_1(B_1)
\end{align}

\begin{align}
k_1(B_1) = k_0 - \alpha B_1
\end{align}

Therefore the fuel burnup capability is:

\begin{align}
B_1 &= \frac{k_0-k_F}{\alpha}
\end{align}

## Two Cycle Refuelling

At the end of each cycle one batch of fuel has been burned for one cycle and the other batch has been burned for two cycles. Thus:

\begin{align}
k_F &= \frac{k_0 - \alpha B_2}{2} + \frac{k_0 - 2\alpha B_2}{2}\\
  &= k_0 - \frac{3\alpha B_2}{2}\\
B_2 &= \frac{2(k_0 - k_F)}{3\alpha}\\
    &= \frac{2}{3}B_1
\end{align}

- Each batch in the two cycle reload scheme is burned for $2B_2$.

So, in terms of the single cycle reload burnup:

\begin{align}
2B_2 &= 2\left(\frac{2}{3}B_1\right)\\
     &= \frac{4}{3}B_1\\
\end{align}

**This means there is 1/3 more burnup in the two cycle reload, for the same initial and final multiplication factors $k_0$ and $k_F$ (exactly the same fuel.)**

## N Cycle Reload Scheme

The relation between end-of-cycle core multiplication factor kF and the fresh fuel batch infinite multiplication factor k0 and the batch burnup in general is

\begin{align}
k_F &= k_0 - \frac{1}{n}\sum_{i=1}^{n}i\alpha B_n\\
\end{align}

Recall from your geometric series intution:
\begin{align}
\sum_{i=1}^{n}i &= \frac{n(n + 1)}{2}\\
\end{align}

Therefore:

\begin{align}
k_F &= k_0 - \left(\frac{n + 1}{2}\right)\alpha B_n\\
\end{align}

The batch burnup in a single cycle is then the result of solving for $B_n$:

\begin{align}
B_n &= \frac{2(k_0 - k_F)}{\alpha(n + 1)}
\end{align}

The discharge burnup of batch n, is the batch burnup in a cycle times the number of cycles:

\begin{align}
B_n^d &= nB_n\\
&= \frac{2n(k_0 - k_F)}{\alpha(n + 1)}\\
      &= \left(\frac{2n}{n + 1}\right)\frac{k_0 - k_F}{\alpha} \\
 &= \left(\frac{2n}{n + 1}\right)B_1 \\
\end{align}

```python
def bd(n, b1):
    num = 2*n*b1
    denom = n+1
    return num/denom

b1 = 12000
n = np.arange(1,50)
plt.plot(n, bd(n, b1))
```

### Discussion: What is the primary drawback of many batches per core?

## Fuel Loading Patterns

Various fuel loading patterns are used to acheive improved fuel utilization (higher burnup), better core control, and lower leakage to the pressure vessel.

## Many and $\infty$ Batch Reactor Designs

Infinite batch refuelling (a.k.a. online refuelling) is possible in liquid fuelled cores with online reprocessing.

What exactly is a pebble core, then, in terms of batches?

Aufiero, 2016

## Determining Burnup

- Direct methods occur while the fuel is still in the core (using ion chambers and in-core flux probes)
- Indirect methods use measurements of activity after the fuel has been removed.

\begin{align}
BU &= a + bA(^{137}Cs)\\
BU &= c(e, r) + d(e, r) \left[A(^{134}Cs)/A(^{137}Cs)\right]\\
BU &= a\cdot exp{\left[b\cdot ln\left(\frac{A(^{106}Ru)A(^{137}Cs)}{[A(^{134}Cs)^2}\right)\right]}\\
a, b, c, d &= \mbox{calibration constants}\\
e &= \mbox{enrichment}\\
r &= \mbox{power rating}
\end{align}

```python

```


# **CS224W - Colab 3**

In Colab 2 we constructed GNN models by using PyTorch Geometric's built in GCN layer, `GCNConv`. In this Colab we will go a step deeper and implement the **GraphSAGE** ([Hamilton et al. (2017)](https://arxiv.org/abs/1706.02216)) layer directly. Then we will run our models on the CORA dataset, which is a standard citation network benchmark dataset.

**Note**: Make sure to **sequentially run all the cells in each section** so that the intermediate variables / packages will carry over to the next cell

Have fun and good luck on Colab 3 :)

# Device
We recommend using a GPU for this Colab.

Please click `Runtime` and then `Change runtime type`. Then set the `hardware accelerator` to **GPU**.

## Installation

```python
# Install torch geometric
import os
if 'IS_GRADESCOPE_ENV' not in os.environ:
  !pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
  !pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
  !pip install torch-geometric
  !pip install -q git+https://github.com/snap-stanford/deepsnap.git
```

    Looking in links: https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
    Collecting torch-scatter
      Downloading https://data.pyg.org/whl/torch-1.9.0%2Bcu111/torch_scatter-2.0.8-cp37-cp37m-linux_x86_64.whl (10.4 MB)
         |████████████████████████████████| 10.4 MB 12.5 MB/s
    Installing collected packages: torch-scatter
    Successfully installed torch-scatter-2.0.8
    Looking in links: https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
    Collecting torch-sparse
      Downloading https://data.pyg.org/whl/torch-1.9.0%2Bcu111/torch_sparse-0.6.12-cp37-cp37m-linux_x86_64.whl (3.7 MB)
         |████████████████████████████████| 3.7 MB 12.2 MB/s
    Requirement already satisfied: scipy in /usr/local/lib/python3.7/dist-packages (from torch-sparse) (1.4.1)
    Requirement already satisfied: numpy>=1.13.3 in /usr/local/lib/python3.7/dist-packages (from scipy->torch-sparse) (1.19.5)
    Installing collected packages: torch-sparse
    Successfully installed torch-sparse-0.6.12
    Collecting torch-geometric
      Downloading torch_geometric-2.0.1.tar.gz (308 kB)
         |████████████████████████████████| 308 kB 14.4 MB/s
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (1.19.5)
    Requirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (4.62.3)
    Requirement already satisfied: scipy in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (1.4.1)
    Requirement already satisfied: networkx in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (2.6.3)
    Requirement already satisfied: scikit-learn in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (0.22.2.post1)
    Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (2.23.0)
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (1.1.5)
    Collecting rdflib
      Downloading rdflib-6.0.2-py3-none-any.whl (407 kB)
         |████████████████████████████████| 407 kB 63.7 MB/s
    Requirement already satisfied: googledrivedownloader in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (0.4)
    Requirement already satisfied: jinja2 in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (2.11.3)
    Requirement already satisfied: pyparsing in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (2.4.7)
    Collecting yacs
      Downloading yacs-0.1.8-py3-none-any.whl (14 kB)
    Requirement already satisfied: PyYAML in /usr/local/lib/python3.7/dist-packages (from torch-geometric) (3.13)
    Requirement already satisfied: MarkupSafe>=0.23 in /usr/local/lib/python3.7/dist-packages (from jinja2->torch-geometric) (2.0.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->torch-geometric) (2018.9)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas->torch-geometric) (2.8.2)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas->torch-geometric) (1.15.0)
    Collecting isodate
      Downloading isodate-0.6.0-py2.py3-none-any.whl (45 kB)
         |████████████████████████████████| 45 kB 3.5 MB/s
    Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from rdflib->torch-geometric) (57.4.0)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->torch-geometric) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->torch-geometric) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests->torch-geometric) (1.24.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests->torch-geometric) (2021.5.30)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn->torch-geometric) (1.0.1)
    Building wheels for collected packages: torch-geometric
      Building wheel for torch-geometric (setup.py) ... done
      Created wheel for torch-geometric: filename=torch_geometric-2.0.1-py3-none-any.whl size=513822 sha256=0508097be7556c0ef204e9968e6883b06a3417e5d4502507cd4808897763bf9b
      Stored in directory: /root/.cache/pip/wheels/78/3d/42/20589db73c66b5109fb93a0c5743edfd6ab5ca820a52afacfc
    Successfully built torch-geometric
    Installing collected packages: isodate, yacs, rdflib, torch-geometric
    Successfully installed isodate-0.6.0 rdflib-6.0.2 torch-geometric-2.0.1 yacs-0.1.8
      Building wheel for deepsnap (setup.py) ... done

```python
import torch_geometric
torch_geometric.__version__
```

    '2.0.2'

# 1) GNN Layers

## Implementing Layer Modules

In Colab 2, we implemented a GCN model for node and graph classification tasks. However, for that notebook we took advantage of PyG's built in GCN module. For Colab 3, we provide a build upon a general Graph Neural Network Stack, into which we will be able to plugin our own module implementations: GraphSAGE and GAT.

We will then use our layer implemenations to complete node classification on the CORA dataset, a standard citation network benchmark. In this dataset, nodes correspond to documents and edges correspond to undirected citations. Each node or document in the graph is assigned a class label and features based on the documents binarized bag-of-words representation. Specifically, the Cora graph has 2708 nodes, 5429 edges, 7 prediction classes, and 1433 features per node.

## GNN Stack Module

Below is the implementation of a general GNN stack, where we can plugin any GNN layer, such as **GraphSage**, **GAT**, etc. This module is provided for you. Your implementations of the **GraphSage** and **GAT** (Colab 4) layers will function as components in the GNNStack Module.

```python
import torch
import torch_scatter
import torch.nn as nn
import torch.nn.functional as F

import torch_geometric.nn as pyg_nn
import torch_geometric.utils as pyg_utils

from torch import Tensor
from typing import Union, Tuple, Optional
from torch_geometric.typing import (OptPairTensor, Adj, Size, NoneType,
                                    OptTensor)

from torch.nn import Parameter, Linear
from torch_sparse import SparseTensor, set_diag
from torch_geometric.nn.conv import MessagePassing
from torch_geometric.utils import remove_self_loops, add_self_loops, softmax

class GNNStack(torch.nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, args, emb=False):
        super(GNNStack, self).__init__()
        conv_model = self.build_conv_model(args.model_type)
        self.convs = nn.ModuleList()
        self.convs.append(conv_model(input_dim, hidden_dim))
        assert (args.num_layers >= 1), 'Number of layers is not >=1'
        for l in range(args.num_layers-1):
            self.convs.append(conv_model(args.heads * hidden_dim, hidden_dim))

        # post-message-passing
        self.post_mp = nn.Sequential(
            nn.Linear(args.heads * hidden_dim, hidden_dim), nn.Dropout(args.dropout),
            nn.Linear(hidden_dim, output_dim))

        self.dropout = args.dropout
        self.num_layers = args.num_layers

        self.emb = emb

    def build_conv_model(self, model_type):
        if model_type == 'GraphSage':
            return GraphSage
        elif model_type == 'GAT':
            # When applying GAT with num heads > 1, you need to modify the
            # input and output dimension of the conv layers (self.convs),
            # to ensure that the input dim of the next layer is num heads
            # multiplied by the output dim of the previous layer.
            # HINT: In case you want to play with multiheads, you need to change the for-loop that builds up self.convs to be
            # self.convs.append(conv_model(hidden_dim * num_heads, hidden_dim)),
            # and also the first nn.Linear(hidden_dim * num_heads, hidden_dim) in post-message-passing.
            return GAT

    def forward(self, data):
        x, edge_index, batch = data.x, data.edge_index, data.batch

        for i in range(self.num_layers):
            x = self.convs[i](x, edge_index)
            x = F.relu(x)
            x = F.dropout(x, p=self.dropout,training=self.training)

        x = self.post_mp(x)

        if self.emb == True:
            return x

        return F.log_softmax(x, dim=1)

    def loss(self, pred, label):
        return F.nll_loss(pred, label)
```

## Creating Our Own Message Passing Layer

Now let's start implementing our own message passing layers! Working through this part will help us become acutely familiar with the behind the scenes work of implementing Pytorch Message Passing Layers, allowing us to build our own GNN models. To do so, we will work with and implement 3 critcal functions needed to define a PyG Message Passing Layer: `forward`, `message`, and `aggregate`.

Before diving head first into the coding details, let us quickly review the key components of the message passing process. To do so, we will focus on a single round of messsage passing with respect to a single central node $x$. Before message passing, $x$ is associated with a feature vector $x^{l-1}$, and the goal of message passing is to update this feature vector as $x^l$. To do so, we implement the following steps: 1) each neighboring node $v$ passes its current message $v^{l-1}$ across the edge $(x, v)$ - 2) for the node $x$, we aggregate all of the messages of the neighboring nodes (for example through a sum or mean) - and 3) we transform the aggregated information by for example applying linear and non-linear transformations. Altogether, the message passing process is applied such that every node $u$ in our graph updates its embedding by acting as the central node $x$ in step 1-3 described above.

Now, we extending this process to that of a single message passing layer, the job of a message passing layer is to update the current feature representation or embedding of each node in a graph by propagating and transforming information within the graph. Overall, the general paradigm of a message passing layers is: 1) pre-processing -> 2) **message passing** / propagation -> 3) post-processing.

The `forward` fuction that we will implement for our message passing layer captures this execution logic. Namely, the `forward` function handles the pre and post-processing of node features / embeddings, as well as initiates message passing by calling the `propagate` function.

The `propagate` function encapsulates the message passing process! It does so by calling three important functions: 1) `message`, 2) `aggregate`, and 3) `update`. Our implementation will vary slightly from this, as we will not explicitly implement `update`, but instead place the logic for updating node embeddings after message passing and within the `forward` function. To be more specific, after information is propagated (message passing), we can further transform the node embeddings outputed by `propagate`. Therefore, the output of `forward` is exactly the node embeddings after one GNN layer.

Lastly, before starting to implement our own layer, let us dig a bit deeper into each of the functions described above:

1.

```
def propagate(edge_index, x=(x_i, x_j), extra=(extra_i, extra_j), size=size):
```
Calling `propagate` initiates the message passing process. Looking at the function parameters, we highlight a couple of key parameters.

  - `edge_index` is passed to the forward function and captures the edge structure of the graph.
  - `x=(x_i, x_j)` represents the node features that will be used in message passing. In order to explain why we pass the tuple `(x_i, x_j)`, we first look at how our edges are represented. For every edge $(i, j) \in \mathcal{E}$, we can differentiate $i$ as the source or central node ($x_{central}$) and j as the neighboring node ($x_{neighbor}$).

    Taking the example of message passing above, for a central node $u$ we will aggregate and transform all of the messages associated with the nodes $v$ s.t. $(u, v) \in \mathcal{E}$ (i.e. $v \in \mathcal{N}_{u}$). Thus we see, the subscripts `_i` and `_j` allow us to specifcally differenciate features associated with central nodes (i.e. nodes  recieving message information) and neighboring nodes (i.e. nodes passing messages).

    This is definitely a somewhat confusing concept; however, one key thing to remember / wrap your head around is that depending on the perspective, a node $x$ acts as a central node or a neighboring node. In fact, in undirected graphs we store both edge directions (i.e. $(i, j)$ and $(j, i)$). From the central node perspective, `x_i`, x is collecting neighboring information to update its embedding. From a neighboring node perspective, `x_j`, x is passing its message information along the edge connecting it to a different central node.

  - `extra=(extra_i, extra_j)` represents additional information that we can associate with each node beyond its current feature embedding. In fact, we can include as many additional parameters of the form `param=(param_i, param_j)` as we would like. Again, we highlight that indexing with `_i` and `_j` allows us to differentiate central and neighboring nodes.

  The output of the `propagate` function is a matrix of node embeddings after the message passing process and has shape $[N, d]$.

2.
```
def message(x_j, ...):
```
The `message` function is called by propagate and constructs the messages from
neighboring nodes $j$ to central nodes $i$ for each edge $(i, j)$ in *edge_index*. This function can take any argument that was initially passed to `propagate`. Furthermore, we can again differentiate central nodes and neighboring nodes by appending `_i` or `_j` to the variable name, .e.g. `x_i` and `x_j`. Looking more specifically at the variables, we have:

  - `x_j` represents a matrix of feature embeddings for all neighboring nodes passing their messages along their respective edge (i.e. all nodes $j$ for edges $(i, j) \in \mathcal{E}$). Thus, its shape is $[|\mathcal{E}|, d]$!
  - In implementing GAT we will see how to access additional variables passed to propagate

  Critically, we see that the output of the `message` function is a matrix of neighboring node embeddings ready to be aggregated, having shape $[|\mathcal{E}|, d]$.

3.
```
def aggregate(self, inputs, index, dim_size = None):
```
Lastly, the `aggregate` function is used to aggregate the messages from neighboring nodes. Looking at the parameters we highlight:

  - `inputs` represents a matrix of the messages passed from neighboring nodes (i.e. the output of the `message` function).
  - `index` has the same shape as `inputs` and tells us the central node that corresponding to each of the rows / messages $j$ in the `inputs` matrix. Thus, `index` tells us which rows / messages to aggregate for each central node.

  The output of `aggregate` is of shape $[N, d]$.

For additional resources refer to the PyG documentation for implementing custom message passing layers: https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html

## GraphSage Implementation

For our first GNN layer, we will implement the well known GraphSage ([Hamilton et al. (2017)](https://arxiv.org/abs/1706.02216)) layer!

For a given *central* node $v$ with current embedding $h_v^{l-1}$, the message passing update rule to tranform $h_v^{l-1} \rightarrow h_v^l$ is as follows:

\begin{equation}
h_v^{(l)} = W_l\cdot h_v^{(l-1)} + W_r \cdot AGG(\{h_u^{(l-1)}, \forall u \in N(v) \})
\end{equation}

where $W_1$ and $W_2$ are learanble weight matrices and the nodes $u$ are *neighboring* nodes. Additionally, we use mean aggregation for simplicity:

\begin{equation}
AGG(\{h_u^{(l-1)}, \forall u \in N(v) \}) = \frac{1}{|N(v)|} \sum_{u\in N(v)} h_u^{(l-1)}
\end{equation}

One thing to note is that we're adding a **skip connection** to our GraphSage implementation through the term $W_l\cdot h_v^{(l-1)}$.

Before implementing this update rule, we encourage you to think about how different parts of the formulas above correspond with the functions outlined earlier: 1) `forward`, 2) `message`, and 3) `aggregate`. As a hint, we are given what the aggregation function is (i.e. mean aggregation)! Now the question remains, what are the messages passed by each neighbor nodes and when do we call the `propagate` function?

Note: in this case the message function or messages are actually quite simple. Additionally, remember that the `propagate` function encapsulates the operations of / the outputs of the combined `message` and `aggregate` functions.

Lastly, $\ell$-2 normalization of the node embeddings is applied after each iteration.

For the following questions, DON'T refer to any existing implementations online.

```python
class GraphSage(MessagePassing):

    def __init__(self, in_channels, out_channels, normalize = True,
                 bias = False, **kwargs):
        super(GraphSage, self).__init__(**kwargs)

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.normalize = normalize

        self.lin_l = None
        self.lin_r = None

        ############################################################################
        # TODO: Your code here!
        # Define the layers needed for the message and update functions below.
        # self.lin_l is the linear transformation that you apply to embedding
        #            for central node.
        # self.lin_r is the linear transformation that you apply to aggregated
        #            message from neighbors.
        # Don't forget the bias!
        # Our implementation is ~2 lines, but don't worry if you deviate from this.

        self.lin_l = Linear(self.in_channels, self.out_channels, bias = bias)
        self.lin_r = Linear(self.in_channels, self.out_channels, bias = bias)

        ############################################################################

        self.reset_parameters()

    def reset_parameters(self):
        self.lin_l.reset_parameters()
        self.lin_r.reset_parameters()

    def forward(self, x, edge_index, size = None):
        """"""

        out = None

        ############################################################################
        # TODO: Your code here!
        # Implement message passing, as well as any post-processing (our update rule).
        # 1. Call the propagate function to conduct the message passing.
        #    1.1 See the description of propagate above or the following link for more information:
        #        https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html
        #    1.2 We will only use the representation for neighbor nodes (x_j), so by default
        #        we pass the same representation for central and neighbor nodes as x=(x, x).
        # 2. Update our node embedding with skip connection from the previous layer.
        # 3. If normalize is set, do L-2 normalization (defined in
        #    torch.nn.functional)
        #
        # Our implementation is ~5 lines, but don't worry if you deviate from this.
        out = torch.zeros(x.shape[0],self.out_channels)
        for i in range(x.shape[0]): # loop over nodes
            index = edge_index[1][edge_index[0] == i]
            agg = x[index].mean(axis = 0)
            out_i = self.lin_l(x[i]) + self.lin_r(agg)
            if self.normalize:
                out_i = F.normalize(out_i, dim = 0)
            out[i] = out_i

        ############################################################################

        return out

    def message(self, x_j):

        out = None

        ############################################################################
        # TODO: Your code here!
        # Implement your message function here.
        # Hint: Look at the formulation of the mean aggregation function, focusing on
        # what message each neighboring node passes.
        #
        # Our implementation is ~1 lines, but don't worry if you deviate from this.

        ############################################################################

        return out

    def aggregate(self, inputs, index, dim_size = None):

        out = None

        # The axis along which to index number of nodes.
        node_dim = self.node_dim

        ############################################################################
        # TODO: Your code here!
        # Implement your aggregate function here.
        # See here as how to use torch_scatter.scatter:
        # https://pytorch-scatter.readthedocs.io/en/latest/functions/scatter.html#torch_scatter.scatter
        #
        # Our implementation is ~1 lines, but don't worry if you deviate from this.

        ############################################################################

        return out

```

## Building Optimizers

This function has been implemented for you. **For grading purposes please use the default Adam optimizer**, but feel free to play with other types of optimizers on your own.

```python
import torch.optim as optim

def build_optimizer(args, params):
    weight_decay = args.weight_decay
    filter_fn = filter(lambda p : p.requires_grad, params)
    if args.opt == 'adam':
        optimizer = optim.Adam(filter_fn, lr=args.lr, weight_decay=weight_decay)
    elif args.opt == 'sgd':
        optimizer = optim.SGD(filter_fn, lr=args.lr, momentum=0.95, weight_decay=weight_decay)
    elif args.opt == 'rmsprop':
        optimizer = optim.RMSprop(filter_fn, lr=args.lr, weight_decay=weight_decay)
    elif args.opt == 'adagrad':
        optimizer = optim.Adagrad(filter_fn, lr=args.lr, weight_decay=weight_decay)
    if args.opt_scheduler == 'none':
        return None, optimizer
    elif args.opt_scheduler == 'step':
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=args.opt_decay_step, gamma=args.opt_decay_rate)
    elif args.opt_scheduler == 'cos':
        scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.opt_restart)
    return scheduler, optimizer
```

## Training and Testing

Here we provide you with the functions to train and test. **Please do not modify this part for grading purposes.**

```python
import time, os

import networkx as nx
import numpy as np
import torch
import torch.optim as optim
from tqdm import trange
import pandas as pd
import copy

from torch_geometric.datasets import TUDataset
from torch_geometric.datasets import Planetoid
from torch_geometric.data import DataLoader

import torch_geometric.nn as pyg_nn

import matplotlib.pyplot as plt

def train(dataset, args):

    print("Node task. test set size:", np.sum(dataset[0]['test_mask'].numpy()))
    print()
    test_loader = loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False)

    # build model
    model = GNNStack(dataset.num_node_features, args.hidden_dim, dataset.num_classes,
                            args)
    scheduler, opt = build_optimizer(args, model.parameters())

    # train
    losses = []
    test_accs = []
    best_acc = 0
    best_model = None
    for epoch in trange(args.epochs, desc="Training", unit="Epochs"):
        total_loss = 0
        model.train()
        for batch in loader:
            opt.zero_grad()
            pred = model(batch)
            label = batch.y
            pred = pred[batch.train_mask]
            label = label[batch.train_mask]
            loss = model.loss(pred, label)
            loss.backward()
            opt.step()
            total_loss += loss.item() * batch.num_graphs
        total_loss /= len(loader.dataset)
        losses.append(total_loss)

        if epoch % 10 == 0:
          test_acc = test(test_loader, model)
          test_accs.append(test_acc)
          if test_acc > best_acc:
            best_acc = test_acc
            best_model = copy.deepcopy(model)
        else:
          test_accs.append(test_accs[-1])

    return test_accs, losses, best_model, best_acc, test_loader

def test(loader, test_model, is_validation=False, save_model_preds=False, model_type=None):
    test_model.eval()

    correct = 0
    # Note that Cora is only one graph!
    for data in loader:
        with torch.no_grad():
            # max(dim=1) returns values, indices tuple; only need indices
            pred = test_model(data).max(dim=1)[1]
            label = data.y

        mask = data.val_mask if is_validation else data.test_mask
        # node classification: only evaluate on nodes in test set
        pred = pred[mask]
        label = label[mask]

        if save_model_preds:
          print ("Saving Model Predictions for Model Type", model_type)

          data = {}
          data['pred'] = pred.view(-1).cpu().detach().numpy()
          data['label'] = label.view(-1).cpu().detach().numpy()

          df = pd.DataFrame(data=data)
          # Save locally as csv
          df.to_csv('CORA-Node-' + model_type + '.csv', sep=',', index=False)

        correct += pred.eq(label).sum().item()

    total = 0
    for data in loader.dataset:
        total += torch.sum(data.val_mask if is_validation else data.test_mask).item()

    return correct / total

class objectview(object):
    def __init__(self, d):
        self.__dict__ = d

```

## Let's Start the Training!

We will be working on the CORA dataset on node-level classification.

This part is implemented for you. **For grading purposes, please do not modify the default parameters.** However, feel free to play with different configurations just for fun!

**Submit your best accuracy and loss on Gradescope.**

```python
if 'IS_GRADESCOPE_ENV' not in os.environ:
    for args in [
        {'model_type': 'GraphSage', 'dataset': 'cora', 'num_layers': 2, 'heads': 1, 'batch_size': 32, 'hidden_dim': 32, 'dropout': 0.5, 'epochs': 500, 'opt': 'adam', 'opt_scheduler': 'none', 'opt_restart': 0, 'weight_decay': 5e-3, 'lr': 0.01},
    ]:
        args = objectview(args)
        for model in ['GraphSage']:
            args.model_type = model

            # Match the dimension.
            if model == 'GAT':
              args.heads = 2
            else:
              args.heads = 1

            if args.dataset == 'cora':
                dataset = Planetoid(root='/tmp/cora', name='Cora')
            else:
                raise NotImplementedError("Unknown dataset")
            test_accs, losses, best_model, best_acc, test_loader = train(dataset, args)

            print("Maximum test set accuracy: {0}".format(max(test_accs)))
            print("Minimum loss: {0}".format(min(losses)))

            # Run test for our best model to save the predictions!
            test(test_loader, best_model, is_validation=False, save_model_preds=True, model_type=model)
            print()

            plt.title(dataset.name)
            plt.plot(losses, label="training loss" + " - " + args.model_type)
            plt.plot(test_accs, label="test accuracy" + " - " + args.model_type)
        plt.legend()
        plt.show()

```

## Question 1.1: What is the maximum accuracy obtained on the test set for GraphSage? (10 points)

Running the cell above will show the results of your best model and save your best model's predictions to a file named *CORA-Node-GraphSage.csv*.

As we have seen before you can view this file by clicking on the *Folder* icon on the left side pannel. When you sumbit your assignment, you will have to download this file and attatch it to your submission.


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

## Sections  0, 1 et 2: Sections 1.0, 1.1 et 1.2: la viscosité des fluides et comment la viscosité affecte le mouvement des fluides.
>#### Pour les deux premières sections, lire les pages 1-23 du document suivant:
>http://gch200-pp.espaceweb.usherbrooke.ca/Chap1.pdf
#### elles vous présentent le concept de viscosité et donnent des exemples de valeurs typiques des viscosités de certains fluides.

# *

## Section 1.3, calcul de la viscosité réduite de fluides. La température, la pression et la viscosité sont normalisées en les divisant par la propriété critique. Le graphe 1.3-1 montre ces viscosités réduites.

> #### Les packages python "CoolProp" et "thermo" ont été développés pour calculer les propriétés thermodynamiques et de transport en utilisant les méthodes les plus récentes de calcul. Au lieu d'utiliser la figure 1.3-1 et l'annexe E pour estimer la viscosité en fonction de la température et de la pression, il est facile de trouver la viscosité en utilisant simplement, par exemple pour la viscosité du CO2 à 300 K et 101325 Pascals (1 atm):
    from CoolProp.CoolProp import PropsSI
    muCO2C=PropsSI('V','T',300,'P',101325,'CO2')
    import thermo as th
    CO2=th.Chemical('CO2',T=300,P=101325)   # thermo utilise une approche objet
    print(muCO2C,CO2.mug)                   # ainsi CO2.mug est la viscosité à l'état gazeux du CO2
#### Dans ce qui suit, on utilise CoolProp pour reconstruire le graphe 1.3-1. Par la suite on utilisera aussi thermo

#### Voyons d'abord si le calcul des propriétés critiques est bien fait, comparons avec l'annexe E, rappelons-nous que le SI est pour Système International, donc Mètres, Kilogramme, Seconde, Joule, etc... Pas d'unités mixtes comme les poises, les atmosphères, les PSI, les livres-forces, et autres unités développées sans standard cohérent. Vérifiez dans l'annexe E si les calculs de CoolProp coincident bien avec les données de Transport Phenomena.

```python
from CoolProp.CoolProp import PropsSI
muCO2C=PropsSI('V','T',300,'P',101325,'CO2')
print(muCO2C)
```

    1.5021470647392787e-05

```python
import thermo as th
CO2=th.Chemical('CO2',T=300,P=101325)
print(CO2.mug)
```

    1.5021470647392787e-05

```python
import numpy as np
import matplotlib.pyplot as plt
from CoolProp.CoolProp import PropsSI
%matplotlib inline
print(('{:<12}'*4).format('Fluide','Tc (K)','Pc (Pa)','muc (Pa*s)'))
print('-'*48)
for fluide in ['methane','ethane','propane','argon','N2','O2','CO2','air']:
    Tc=PropsSI('Tcrit',fluide)
    Pc=PropsSI('Pcrit',fluide)
    vc=PropsSI('V','T',Tc,'P',Pc,fluide)
    print('{:<12}{:<12.3f}{:<12.3e}{:<12.3e}'.format(fluide,Tc,Pc,vc))
```

    Fluide      Tc (K)      Pc (Pa)     muc (Pa*s)
    ------------------------------------------------
    methane     190.564     4.599e+06   1.593e-05
    ethane      305.322     4.872e+06   2.187e-05
    propane     369.890     4.251e+06   2.393e-05
    argon       150.687     4.863e+06   2.731e-05
    N2          126.192     3.396e+06   1.808e-05
    O2          154.581     5.043e+06   2.632e-05
    CO2         304.128     7.377e+06   3.404e-05
    air         132.531     3.786e+06   2.008e-05

#### Maintenant construisons le graphe en nous basant sur la figure 1.3-1

```python
fluide='argon'
Tc=PropsSI('Tcrit',fluide)
Pc=PropsSI('Pcrit',fluide)
vc=PropsSI('V','T',Tc,'P',Pc,fluide)

T = np.logspace(-0.2, 1)
P = np.logspace(-0.3, 1.3979, 6)

vr = np.zeros((len(T),len(P)))
for i,Tr in enumerate(T):
    for j,Pr in enumerate(P):
        vr[i,j]=PropsSI('V','T',Tr*Tc,'P',Pr*Pc,fluide)/vc

with plt.xkcd():
    plt.rcParams['figure.figsize'] = 10,8
    plt.loglog(T,vr)
    plt.xlabel('temperature reduite')
    plt.ylabel('viscosite reduite')
    plt.grid(True,which="both")
    vr=[0.4,0.8,0.9,1,2,3,4,5,6,7,8,9,10,20]
    plt.xlim(T[0],T[len(T)-1])
    plt.ylim(np.amin(vr),np.amax(vr))
    plt.legend(['{:0.1f}'.format(p) for p in P])
    plt.title('Viscosite reduite en fonction de la temperature \n et de la pressions reduites')
plt.show()
```

#### Comparez avec la figure 1.3-1, vous verrez que les résultats sont très similaires. On peut donc utiliser CoolProp avec confiance sur une grande gamme de températures et de pressions pour gaz et liquides.

>>

# *

## Section 1.4, calcul des viscosités de gaz à des pressions inférieures à 10% de la pression critique (CoolProp et thermo)

La viscosité individuelle peut être à partir de l'équation 1.4-14 en utilisant l'annexe E2.

>>> $
\begin{equation*}
  \boxed{
          \mu = 2.6693 \times 10^{-5} \frac{\sqrt{MT}}{\sigma^2 \Omega_{\mu}}
           }
  \end{equation*}
$

> Cependant, on a vu CoolProp qui effectue ce calcul, maintenant regardons comment un autre package python (thermo) utilise lui-même CoolProp pour calculer la viscosité à partir de 1.4-14, et utilise une syntaxe qui peut sembler plus facile et ajoute les calculs de mélange de gaz comme l'équation 1.4-15, 1.4-16 (Wilkes).
>Pour un  mélange de gaz on peut approximer la viscosité en utilisant une pondération proposée par Wilkes en 1949.

>>> $
\begin{equation*}
  \boxed{
          \mu_{mix} = \sum_{i=0}^N \frac {x_i \mu_i} {\sum_{j=0}^N (x_j \phi_{ij})}
        }
\end{equation*}
$

avec

>>>
$ \begin{equation*}
  \boxed{
          \phi_{ij} = \frac {1} { \sqrt{8} }
                     \bigg ( 1+ \frac{M_i}{M_j} \bigg )^{-1/2}
                     \bigg [ 1+ \bigg ( \frac{\mu_i}{\mu_j} \bigg )^{1/2}
                     \bigg ( \frac{M_j}{M_i} \bigg )^{1/4}
                     \bigg ]^{2}
        }
        \end{equation*}
   $

 Voyons comment thermo résoud l'exemple 1.4-1. Il calcule les viscosités individuelles des 3 gaz en utilisant 1.4-14, 1.4-15 et 1.4-16. Le résultat est légèrement différent de celui de Transport Phenomena, pas moins précis.

```python
# package thermo
import thermo as th
#
# regardons d'abord comment thermo calcule la viscosité d'un gaz et comparons avec
# la syntaxe de Coolprop
thermoMu   = th.Chemical('CO2',300,101325).mu
coolPropMu = PropsSI('V','T',300,'P',101325,'CO2')
ExempleMu  = th.Mixture(['CO2','O2','N2'],zs=[0.133,0.039,0.828],T=293).mu

print('Avec thermo               : {:8.3e}'.format(thermoMu))
print('Avec Coolprop             : {:8.3e}'.format(coolPropMu))
print('Calcul de l exemple 1.4-1 : {:8.3e}'.format(ExempleMu))
```

    Avec thermo               : 1.502e-05
    Avec Coolprop             : 1.502e-05
    Calcul de l exemple 1.4-1 : 1.744e-05

# *

## Section 1.5, calcul des viscosités de liquides (CoolProp et thermo)

>> Avec thermo, le calcul des propriétés des liquides est très simple et très naturel, par exemple pour l'eau liquide ou la vapeur d'eau à 50 degrés C et pression atmosphérique (on utilise toujours des unités du système international dans thermo et CoolProp, donc P=101325 Pascals = 1 atmosphère):

```python
print(th.Chemical('water',273+50,101325).mug)
# une autre façon de faire le même travail, utilisant la nature objet du programme thermo
eau=th.Chemical('water',273+50,101325)
eau.mug
```

    1.0511439780362781e-05

    1.0511439780362781e-05

```python
print(th.Chemical('water',273+50,101325).mul)
# ou puisqu'on a déjà défini l'objet eau de la classe Chemical, avec les T et P précédents
eau.mul   # on a pas besoin de print, car la dernière ligne de chaque cellule est imprimée...
```

    0.0005478953637518872

    0.0005478953637518872

#### Comparons les valeurs obtenues avec thermo avec celles des équations proposées dans Transport Phenomena

```python
# viscosité du benzène, exemple 1.5-1
print(th.Chemical('benzene', T=273+20, P=101325).mul)
```

    0.0006482417530392874

#### Le résultat est bien meilleur, on peut le comparer avec le tableau 1.1-3, donc on utilisera donc thermo pour le calcul des viscosités des liquides. Continuons à comparer les valeurs prédites par thermo avec le tableau 1.1-3. Les méthodes utilisées par CoolProp et thermo pour calculer la viscosité des liquides sont beaucoup plus précises que celle proposée dans la section 1.5, alors on utilisera thermo.

```python
print('***Tableau 1.1-3 Transport Phenomena, à comparer\n')
#Nom, Nom Thermo, temperature (°C), pression (kPa)
elements=[
    ['brome','bromine',   25,101.325],
    ['C2H5OH  0°C','C2H5OH',    0,101.325],
    ['C2H5OH 25°C','C2H5OH',    25,101.325],
    ['C2H5OH 50°C','C2H5OH',    50,101.325],
    ['Mercure','mercury', 20,101.325]
]

for elem in elements:
    mul=th.Chemical(elem[1], T=273+elem[2], P=elem[3]*1000).mul
    print('{:<20}{:10.6e} Pa*s'.format(elem[0],mul))
```

    ***Tableau 1.1-3 Transport Phenomena, à comparer

    brome               9.755912e-04 Pa*s
    C2H5OH  0°C         1.824754e-03 Pa*s
    C2H5OH 25°C         1.085494e-03 Pa*s
    C2H5OH 50°C         6.907656e-04 Pa*s
    Mercure             1.558125e-03 Pa*s

# *

## Section 1.6- Calcul de la viscosité des suspensions.

En 1906, un jeune chercheur totalement inconnu de 27 ans propose une expression qui permet de calculer la viscosité de suspensions (diluées) de sphères solides dans un liquide. Cette expression se lit:

>>> $
\begin{equation*}
  \boxed{
          \frac {\mu_{eff}}{\mu_0}= 1 + \frac {5}{2}\phi
           }
  \end{equation*} (1.6-1)
$

En fait, il y avait une petite erreur de calcul, au lieu de 5/2 Einstein a donné 2 comme coefficient devant $\phi$. Il l'a corrigée quelques années plus tard, en 1911.

Cette équation a été utilisée comme base pour plusieurs travaux par la suite , celle de Mooney par exemple:

>>> $
\begin{equation*}
  \boxed{
          \frac {\mu_{eff}}{\mu_0}= exp \bigg(
                                      { \frac  {\frac {5}{2} \phi} {1-(\phi/\phi_0)} } \bigg )
           }
  \end{equation*} (1.6-2)
$

La cellule suivante effectue l'implémentation de la formule de Mooney et la compare avec un travail (Vand et al.) ou les auteurs ont lissé des résultats expérimentaux. C'est l'exercice 1B-3

```python
# Exercice 1B3
#
import sympy as sp
from IPython.display import *
from matplotlib import rcParams
%matplotlib inline

sp.init_printing(use_latex=True)
phi,phi0=sp.symbols('phi,phi_0')
muV=sp.Function('mu_V')(phi)
muV=1+2.5*phi+7.17*phi**2+16.2*phi**3
muM=sp.Function('mu_M')(phi)
muM=sp.exp((5./2.*phi)/(1-phi/phi0))
muM=muM.subs(phi0,0.7)
rcParams['lines.linewidth'] = 3
plt.rcParams['figure.figsize'] = 10,8
p1=sp.plot(muV,(phi,0,0.4),ylim=(1,5),legend=True,
           xlabel='$\phi$',ylabel='${\mu/\mu_0}$',show=False)
p2=sp.plot(muM,(phi,0,0.4),ylim=(1,5),show=False)
p1.append(p2[0])
p1[0].label='Vand, fit valide environ jusqu''à 0.5'
p1[0].line_color='red'
p1[1].label='Mooney'
p1[1].line_color='black'
p1.show()
```

> #### On constate que l'approximation de Mooney n'est valide que pour des valeurs de $\phi$ qui ne dépassent pas 25%, environ. Une suspension qui contient 25% par volume de solides est considérée habituellement comme très concentrée.

# *

### Section 1.7: Flux de quantité de mouvement.

>http://pierreproulx.espaceweb.usherbrooke.ca//images/GCH200_Ch1_resume.pdf


# メモ

https://github.com/drvinceknight/Python-Mathematics-Handbook

という github 上の jupyter で 3 年前に書かれている本の第2章が

Symbolic mathematics with Sympy (Sympy による数式処理)

と言う章になっている。

これを読む。

# Sympy による数式処理

Symbolic Mathematics with Sympy

Sympy による数式処理でできること

* 数式を扱う
* 数式を解く
* 数式を微積分する
* 数式をグラフにする

ほかにもいろいろできることがある。 sympy のホームページを参照。

```python
# sympy を使うための準備
from sympy import *
x = symbols ('x')
display(x)
display(x - x == 0)
display(x - x)
```

$\displaystyle x$

    True

$\displaystyle 0$

# sympy を使うための準備

sympy は import が必要。

変数も宣言が必要。 from sympy.abc import * という手もある。

print の代わりに display を使うと latex で表示してくれる。 数学においてシンボルは大切。

x に値を入れなくても x - x == 0 という演算が可能。

次の式を確かめてみよう。

$$(a + b) ^ 2 = a ^ 2 + 2ab + b ^2$$

```python
from sympy.abc import *
from sympy import *
expr = (a + b) ** 2
display(expr)
```

$\displaystyle \left(a + b\right)^{2}$

右辺のような入れ方をするとどうなるか。

```python
expr2 = a**2+2*a*b+b**2
display(expr2)
```

$\displaystyle a^{2} + 2 a b + b^{2}$

展開 expand したり、因数分解 factor したりできる。

```python
display(expand(expr))
```

$\displaystyle a^{2} + 2 a b + b^{2}$

```python
display(factor(expr2))
```

$\displaystyle \left(a + b\right)^{2}$

同等性を確かめる。

```python
from sympy.abc import *
from sympy import *
expr = (a + b) ** 2
expr2 = a**2+2*a*b+b**2
display(expr)
display(expr2)
display(expr == expr)
display(expr == expr2)
display(expr == expand(expr))
a=2;b=3
expr = (a + b) ** 2
expr2 = a**2+2*a*b+b**2
display(expr == expr2)
display(expr == expand(expr))
```

$\displaystyle \left(a + b\right)^{2}$

$\displaystyle a^{2} + 2 a b + b^{2}$

    True

    False

    False

    True

    True

上の実験からわかることは数式の形が違うと `==` は真にはならない。

値が入れば、当然 `==` が真になる。

latex という関数で latex 表現を得ることができる。

```python
from sympy.abc import *
from sympy import *
expr = (a + b) ** 2
latex(expand(expr))
```

    'a^{2} + 2 a b + b^{2}'

`a^{2} + 2 a b + b^{2}`
$$a^{2} + 2 a b + b^{2}$$

---
**練習問題** $\quad$ sympy を使って下記の等式を確認する。

* $(a - b) ^ 2 = a ^ 2 - 2 a b + b^2$
* $a ^ 2 - b ^ 2 = (a - b) (a + b)\quad$ (`expand` ではなく `factor` を使ってみる)

```python
from sympy.abc import *
from sympy import *
expr = (a-b)**2
display(expr)
display(expand(expr))
```

$\displaystyle \left(a - b\right)^{2}$

$\displaystyle a^{2} - 2 a b + b^{2}$

```python
from sympy.abc import *
from sympy import *
expr = a**2-b**2
display(expr)
display(factor(expr))
```

$\displaystyle a^{2} - b^{2}$

$\displaystyle \left(a - b\right) \left(a + b\right)$

# 数式を解く

sympy を使って $x$ について、次の 2次方程式 (quadratic equasion) を解く。

$$a x ^ 2 + b x + c = 0$$

```python
from sympy.abc import *
from sympy import *
expr = a * x ** 2 + b * x + c
display(solveset(expr,x))
```

sympy の `solveset` は引数を 2 つ取り、第 1 引数は数式で、第 2 引数は解く対象となる変数である。

---
**練習問題** $\quad$ sympy を使って 3 次方程式の一般解を求める。

$$a x ^ 3 + b x ^ 2 + c  x + d = 0$$

```python
from sympy.abc import *
from sympy import *
expr = a * x ** 3 + b * x**2 + c*x + d
display (expr)
display(solveset(expr,x))
```

$\displaystyle a x^{3} + b x^{2} + c x + d$

$\displaystyle \left\{- \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}, - \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \left(- \frac{1}{2} - \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\left(- \frac{1}{2} - \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}, - \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \left(- \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\left(- \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}\right\}$

 `solveset` に引数を足して、解の範囲 domain を指定することが可能である。

 次の式の domain を $\mathbb{R}$ として解いてみる。

$$x^2=-1$$

メモ $\quad$ domain は S.Reals とかで指定する。 S の書式は dir(S) で調べられる。黒板ボールドは `\mathbb` で表記できる。

```python
solveset(x ** 2 + 1, x, domain=S.Reals)
```

$\displaystyle \emptyset$

$\emptyset$ は空集合の意味である。

空集合は `\emptyset` `\varnothing` `\phi` で表される。$\emptyset, \varnothing, \phi$

---
**練習問題** $\quad$ sympy を使って次の方程式を解く

* $x ^ 2 = 2$ $\quad (x \in \mathbb{N})$
* $x ^ 3 + 2 x = 0$ $\quad (x \in \mathbb{R})$

```python
from sympy.abc import *
from sympy import *
expr = x**2 - 2
display (expr)
display(solveset(expr,x,domain=S.Naturals))
```

$\displaystyle x^{2} - 2$

$\displaystyle \emptyset$

```python
from sympy.abc import *
from sympy import *
expr = x**3 + 2*x
display (expr)
display(solveset(expr,x,domain=S.Reals))
```

$\displaystyle x^{3} + 2 x$

$\displaystyle \left\{0\right\}$

# 微分積分方程式
symbolic calculus

sympy で極限 limit を扱うことができる。

次の式を考えよう。

$$\lim_{x\to 0^+}\frac{1}{x}$$

```python
from sympy.abc import *
from sympy import *
display(limit(1/x, x, 0, dir="+"))
```

$\displaystyle \infty$

---
**練習問題** $\quad$ 次の極限を計算する。

1. $\displaystyle \lim_{x\to 0^-}\frac{1}{x}$
2. $\displaystyle \lim_{x\to 0}\frac{1}{x^2}$

```python
from sympy.abc import *
from sympy import *
display(limit(1/x, x, 0, dir="-"))
display(limit(1/x**2, x, 0))

```

$\displaystyle -\infty$

$\displaystyle \infty$

sympy を使って微分 differentiate/Derivative や積分 integrate/Integral ができる。

次の式を微分する。

$$x ^ 2 - \cos(x)$$

微分方程式は `Derivative` 、微分計算は `diff`

```python
from sympy.abc import *
from sympy import *
expr = x**2 - cos(x)
display(Derivative(x ** 2 - cos(x), x))
display(Derivative(expr, x))
display(diff(x ** 2 - cos(x), x))
display(diff(expr, x))
```

$\displaystyle \frac{d}{d x} \left(x^{2} - \cos{\left(x \right)}\right)$

$\displaystyle \frac{d}{d x} \left(x^{2} - \cos{\left(x \right)}\right)$

$\displaystyle 2 x + \sin{\left(x \right)}$

$\displaystyle 2 x + \sin{\left(x \right)}$

積分は Integral と integrate

```python
from sympy.abc import *
from sympy import *
expr = x**2 - cos(x)
display(expr)
display(Integral(expr, x))
display(integrate(expr, x))
integrate(x ** 2 - cos(x), x)
```

$\displaystyle x^{2} - \cos{\left(x \right)}$

$\displaystyle \int \left(x^{2} - \cos{\left(x \right)}\right)\, dx$

$\displaystyle \frac{x^{3}}{3} - \sin{\left(x \right)}$

$\displaystyle \frac{x^{3}}{3} - \sin{\left(x \right)}$

定積分は次のように行う。

```python
from sympy.abc import *
from sympy import *
expr = x**2 - cos(x)
display(Integral(expr, (x, 0, 5)))
display(integrate(expr,  (x, 0, 5)))
integrate(x ** 2 - cos(x), (x, 0, 5))
```

$\displaystyle \int\limits_{0}^{5} \left(x^{2} - \cos{\left(x \right)}\right)\, dx$

$\displaystyle \frac{125}{3} - \sin{\left(5 \right)}$

$\displaystyle \frac{125}{3} - \sin{\left(5 \right)}$

---

**練習問題** $\quad$ sympy を使って下記の式を計算する。

1. $\displaystyle \frac{d\sin(x ^2)}{dx}$
2. $\displaystyle \frac{d(x ^2 + xy - \ln(y))}{dy}$
3. $\displaystyle \int e^x \cos(x)\;dx$
4. $\displaystyle \int_0^5 e^{2x}\;dx$

```python
from sympy.abc import *
from sympy import *
expr = sin (x**2)
display(Derivative(expr, x))
display(diff(expr,  x))

```

```python
from sympy.abc import *
from sympy import *
expr = x**2+x*y-log(y)
display(Derivative(expr, y))
display(diff(expr,  y))
```

```python
from sympy.abc import *
from sympy import *
expr = exp(x)*cos(x)
display(expr)
display(Integral(expr, x))
display(integrate(expr,  x))
```

$\displaystyle e^{x} \cos{\left(x \right)}$

$\displaystyle \int e^{x} \cos{\left(x \right)}\, dx$

$\displaystyle \frac{e^{x} \sin{\left(x \right)}}{2} + \frac{e^{x} \cos{\left(x \right)}}{2}$

```python
from sympy.abc import *
from sympy import *
expr = exp(2*x)
display(expr)
display(Integral(expr, (x,0,5)))
display(integrate(expr, (x,0,5)))
```

$\displaystyle e^{2 x}$

$\displaystyle \int\limits_{0}^{5} e^{2 x}\, dx$

$\displaystyle - \frac{1}{2} + \frac{e^{10}}{2}$

# グラフ描画

plotting

メモ $\quad$ ここで注意。 sympy で簡易に例えば $y=x^2$ のグラフが描けるが、グラフは本来座標の範囲を決めたり、ゼロをどこに置くか、目盛りをどうするかなど面倒な設定が必要なところを裏で処理している。

裏で動いているのが matplotlib (http://matplotlib.org/)という python のライブラリーだが、これも簡易な使い方と細かく設定して使う方法がある。

sympy でグラフ描画する際には matplotlib のどの機能を使っているとか、意識しながら使うのがよいと思う。

numpy と matplotlib を明示的に使う

$x^2$ のグラフを描く

```python
from sympy.abc import *
from sympy import *
expr = x ** 2
p = plot(expr);
```

matplotlib と numpy では次のように描く。

```python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-10, 10, 100) # 100に刻む
fig, ax = plt.subplots()  # figure と ax を作る
ax.spines['left'].set_position(('data', 0))
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_position(('data', 0))
ax.set_xlabel('x')  # x軸のラベル
ax.set_ylabel('f(x)')  # y軸のラベル
ax.plot(x, x**2)  # プロットする
plt.show()
```

メモ $\quad$ xlabel や ylabel の位置はどうやって指示するのか。

座標の位置を変えるために spines を設定するのは手間が多い。

テキストには pdf で保存する方法が書いてある。

```python
p.save("x_squared.pdf")
```

メモ $\quad$ `p.save("x_squared.pdf")` で pdf を作ると同時に画面上に描画をしている。

numpy+matplotlib の `plt.savefig("temp.svg", format="svg")` も同じだった!!!!

```python
print(type(p) )
print(type(ax))
print(len(dir(p)))
print(len(dir(ax)))
```

    <class 'sympy.plotting.plot.Plot'>
    <class 'matplotlib.axes._subplots.AxesSubplot'>
    54
    449

上の p と ax の dir を取ってみると圧倒的に ax の方が機能が多い。

そういうことなのだろう。

---
**練習問題** $\quad$ 次の関数をグラフ描画する。

* $y=x + cos(x)$
* $y=x ^ 2 - e^x$ (you might find `ylim` helpful as an argument)

```python
from sympy.abc import *
from sympy import *
expr = x + cos(x)
p = plot(expr)
```

同じグラフを numpy+matplotlib で描く。

```python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-10, 10, 100) # 100に刻む
fig, ax = plt.subplots()  # figure と ax を作る
ax.spines['left'].set_position(('data', 0))
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_position(('data', 0))
ax.set_xlabel('x')  # x軸のラベル
ax.set_ylabel('f(x)')  # y軸のラベル
ax.plot(x, x + np.cos(x))  # cos は numpy の cos を使うので注意
plt.show()
```

```python
from sympy.abc import *
from sympy import *
expr = x**2 - exp(x)
p = plot(expr, xlim=(-1,10))
```

同じグラフを numpy+matplotlib で描く。

```python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-1, 10, 100) # 100に刻む
fig, ax = plt.subplots()  # figure と ax を作る
ax.spines['left'].set_position(('data', 0))
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_position(('data', 0))
ax.set_xlabel('x')  # x軸のラベル
ax.set_ylabel('f(x)')  # y軸のラベル
ax.plot(x, x**2 - np.exp(x))  # exp は numpy の exp を使うので注意
plt.show()
```


Table of Contents

1  Collaborative Filtering for Implicit Feedback Datasets

1.1  Formulation

1.2  Alternating Least Squares

1.3  Implementation

2  Ranking Metrics

2.1  Mean Average Precision

2.2  NDCG

3  Reference

```python
# code for loading the format for the notebook
import os

# path : store the current path to convert back to it later
path = os.getcwd()
os.chdir(os.path.join('..', 'notebook_format'))
from formats import load_style
load_style(css_style = 'custom2.css', plot_style = False)
```

```python
os.chdir(path)

# 1. magic to print version
# 2. magic so that the notebook will reload external python modules
%load_ext watermark
%load_ext autoreload
%autoreload 2

import numpy as np
import pandas as pd
from math import ceil
from tqdm import trange
from subprocess import call
from scipy.sparse import csr_matrix, dok_matrix

%watermark -a 'Ethen' -d -t -v -p numpy,pandas,sklearn,tqdm,scipy
```

    Ethen 2018-04-07 14:28:04

    CPython 3.6.4
    IPython 6.2.1

    numpy 1.14.2
    pandas 0.22.0
    sklearn 0.19.1
    tqdm 4.19.8
    scipy 1.0.0

# Collaborative Filtering for Implicit Feedback Datasets

One common scenario in real-world recommendation system is we only have **implicit** instead of **explicit** user-item interaction data. To elaborate on this a little bit more, a user may be searching for an item on the web, or listening to songs. Unlike a rating data, where we have direct access to the user's preference towards an item, these type of actions do not **explicitly** state or quantify any preference of the user for the item, but instead gives us **implicit confidence** about the user's opinion.

Even when we do have explicit data, it might still be a good idea to incorporate implicit data into the model. Consider, for example, listening to songs. When users listen to music on a streaming service, they might rarely ever rate a song that he/she like or dislike. But more often they skip a song, or listen only halfway through it if they dislike it. If the user really liked a song, they will often come back and listen to it. So, to infer a user's musical taste profile, their listens, repeat listens, skips and fraction of tracks listened to, etc. might be far more valuable signals than explicit ratings.

## Formulation

Recall from the previous notebook that the loss function for training the recommendation model on explicit feedback data was:

$$
\begin{align}
L_{explicit} &= \sum\limits_{u,i \in S}( r_{ui} - x_{u} y_{i}^{T} )^{2} + \lambda \big( \sum\limits_{u} \left\Vert x_{u} \right\Vert^{2} + \sum\limits_{i} \left\Vert y_{i} \right\Vert^{2} \big)
\end{align}
$$

Where:

- $r_{ui}$ is the true rating given by user $u$ to the item $i$
- $x_u$ and $y_i$ are user u's and item i's latent factors, both are $1×d$ dimensional, where $d$ the number of latent factors that the user can specify
- $S$ was the set of all user-item ratings
- $\lambda$ controls the regularization strength that prevents overfitting the user and item vectors

To keep it concrete, let's assume we're working music data and the value of our $r_{ui}$ will consists of implicit ratings that counts the number of times a user has listened to a song (song listen count). Then new formulation becomes:

$$
\begin{align}
L_{implicit} &= \sum\limits_{u,i} c_{ui}( p_{ui} - x_{u} y_{i}^{T} )^2 + \lambda \big( \sum\limits_{u} \left\Vert x_{u} \right\Vert^{2} + \sum\limits_{i} \left\Vert y_{i} \right\Vert^{2} \big)
\end{align}
$$

Recall that with implicit feedback, we do not have ratings anymore; rather, we have users' preferences for items. Therefore, in the new loss function, the ratings $r_{ui}$ has been replaced with a preference $p_{ui}$ indicating the preference of user $u$ to item $i$. $p_{ui}$ is a set of binary variables and is computed by binarizing $r_{ui}$.

$$
\begin{align}
p_{ui} &= \begin{cases} 1 &\mbox{if } r_{ui} > 0 \\ 0 & \mbox{otherwise} \end{cases}
\end{align}
$$

We make the assumption that if a user has interacted at all with an item ($r_{ui} > 0$), then we set $p_{ui} = 1$ to indicate that user $u$ has a liking/preference for item $i$. Otherwise, we set $p_{ui} = 0$. However, these assumptions comes with varying degrees of confidence. First of all, when $p_{ui} = 0$, we assume that it should be associated with a lower confidence, as there are many reasons beyond disliking the item as to why the user has not interacted with it. e.g. Unaware of it's existence. On the other hand, as the number of implicit feedback, $r_{ui}$,   grows, we have a stronger indication that the user does indeed like the item (regardless of whether he/she if buying a gift for someone else). So to measure the level of confidence mentioned above, we introduce another set of variables $c_{ui}$ that measures our confidence in observing $p_{ui}$:

$$
\begin{align}
c_{ui} = 1 + \alpha r_{ui}
\end{align}
$$

Where the 1 ensures we have some minimal confidence for every user-item pair, and as we observe more and more implicit feedback (as $r_{ui}$ gets larger and larger), our confidence in $p_{ui} = 1$ increases accordingly. The term $\alpha$ is a parameter that we have to specify to control the rate of the increase. This formulation makes intuitive sense when we look back at the $c_{ui}( p_{ui} - x_{u} y_{i}^{T} )^2$ term in the loss function. A larger $c_{ui}$ means that the prediction $x_{u} y_{i}^{T}$ has to be that much closer to $p_{ui}$ so that term will not contribute too much to the total loss.

The implementation in the later section will be based on the formula above, but note that there are many ways in which we can tune the formulation above. For example, we can derive $p_{ui}$ from $r_{ui}$ differently. So instead of setting the binary cutoff at 0, we can set it at another threshold that we feel is appropriate for the domain. Similarly, there are many ways to transform $r_{ui}$ into the confidence level $c_{ui}$. e.g. we can use:

$$
\begin{align}
c_{ui} = 1 + \alpha log \left( 1 + r_{ui} / \epsilon \right)
\end{align}
$$

Regardless of the scheme, it's important to realize that we are transforming the raw observation $r_{ui}$ into two distinct representation, the preference $p_{ui}$ and the confidence levels of the preference $c_{ui}$.

## Alternating Least Squares

Let's assume we have $m$ users and $n$ items. Now, to solve for the loss function above, we start by treating $y_i$ as constant and solve the loss function with respect to $x_u$. To do this, we rewrite and expand the first term in the loss function (excluding the regularization terms), $\sum\limits_{u,i} c_{ui}( p_{ui} - x_{u} y_{i}^{T} )^2$ part as:

$$
\begin{align}
\sum\limits_{u,i} c_{ui}( p_{ui} - x_{u} y_{i}^{T} )^2
&= \sum_u c_u( p_u^T - x_u Y^T )^2 \\
&= \sum_u p_u^T C^u p_u - 2 x_u Y^T C^u p_u + x_u Y^T C^u Y x_u^T
\end{align}
$$

Where:

- $Y \in \mathbb{R}^{n \times d}$ represents all item row vectors vertically stacked on each other
- $p_u \in \mathbb{R^{n \times 1}}$ contains element all of the preferences of the user
- The diagonal matrix $C^u \in \mathbb{R^{n \times n}}$ consists of $c_{ui}$ in row/column $i$, which is the user's confidence across all the items. e.g. if $u = 0$ then the matrix for user $u_0$ will look like:

$$
\begin{align}
{C}^{u_0} = \begin{bmatrix} c_{u_{01}} & 0 & 0 & 0 & ... & 0 \\ 0 & c_{u_{02}} & 0 & 0 & ... &0\\ ... \\ ... \\ 0 & 0 & 0 & 0 & ... & c_{u_{0n}}\end{bmatrix}
\end{align}
$$

The formula above can also be used to monitor the loss function at each iteration. If we set $A = Y^T C^u Y$ and $b = Y^T C^u$, the last two terms can be rewritten as $(A x_u^T - 2b p_u) x_u$. As for the first term $p_u^T C^u p_u$ we can leverage the fact that $p_u$ is 1 for all positive items, and just sum the confidence term $C^u$.

Now for the derivation of the partial derivative.

$$
\begin{align}
\frac{\partial L_{implicit}}{\partial x_u}
&= -2 Y^T C^u p_u + 2 Y^T C^u Y x_u + 2 \lambda x_u = 0 \\
&= (Y^T C^u Y + \lambda I)x_u = Y^T C^u p_{u} \\
&= x_u = (Y^T C^u Y + \lambda I)^{-1} Y^T C^u p_u
\end{align}
$$

The main computational bottleneck in the expression above is the need to compute $Y^T C^u Y$ for every user. Speedup can be obtained by re-writing the expression as:

$$
\begin{align}
{Y}^T {C}^{u} {Y} &= Y^T Y + {Y}^T \left( C^u - I \right) Y
\end{align}
$$

Now the term $Y^T Y$ becomes independent of each user and can be computed independently, next notice $\left(C^u - I \right)$ has only $n_u$ non-zero elements, where $n_u$ is the number of items for which $r_{ui} > 0$. Similarly, $C^u p_u$ contains only $n_u$ non-zero elements since $p_u$ is a binary transformation of $r_{ui}$. Thus the final formulation becomes:

$$
\begin{align}
\frac{\partial L_{implicit}}{\partial x_u}
&= x_u = (Y^T Y + Y^T \left( C^u - I \right) Y + \lambda I)^{-1} Y^T C^u p_u
\end{align}
$$

After solving for $x_u$ the same procedure can be carried out to solve for $y_i$ giving a similar expression:

$$
\begin{align}
\frac{\partial L_{implicit}}{\partial y_i}
&= y_i = (X^T X + X^T \left( C^i - I \right) X + \lambda I)^{-1} X^T C^i p_i
\end{align}
$$

## Implementation

We'll use the same movielens dataset like the previous notebook. The movielens data is not an implicit feedback dataset as the user did provide explicit ratings, but we will use it for now to test out our implementation. The overall preprocessing procedure of loading the data and doing the train/test split is the same as the previous notebook. But here we'll do it in a sparse matrix fashion.

```python
file_dir = 'ml-100k'
file_path = os.path.join(file_dir, 'u.data')
if not os.path.isdir(file_dir):
    call(['curl', '-O', 'http://files.grouplens.org/datasets/movielens/' + file_dir + '.zip'])
    call(['unzip', file_dir + '.zip'])

names = ['user_id', 'item_id', 'rating', 'timestamp']
df = pd.read_csv(file_path, sep = '\t', names = names)
print('data dimension: \n', df.shape)
df.head()
```

    data dimension:
     (100000, 4)

      user_id
      item_id
      rating
      timestamp

      0
      196
      242
      3
      881250949

      1
      186
      302
      3
      891717742

      2
      22
      377
      1
      878887116

      3
      244
      51
      2
      880606923

      4
      166
      346
      1
      886397596

```python
def create_matrix(data, user_col, item_col, rating_col):
    """
    creates the sparse user-item interaction matrix

    Parameters
    ----------
    data : DataFrame
        implicit rating data

    user_col : str
        user column name

    item_col : str
        item column name

    ratings_col : str
        implicit rating column name

    Returns
    -------
    ratings : scipy sparse csr_matrix [n_users, n_items]
        user/item ratings matrix

    data : DataFrame
        the implict rating data that retains only the positive feedback
        (if specified to do so)
    """
    # map each item and user to a unique numeric value
    for col in (item_col, user_col):
        data[col] = data[col].astype('category')

    # create a sparse matrix of using the (rating, (rows, cols)) format
    rows = data[user_col].cat.codes
    cols = data[item_col].cat.codes
    rating = data[rating_col]
    ratings = csr_matrix((rating, (rows, cols)))
    ratings.eliminate_zeros()
    return ratings, data
```

```python
user_col = 'user_id'
item_col = 'item_id'
rating_col = 'rating'
X, df = create_matrix(df, user_col, item_col, rating_col)
X
```

    <943x1682 sparse matrix of type '<class 'numpy.int64'>'
    	with 100000 stored elements in Compressed Sparse Row format>

The following train and test set split function is assuming that you're doing a train/test split using the current dataset. Though it's probably better to use time to perform the train/test split. For example, using the year 2016's data as training and the 1 first month of 2017's data as testing.

```python
def create_train_test(ratings, test_size = 0.2, seed = 1234):
    """
    split the user-item interactions matrix into train and test set
    by removing some of the interactions from every user and pretend
    that we never seen them

    Parameters
    ----------
    ratings : scipy sparse csr_matrix
        The user-item interactions matrix

    test_size : float between 0.0 and 1.0, default 0.2
        Proportion of the user-item interactions for each user
        in the dataset to move to the test set; e.g. if set to 0.2
        and a user has 10 interactions, then 2 will be moved to the
        test set

    seed : int, default 1234
        Seed for reproducible random splitting the
        data into train/test set

    Returns
    -------
    train : scipy sparse csr_matrix
        Training set

    test : scipy sparse csr_matrix
        Test set
    """
    assert test_size < 1.0 and test_size > 0.0

    # Dictionary Of Keys based sparse matrix is more efficient
    # for constructing sparse matrices incrementally compared with csr_matrix
    train = ratings.copy().todok()
    test = dok_matrix(train.shape)

    # 1. for all the users assign randomly chosen interactions
    # to the test and assign those interactions to zero in the training;
    # when computing the interactions to go into the test set,
    # remember to round up the numbers (e.g. a user has 4 ratings, if the
    # test_size is 0.2, then 0.8 ratings will go to test, thus we need to
    # round up to ensure the test set gets at least 1 rating);
    # 2. note that we can easily the parallelize the for loop if we were to
    # aim for a more efficient implementation
    rstate = np.random.RandomState(seed)
    for u in range(ratings.shape[0]):
        split_index = ratings[u].indices
        n_splits = ceil(test_size * split_index.shape[0])
        test_index = rstate.choice(split_index, size = n_splits, replace = False)
        test[u, test_index] = ratings[u, test_index]
        train[u, test_index] = 0

    train, test = train.tocsr(), test.tocsr()
    return train, test
```

```python
seed = 1234
test_size = 0.2
X_train, X_test = create_train_test(X, test_size, seed)
X_train
```

    <943x1682 sparse matrix of type '<class 'numpy.int64'>'
    	with 79619 stored elements in Compressed Sparse Row format>

The following implementation uses some tricks to speed up the procedure. First of all, when we need to solve $Ax = b$ where $A$ is an $n \times n$ matrix, a lot of books might write the solution as $x = A^{-1} b$, however, in practice there is hardly ever a good reason to calculate that it that way as solving the equation $Ax = b$ is faster than finding $A^{-1}$.

The next one is the idea of computing matrix product $X^T X$ using a [outer product](https://docs.scipy.org/doc/numpy/reference/generated/numpy.outer.html) of each row.

```python
# example matrix
X = np.array([[9, 3, 5], [4, 1, 2]]).T
X
```

    array([[9, 4],
           [3, 1],
           [5, 2]])

```python
# normal matrix product
X.T.dot(X)
```

    array([[115,  49],
           [ 49,  21]])

```python
# intialize an empty array
end_result = np.zeros((2, 2))

# loop through each row add up the outer product
for i in range(X.shape[0]):
    out = np.outer(X[i], X[i])
    end_result += out
    print('row:\n', X[i])
    print('outer product of row:\n', out)

end_result
```

    row:
     [9 4]
    outer product of row:
     [[81 36]
     [36 16]]
    row:
     [3 1]
    outer product of row:
     [[9 3]
     [3 1]]
    row:
     [5 2]
    outer product of row:
     [[25 10]
     [10  4]]

    array([[115.,  49.],
           [ 49.,  21.]])

The reason why this can speed things up is that the matrix product is now the sum of the outer products of the rows, where each row's computation is independent of another can be computed in the parallelized fashion then added back together!

Last but not least, is exploiting the property of scipy's [Compressed Sparse Row Matrix](https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.sparse.csr_matrix.html) to access the non-zero elements. For those that are unfamiliar with it, the following link has a pretty decent quick introduction. [Blog: Empty rows in sparse arrays](http://mike.place/2015/sparse/).

```python
class ALSWR:
    """
    Alternating Least Squares with Weighted Regularization
    for implicit feedback

    Parameters
    ----------
    n_iters : int
        number of iterations to train the algorithm

    n_factors : int
        number/dimension of user and item latent factors

    alpha : int
        scaling factor that indicates the level of confidence in preference

    reg : int
        regularization term for the user and item latent factors

    seed : int
        seed for the randomly initialized user, item latent factors

    Reference
    ---------
    Y. Hu, Y. Koren, C. Volinsky Collaborative Filtering for Implicit Feedback Datasets
    http://yifanhu.net/PUB/cf.pdf
    """
    def __init__(self, n_iters, n_factors, alpha, reg, seed):
        self.reg = reg
        self.seed = seed
        self.alpha = alpha
        self.n_iters = n_iters
        self.n_factors = n_factors

    def fit(self, ratings):
        """
        ratings : scipy sparse csr_matrix [n_users, n_items]
            sparse matrix of user-item interactions
        """
        # the original confidence vectors should include a + 1,
        # but this direct addition is not allowed when using sparse matrix,
        # thus we'll have to deal with this later in the computation
        Cui = ratings.copy().tocsr()
        Cui.data *= self.alpha
        Ciu = Cui.T.tocsr()
        self.n_users, self.n_items = Cui.shape

        # initialize user latent factors and item latent factors
        # randomly with a specified set seed
        rstate = np.random.RandomState(self.seed)
        self.user_factors = rstate.normal(size = (self.n_users, self.n_factors))
        self.item_factors = rstate.normal(size = (self.n_items, self.n_factors))

        for _ in trange(self.n_iters, desc = 'training progress'):
            self._als_step(Cui, self.user_factors, self.item_factors)
            self._als_step(Ciu, self.item_factors, self.user_factors)

        return self

    def _als_step(self, Cui, X, Y):
        """
        when solving the user latent vectors,
        the item vectors will be fixed and vice versa
        """
        # the variable name follows the notation when holding
        # the item vector Y constant and solving for user vector X

        # YtY is a d * d matrix that is computed
        # independently of each user
        YtY = Y.T.dot(Y)
        data = Cui.data
        indptr, indices = Cui.indptr, Cui.indices

        # for every user build up A and b then solve for Ax = b,
        # this for loop is the bottleneck and can be easily parallized
        # as each users' computation is independent of one another
        for u in range(self.n_users):
            # initialize a new A and b for every user
            b = np.zeros(self.n_factors)
            A = YtY + self.reg * np.eye(self.n_factors)

            for index in range(indptr[u], indptr[u + 1]):
                # indices[index] stores non-zero positions for a given row
                # data[index] stores corresponding confidence,
                # we also add 1 to the confidence, since we did not
                # do it in the beginning, when we were to give every
                # user-item pair and minimal confidence
                i = indices[index]
                confidence = data[index] + 1
                factor = Y[i]

                # for b, Y^T C^u p_u
                # there should be a times 1 for the preference
                # Pui = 1
                # b += confidence * Y[i] * Pui
                # but the times 1 can be dropped
                b += confidence * factor

                # for A, Y^T (C^u - I) Y
                A += (confidence - 1) * np.outer(factor, factor)

            X[u] = np.linalg.solve(A, b)

        return self

    def predict(self):
        """predict ratings for every user and item"""
        prediction = self.user_factors.dot(self.item_factors.T)
        return prediction

    def _predict_user(self, user):
        """
        returns the predicted ratings for the specified user,
        this is mainly used in computing evaluation metric
        """
        user_pred = self.user_factors[user].dot(self.item_factors.T)
        return user_pred
```

```python
als = ALSWR(n_iters = 15, n_factors = 20, alpha = 15, reg = 0.01, seed = 1234)
als.fit(X_train)
```

    training progress: 100%|██████████| 15/15 [00:28<00:00,  1.93s/it]

    <__main__.ALSWR at 0x1079186d8>

# Ranking Metrics

Now that we've built our recommendation engine, the next important thing to do is to evaluate our model's offline performance.

Let's say that there are some users and some items, like movies, songs or jobs. Each user might be interested in some items. The client asks us to recommend a few items (the number is x) for each user. In other words, what we're after is the top-N recommendation for each user and after recommending these items to the user, we need a way to measure whether the recommendation is useful or not. One key thing to note is that metrics such as RMSE might not be the best at assessing the quality of recommendations because the training focused on items with the most ratings, achieving a good fit for those. The items with few ratings don't mean much in terms of their impact on the loss. As a result, predictions for them will be off.

Long story short, we need metrics specifically crafted for ranking evaluation and the two most popular ranking metrics are **MAP (Mean Average Precision)** and **NDCG (Normalized Discounted Cumulative Gain)**. The main difference between the two is that MAP assumes binary relevance (an item is either of interest or not, e.g. a user clicked a link, watched a video, purchased a product), while NDCG can be used in any case where we can assign relevance score to a recommended item (binary, integer or real). The relationship is just like classification and regression.

## Mean Average Precision

For this section of the content, a large portion is based on the excellent blog post at the following link. [Blog: What you wanted to know about Mean Average Precision](http://fastml.com/what-you-wanted-to-know-about-mean-average-precision/). This documentation builds on top of it by carrying out the educational implementation.

Let's say that there are some users and some items, like movies, songs or jobs. Each user might be interested in some items. The client asks us to recommend a few items (the number is x) for each user. After recommending the items to the user, we need a way to measure whether the recommendation is useful or not. One way to do this is using **MAP@k (Mean Average Precision at k)** .

The intuition behind this evaluation metric is that:

- We can recommend at most k items for each user (this is essentially the `@k` part), but we will be penalized for bad recommendations
- Order matters, so it's better to submit more certain recommendations first, followed by recommendations we are less sure about

Diving a bit deeper, we will first ignore `@k` and get M out of the way. MAP is just the mean of APs, or average precision, for all users. If we have 1000 users, we sum APs for each user and divide the sum by 1000. This is MAP. So now, what is AP, or average precision?

One way to understand average precision is this way: we type something in Google and it shows us 10 results. It's probably best if all of them were relevant. If only some are relevant, say five of them, then it's much better if the relevant ones are shown first. It would be bad if first five were irrelevant and good ones only started from sixth, wouldn't it? The formula for computing AP is:

sum i=1:k of (precision at i * change in recall at i)

Where precision at i is a percentage of correct items among first i recommendations. Change in recall is 1/k if item at i is correct (for every correct item), otherwise zero. Note that this is base on the assumption that the number of relevant items is bigger or equal to k: r >= k. If not, change in recall is 1/r for each correct i instead of 1/k.

For example, If the actual items were [1 2 3 4 5] and we recommend [6 4 7 1 2]. In this case we get 4, 1 and 2 right, but with some incorrect guesses in between. Now let's say we were to compute AP@2, so only two first predictions matter: 6 and 4. First is wrong, so precision@1 is 0. Second is right, so precision@2 is 0.5. Change in recall is 0 and 0.5 (that's 1/k) respectively, so AP@2 = 0 * 0 + 0.5 * 0.5 = 0.25

```python
def compute_apk(y_true, y_pred, k):
    """
    average precision at k, y_pred is assumed
    to be truncated to length k prior to feeding
    it to the function
    """
    # convert to set since membership
    # testing in a set is vastly faster
    actual = set(y_true)

    # precision at i is a percentage of correct
    # items among first i recommendations; the
    # correct count will be summed up by n_hit
    n_hit = 0
    precision = 0
    for i, p in enumerate(y_pred, 1):
        if p in actual:
            n_hit += 1
            precision += n_hit / i

    # divide by recall at the very end
    avg_precision = precision / min(len(actual), k)
    return avg_precision
```

```python
# example 1

# y_true, is the true interaction of a user
# and y_pred is the recommendation we decided
# to give to the user
k = 2
y_true = np.array([1, 2, 3, 4, 5])
y_pred = np.array([6, 4, 7, 1, 2])
compute_apk(y_true, y_pred[:k], k) # 0.25
```

    0.25

```python
# example 2

k = 5
y_true = np.array([1, 2])
y_pred = np.array([6, 4, 7, 1, 2])
compute_apk(y_true, y_pred[:k], k) # 0.325
```

    0.325

After computing the average precision for this individual user, we then compute this for every single user and take the mean of these values, then that would essentially be our MAP@k, mean average precision at k. For this metric, the bigger the better.

```python
def mapk_score(model, ratings, k):
    """
    mean average precision at rank k for the ALS model

    Parameters
    ----------
    model : ALSWR instance
        fitted ALSWR model

    ratings : scipy sparse csr_matrix [n_users, n_items]
        sparse matrix of user-item interactions

    k : int
        mean average precision at k's k

    Returns
    -------
    mapk : float
        the mean average precision at k's score
    """
    # compare the top k predictions' index to the actual index,
    # the model is assumed to have the _predict_user method
    mapk = 0
    n_users = ratings.shape[0]
    for u in range(n_users):
        y_true = ratings[u].indices
        u_pred = model._predict_user(u)
        y_pred = np.argsort(u_pred)[::-1][:k]
        mapk += compute_apk(y_true, y_pred, k)

    mapk /= n_users
    return mapk
```

```python
k = 5
mapk_train = mapk_score(als, X_train, k)
mapk_test = mapk_score(als, X_test, k)
print('mapk training:', mapk_train)
print('mapk testing:', mapk_test)
```

    mapk training: 0.20038882997525595
    mapk testing: 0.05916489925768833

Note that it's normal for this metric to be low. We can compare this metric with a baseline to get a sense of how well the algorithm is performing. And a nice baseline for recommendation engine is to simply recommend every user the most popular items (items that has the most user interaction)

## NDCG

Suppose that on a four-point scale, we give a 0 score for an irrelevant result, 1 for a partially relevant, 2 for relevant, and 3 for perfect. Suppose also that a query is judged by one of our judges, and the first four results that the search engine returns are assessed as relevant (2), irrelevant (0), perfect (3), and relevant (2) by a judge.

The idea behind **NDCG** is: A recommender returns some items and we'd like to compute how good the list is. Each item has a relevance score, usually a non-negative number. That's **gain (G)**. For items we don't have user feedback for we usually set the gain to zero. Now we add up those scores, that's **cumulative gain (CG)**. So, the cumulative gain for the four results is the sum of the scores for each result: 2 + 0 + 3 + 2 = 7. In mathematical notations, the CG at a particular rank ${\displaystyle k}$ is defined as:

\begin{align}
CG_k = \sum_{i=1}^k rel_i
\end{align}

Where $rel_i$ is the graded relevance of the result at position ${\displaystyle i}$. As we can see from the formula, the value computed with this function is unaffected by changes in the ordering of search results, thus DCG is used in place of CG for a more accurate measure about the usefulness of results' ranking.

When evaluating rankings, we’d prefer to see the most relevant items at the top of the list, i.e the first result in our search results is more important than the second, the second more important than the third, and so on. Therefore before summing the scores we divide each by a growing number, which is the **discount (D)**. One simple way to make position one more important than two (and so on) is to divide each score by the rank. So, for example, if the third result is "great", its contribution is $3 / 3 = 1$ (since the score for "great" is 3 , and the rank of the result is 3). If "great" were the first result, its contribution would be 3 / 1 = 3. Though in practice, it's more common to discount it using a logarithm of the item position, giving us:

\begin{align}
DCG_k = \sum_{i=1}^k \frac{rel_i}{\log_2{\left(i+1\right)}}
\end{align}

```python
def dcg_at_k(score, k = None):
    """
    discounted cumulative gain (dcg)

    Parameters
    ----------
    score : 1d nd.array
        ranking/relevance score

    k : int, default None
        evaluate the measure for the top-k ranking score,
        default None evaluates all

    Returns
    -------
    dcg: float
    """
    if k is not None:
        score = score[:k]

    discounts = np.log2(np.arange(2, score.size + 2))
    dcg = np.sum(score / discounts)
    return dcg

score = np.array([2, 0, 3, 2])
dcg_at_k(score)
```

    4.361353116146786

There's an alternative formulation of DCG that places stronger emphasis on retrieving relevant documents:

\begin{align}
DCG_k = \sum_{i=1}^k \frac{2^{rel_i} - 1}{\log_2{\left(i+1\right)}}
\end{align}

This formula is commonly used in industry including major web search companies and data science competition platform such as Kaggle.

```python
def dcg_at_k(score, k = None):
    """
    discounted cumulative gain (dcg)

    Parameters
    ----------
    score : 1d nd.array
        ranking/relevance score

    k : int, default None
        evaluate the measure for the top-k ranking score,
        default None evaluates all

    Returns
    -------
    dcg: float
    """
    if k is not None:
        score = score[:k]

    gain = 2 ** score - 1
    discounts = np.log2(np.arange(2, score.size + 2))
    dcg = np.sum(gain / discounts)
    return dcg

score = np.array([2, 0, 3, 2])
dcg_at_k(score)
```

    7.79202967422018

The final touch to this metric is **Normalized (N)**. It's not fair to compare DCG values across queries because some queries are easier than others or result lists vary in length depending on the query, so we normalize them by: First, figure out what the best ranking score is for this result and compute DCG for that, then we divide the raw DCG by this ideal DCG to get NDCG@K, a number between 0 and 1. In our previous example, we had 2, then 0, 3, and a 2. The best arrangement of these same results would have been: 3, 2, 2, 0, that is, if the "great" result had been ranked first, followed by the two "relevant" ones, and then the "irrelevant". So we compute the DCG score for the rank 3, 2, 2, 0 to obtain our ideal DCG (IDCG) and simply perform:

\begin{align}
NDCG_k = \frac{DCG_k}{IDCG_k}
\end{align}

to obtain our final ndcg.

```python
def ndcg_at_k(score, k = None):
    """
    normalized discounted cumulative gain (ndcg)

    Parameters
    ----------
    score : 1d nd.array
        ranking/relevance score

    k : int, default None
        evaluate the measure for the top-k ranking score,
        default None evaluates all

    Returns
    -------
    ndcg: float, 0.0 ~ 1.0
    """
    actual_dcg = dcg_at_k(score, k)
    sorted_score = np.sort(score)[::-1]
    best_dcg = dcg_at_k(sorted_score, k)
    ndcg = actual_dcg / best_dcg
    return ndcg

ndcg_at_k(score)
```

    0.7497534568197889

The next section modifies the function API a little bit so it becomes more suitable for evaluating the recommendation engine.

```python
def ndcg_score(model, ratings, k):
    """
    Normalized discounted cumulative gain (NDCG) at rank k
    for the ALS model; which computes the ndcg score for
    each users' recommendation and does a simply average

    Parameters
    ----------
    model : ALSWR instance
        fitted ALSWR model

    ratings : scipy sparse csr_matrix [n_users, n_items]
        sparse matrix of user-item interactions

    k : int
        rank k's k

    Returns
    -------
    avg_ndcg : float
        ndcg at k's score averaged across all users
    """
    ndcg = 0.0
    n_users, n_items = ratings.shape
    for u in range(n_users):
        y_true = np.zeros(n_items)
        y_true[ratings[u].indices] = 1
        u_pred = model._predict_user(u)
        ndcg += ndcg_at_k(y_true, u_pred, k)

    avg_ndcg = ndcg / n_users
    return avg_ndcg

def ndcg_at_k(y_true, y_score, k = 10):
    """
    Normalized discounted cumulative gain (NDCG) at rank k

    Parameters
    ----------
    y_true : array-like, shape = [n_samples]
        Ground truth (true relevance labels)

    y_score : array-like, shape = [n_samples]
        Predicted scores

    k : int
        Rank

    Returns
    -------
    ndcg : float, 0.0 ~ 1.0
    """
    actual = dcg_at_k(y_true, y_score, k)
    best = dcg_at_k(y_true, y_true, k)
    ndcg = actual / best
    return ndcg

def dcg_at_k(y_true, y_score, k = 10):
    """
    Discounted cumulative gain (DCG) at rank k

    Parameters
    ----------
    y_true : array-like, shape = [n_samples]
        Ground truth (true relevance labels)

    y_score : array-like, shape = [n_samples]
        Predicted scores

    k : int
        Rank

    Returns
    -------
    dcg : float
    """
    order = np.argsort(y_score)[::-1]
    y_true = np.take(y_true, order[:k])
    gains = 2 ** y_true - 1
    discounts = np.log2(np.arange(2, gains.size + 2))
    dcg = np.sum(gains / discounts)
    return dcg
```

```python
k = 5
ndcg_train = ndcg_score(als, X_train, k)
ndcg_test = ndcg_score(als, X_test, k)
print('ndcg training:', ndcg_train)
print('ndcg testing:', ndcg_test)
```

    ndcg training: 0.2959125755797325
    ndcg testing: 0.11226091289209723

# Reference

- [Blog: Don’t invert that matrix](https://www.johndcook.com/blog/2010/01/19/dont-invert-that-matrix/)
- [Blog: Empty rows in sparse arrays](http://mike.place/2015/sparse/)
- [Blog: Implicit Feedback and Collaborative Filtering](http://datamusing.info/blog/2015/01/07/implicit-feedback-and-collaborative-filtering/)
- [Paper: Y. Hu, Y. Koren, C. Volinsky Collaborative Filtering for Implicit Feedback Datasets](http://yifanhu.net/PUB/cf.pdf)
- [StackExchange: Analytic solution for matrix factorization using alternating least squares](http://math.stackexchange.com/questions/1072451/analytic-solution-for-matrix-factorization-using-alternating-least-squares)

---

- [Gist: Ranking Metrics](https://gist.github.com/bwhite/3726239)
- [Gist: Learning to rank metrics](https://gist.github.com/mblondel/7337391)
- [Github: Average Precision](https://github.com/benhamner/Metrics/blob/master/Python/ml_metrics/average_precision.py)
- [Github: Average Precision Unit Test](https://github.com/benhamner/Metrics/blob/master/Python/ml_metrics/test/test_average_precision.py)
- [Wiki: Discounted cumulative gain](https://en.wikipedia.org/wiki/Discounted_cumulative_gain)
- [Blog: Measuring Search Relevance](http://www.ebaytechblog.com/2010/11/10/measuring-search-relevance/)
- [Blog: Evaluating recommender systems](http://fastml.com/evaluating-recommender-systems/)
- [Blog: What you wanted to know about Mean Average Precision](http://fastml.com/what-you-wanted-to-know-about-mean-average-precision/)