# Рекомендательные системы

Набор данных взят из книги [Programming Collective Intelligence by Toby Segaran](https://books.google.ro/books?id=fEsZ3Ey-Hq4C), [русская версия](http://www.symbol.ru/alphabet/613828.html).\
Первая версия кода [взята отсюда](https://cocalc.com/share/public_paths/66a0d74bf8b6f7b246e281a1a87fcd6a14d2ec0c) и [отсюда](https://gist.github.com/robt/4383763)

[Simple introduction to basics of recommender systems by Google](https://developers.google.com/machine-learning/recommendation/overview)

Идея рекомендательных систем на основе ближайшего соседа – использовать предпочтения некоторой группы людей, похожих на данного пользователя, чтобы рекомендовать ему что-либо.

У такой модели много применений:

   * рекомендация товаров интернет-магазинов
   * рекомендация музыки или фильмов
   * рекомендация людей/контента в социальных сетях

Пример рекомендательной системы Amazon.com

Рекомендация фильмов от IMDb.

Естественный способ получить рекомендацию о чем-либо - спросить мнение об этом у друзей или любых людей, которым нравится то же, что и вам. Эту идею можно использовать и для машины: для каждого человека алгоритм просматривает большую группу людей и ищет в ней подгруппу с похожим на данного человека вкусом. Далее создается список того, что еще нравится этим людям, а затем человеку рекомендуются предложения из этого списка. Такой алгоритм называется алгоритмом **коллаборативной фильтрации**.

Для красивого вывода на экран сложных объектов будем использовать **pprint**

```python
import pprint

pp = pprint.PrettyPrinter(indent=4)
```

## Подготовка данных

Чтобы хранить сразу много предпочтений для каждого человека, удобнее всего воспользоваться **вложенным словарем**.

Самый простой способ создать словарь в python - использовать фигурные скобки {}. Данные в словаре хранятся в формате ключ – значение, разделенные двоеточием:

```python
dict = {'key': value}
```

Мы будем работать с *вложенным словарем* кинокритиков и выставленных ими оценок для небольшого набора данных о фильмах. Построим его следующим образом:

1. для каждого кинокритика создаем словарь оценок фильмов в формате
   ```python
    scores_dict = {
        'film_1': 'score_1',
        'film_2': 'score_2',
        ...
    }
   ```

2. создаем словарь кинокритиков, где в качестве значений будет соответствующий ему словарь оценок
   ```python
     critics = {
         'critic_name_1': <scores_dict_1>,
         'critic_name_2': <scores_dict_2>,
         ...
     }
   ```

```python
# Словарь кинокритиков и выставленных ими оценок
critics = {
    "Lisa Rose": {
        "Lady in the Water": 2.5,
        "Snakes on a Plane": 3.5,
        "Just My Luck": 3.0,
        "Superman Returns": 3.5,
        "You, Me and Dupree": 2.5,
        "The Night Listener": 3.0,
    },
    "Gene Seymour": {
        "Lady in the Water": 3.0,
        "Snakes on a Plane": 3.5,
        "Just My Luck": 1.5,
        "Superman Returns": 5.0,
        "The Night Listener": 3.0,
        "You, Me and Dupree": 3.5,
    },
    "Michael Phillips": {
        "Lady in the Water": 2.5,
        "Snakes on a Plane": 3.0,
        "Superman Returns": 3.5,
        "The Night Listener": 4.0,
    },
    "Claudia Puig": {
        "Snakes on a Plane": 3.5,
        "Just My Luck": 3.0,
        "The Night Listener": 4.5,
        "Superman Returns": 4.0,
        "You, Me and Dupree": 2.5,
    },
    "Mick LaSalle": {
        "Lady in the Water": 3.0,
        "Snakes on a Plane": 4.0,
        "Just My Luck": 2.0,
        "Superman Returns": 3.0,
        "The Night Listener": 3.0,
        "You, Me and Dupree": 2.0,
    },
    "Jack Matthews": {
        "Lady in the Water": 3.0,
        "Snakes on a Plane": 4.0,
        "The Night Listener": 3.0,
        "Superman Returns": 5.0,
        "You, Me and Dupree": 3.5,
    },
    "Toby": {"Snakes on a Plane": 4.5, "You, Me and Dupree": 1.0, "Superman Returns": 4.0},
}
```

## Критерий похожести

Как мы уже говорили, чтобы выделить подгруппу людей с похожим вкусом, необходимо как-то определить, насколько люди похожи.
В нашем случае мы будем сравнивать оценки людей у одинаковых фильмов.
Мы рассмотрим два способа сравнения. С помощью:

- расстояния Евклида
- корреляции Пирсона

### Оценка по евклидову расстоянию

Евклидово расстояние для точек $x = (x_1, x_2, ... , x_n)$ и $y = (y_1, y_2, ... , y_n)$ определяется следующим образом:

\begin{equation}
d(x, y) = \sqrt{\sum_{i = 1}^n (x_i - y_i)^2}
\end{equation}

```python
from math import sqrt
```

Для понимания расстояния Евклида рассмотрим случай, когда *n = 2*. То есть каждый объект выборки (точка) описывается двумя параметрами (координатами). $x = (x_1, x_2)$ и $y = (y_1, y_2)$. Тогда Евклидово расстояние равно расстоянию между точками на плоскости.

\begin{equation}
d(x, y) = \sqrt{\sum_{i = 1}^n (x_i - y_i)^2} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
\end{equation}

Или, если представить задачу геометрически:

Для примера вычислим расстояния Евклида для близких точек с координатам (1, 1) и (2, 3):

```python
sqrt(pow(2 - 1, 2) + pow(3 - 1, 2))
```

    2.23606797749979

Теперь вычислим расстояния Евклида для точек, которые находятся далеко (1, 1) и (8, 10):

```python
sqrt(pow(8 - 1, 2) + pow(10 - 0, 2))
```

    12.206555615733702

Расстояние, вычисленное по этой формуле, будет тем меньше, чем больше сходство людей (чем ближе точки). Нам же нужна функция, значение которой будет, наоборот, большое, если люди сильно похожи друг на друга. То есть схожесть больше (от 0 до 1), если точки ближе друг другу

Для этого будем использовать функцию "похожести" в таком виде:

\begin{equation}
\text{similarity }(x, y) = \dfrac{1}{1 + d(x, y)},
\end{equation}

где $d(x, y)$ - расстояние Евклида.

Пример вычисления сходства по новой функции для точек с координатами (1, 1) и (2, 3):

```python
1 / (1 + sqrt(pow(2 - 1, 2) + pow(3 - 1, 2)))
```

    0.3090169943749474

Пример вычисления сходства по новой функции для точек с координатами (1, 1) и (8, 10):

```python
1 / (1 + sqrt(pow(8 - 1, 2) + pow(10 - 1, 2)))
```

    0.08063375388365411

Далее реализуем несколько простых функций для коллаборативной фильтрации на основе ближайшего соседа.

Для начала научимся искать общие для двух пользователей фильмы

```python
def similar_films(critics: dict, person1: str, person2: str):
    """Return list of films rated by both users"""
    # создаем пустой список фильмов
    sim_film = []

    # для всех фильмов у person1...
    for film in critics[person1]:
        # если такой фильм оценивался person 2...
        if film in critics[person2]:
            # добавляем этот фильм в список sim_film
            sim_film.append(film)

    return sim_film
```

Функцию для подсчета сходства двух критиков на основе расстояния Евклида реализована ниже. Функция принимает на вход три аргумента:

* словарь критиков
* имя первого критика для сравнения
* имя второго критика для сравнения

```python
def sim_distance(critics: dict, person1: str, person2: str) -> float:
    """Calculates euclidian distance based similarity score for persons"""
    # используем написанную функцию для получения фильмов, оцененных обоими критиками
    sim_films = similar_films(critics, person1, person2)

    # если нет ни одной общей оценки, то есть длина sim_film равна 0 - вернуть 0
    if len(sim_films) == 0:
        return 0

    # для каждого фильма из sim_film посчитаем евклидово расстояние и просуммируем все полученные значения
    sum_of_euclead_dist = 0

    for film in sim_films:
        sum_of_euclead_dist += pow(critics[person1][film] - critics[person2][film], 2)

    # считаем похожесть
    return 1 / (1 + sum_of_euclead_dist)
```

Посмотрим, как работает эта функция для двух критиков Lisa Rose и Toby:

```python
sim_distance(critics, "Lisa Rose", "Toby")
```

    0.2222222222222222

```python
import numpy as np
```

```python
dist_sim_vals = np.zeros((len(critics), len(critics)))

for i, source in enumerate(critics):
    for j, referer in enumerate(critics):
        dist_sim_vals[i, j] = sim_distance(critics, source, referer)

dist_sim_vals.round(3)
```

    array([[1.   , 0.148, 0.444, 0.286, 0.333, 0.211, 0.222],
           [0.148, 1.   , 0.211, 0.133, 0.129, 0.8  , 0.108],
           [0.444, 0.211, 1.   , 0.571, 0.286, 0.182, 0.286],
           [0.286, 0.133, 0.571, 1.   , 0.174, 0.182, 0.235],
           [0.333, 0.129, 0.286, 0.174, 1.   , 0.138, 0.308],
           [0.211, 0.8  , 0.182, 0.182, 0.138, 1.   , 0.118],
           [0.222, 0.108, 0.286, 0.235, 0.308, 0.118, 1.   ]])

```python
dist_sim_vals.mean(), dist_sim_vals.std()
```

    (0.36752489116590864, 0.2993937425464388)

```python
tirangle_mask = np.tri(len(critics), k=-1, dtype=np.bool_)

norm_dist_sim_vals = dist_sim_vals[tirangle_mask]

norm_dist_sim_vals.round(3)
```

    array([0.148, 0.444, 0.211, 0.286, 0.133, 0.571, 0.333, 0.129, 0.286,
           0.174, 0.211, 0.8  , 0.182, 0.182, 0.138, 0.222, 0.108, 0.286,
           0.235, 0.308, 0.118])

```python
norm_dist_sim_vals.mean(), norm_dist_sim_vals.std()
```

    (0.26211237302689333, 0.1636870664584294)

```python
import matplotlib.pyplot as plt

def plot_pdf(values: np.ndarray, title: str):
    """Plots `values` distribution density"""
    plt.figure(figsize=(10, 5))
    plt.violinplot(values.flatten(), vert=False, showmedians=True)
    plt.xlabel("Similarity")
    plt.grid()
    plt.title(title)
    plt.show()
```

```python
plot_pdf(dist_sim_vals, "Euclidian similarity distribution")
```

```python
plot_pdf(norm_dist_sim_vals, "Normalized euclidian similarity distribution")
```

Эта функция работает также как:

\begin{equation}
\text{similarity }(x, y) = \dfrac{1}{1 + d(x, y)},
\end{equation}

где $d(x, y)$ - расстояние Евклида.

Однако, по осям будет фильмы и оценки от критиков. Если у критиков нет одинаковых фильмов, то мы этих критиков не рассматриваем

### Коэффициент корреляции Пирсона

Другой способ определять степень схожести - использовать коэффициент корреляции Пирсона. Этот коэффициент показывает, насколько два набора данных хорошо ложатся на прямую.

Два кинокритика со средним значением корреляции 0.41 (слева) и два кинокритика с высоким значением корреляции 0.75 (справа).

Корреляция Пирсона считается по формуле:

\begin{equation}
    \text{corr}(x, y) = \dfrac{\sum_{i = 1}^n (x_i - \frac{\sum_{j = 1}^n x_j}{n}) (y_i - \frac{\sum_{j = 1}^n y_j}{n})}{\sqrt{\sum_{i = 1}^n (x_i - \frac{\sum_{j = 1}^n x_j}{n})^2 \sum_{i = 1}^n (y_i - \frac{\sum_{j = 1}^n y_j}{n})^2}}
\end{equation}

По аналогии с функцией **sim_distance()** в модуле **recommendations** реализована функция для подсчета сходства двух критиков на основе корреляции Пирсона. Она называется **sim_pearson()**. Функция принимает на вход те же самые аргументы, что и **sim_distance()**:

* словарь критиков
* имя первого критика для сравнения
* имя второго критика для сравнения

Посмотрим, как работает эта функция для двух критиков Lisa Rose и Gene Seymour:

```python
from math import isnan

from scipy.stats import pearsonr

def sim_pearson(critics_dict, person1, person2):
    """Calculates pearson correlation based similarity between persons"""
    # получить список фильмов, оцененных обоими критиками
    sim_films = similar_films(critics_dict, person1, person2)

    # если нет хотя бы пары общих оценок, вернуть 0
    if len(sim_films) < 2:
        return 0

    # получим оценки критиков для фильмов из пересечения
    scores1 = []
    scores2 = []
    for film in sim_films:
        scores1.append(critics_dict[person1][film])
        scores2.append(critics_dict[person2][film])

    # посчитаем коэффициент корреляции
    # берем [0] от результата, так как функция pearsonr() возвращает еще значение p-value
    correlation = pearsonr(scores1, scores2)[0]

    # если вдруг было деление на ноль и функция вернула nan, то присваиваем ноль
    if isnan(correlation):
        correlation = 0

    return correlation
```

```python
sim_pearson(critics, "Lisa Rose", "Gene Seymour")
```

    0.3960590171906697

```python
sim_pearson(critics, "Lisa Rose", "Toby")
```

    0.9912407071619302

Исследуем распределение такой величины схожести

```python
dist_pear_vals = np.zeros((len(critics), len(critics)))

for i, source in enumerate(critics):
    for j, referer in enumerate(critics):
        dist_pear_vals[i, j] = sim_pearson(critics, source, referer)

dist_pear_vals.round(3)
```

    array([[ 1.   ,  0.396,  0.405,  0.567,  0.594,  0.747,  0.991],
           [ 0.396,  1.   ,  0.205,  0.315,  0.412,  0.964,  0.381],
           [ 0.405,  0.205,  1.   ,  1.   , -0.258,  0.135, -1.   ],
           [ 0.567,  0.315,  1.   ,  1.   ,  0.567,  0.029,  0.893],
           [ 0.594,  0.412, -0.258,  0.567,  1.   ,  0.211,  0.924],
           [ 0.747,  0.964,  0.135,  0.029,  0.211,  1.   ,  0.663],
           [ 0.991,  0.381, -1.   ,  0.893,  0.924,  0.663,  1.   ]])

```python
dist_pear_vals.mean(), dist_pear_vals.std()
```

    (0.5159356584296166, 0.4713252827635987)

```python
tirangle_mask = np.tri(len(critics), k=-1, dtype=np.bool_)

norm_pear_sim_vals = dist_pear_vals[tirangle_mask]

norm_pear_sim_vals.round(3)
```

    array([ 0.396,  0.405,  0.205,  0.567,  0.315,  1.   ,  0.594,  0.412,
           -0.258,  0.567,  0.747,  0.964,  0.135,  0.029,  0.211,  0.991,
            0.381, -1.   ,  0.893,  0.924,  0.663])

```python
norm_pear_sim_vals.mean(), norm_pear_sim_vals.std()
```

    (0.43525826816788604, 0.4621798568790931)

```python
plot_pdf(dist_pear_vals, "Pearson similarity distribution")
```

```python
plot_pdf(norm_pear_sim_vals, "Normalized pearson similarity distribution")
```

### Ещё варианты критериев

* [Cosine similarity](https://en.wikipedia.org/wiki/Cosine_similarity)\
$$
S_C (A,B):= \cos(\theta) = {\mathbf{A} \cdot \mathbf{B} \over \|\mathbf{A}\| \|\mathbf{B}\|} = \frac{ \sum\limits_{i=1}^{n}{A_i  B_i} }{ \sqrt{\sum\limits_{i=1}^{n}{A_i^2}}  \sqrt{\sum\limits_{i=1}^{n}{B_i^2}} }$$
* [the Tanimoto score](https://stackoverflow.com/questions/4558830/tanimoto-score-and-when-it-is-used)

## Ранжирование критиков

После того как мы выбрали критерий для сравнения двух людей, можно начать искать для каждого человека подгруппу других людей с наиболее похожими вкусами.

Для этого ниже реализована функция **top_matches()**. Функция возвращает список n наилучших соответствий для человека из словаря critics.

Соответственно, аргументы у функции:

* словарь критиков *critics*
* имя персоны *person*, для которой подбираем соответствия
* число *n* наилучших соответствий

Кроме этого, так как мы хотим использовать различные расстояния, то в качестве аргумента мы будем также передавать имя функции-расстояния (*sim_fn*).

```python
from typing import Callable, List

def top_matches(
    critics: dict, person: str, n: int = 5, similarity_fn: Callable = sim_pearson
) -> List[str]:
    """Retruns top-n most similar critics"""
    # инициализируем лист оценок похожести
    scores = []

    # для каждого человека в словаре критиков
    for other in critics:
        if other == person:
            continue

        # добавить в scores похожесть, вычисленную с помощью функции, записанной в similarity, и имя человека
        similarity_score = similarity_fn(critics, person, other)
        scores.append((similarity_score, other))

    # отсортировать список по убыванию оценок
    scores.sort(reverse=True)  # аргумент reverse=True отвечает за убывающий порядок

    # возвращаем первые n человек
    return scores[0:n]
```

Пример вычисления топ-3 критиков похожих на Toby по корреляции Пирсона:

```python
top_matches(critics, "Toby", n=3, similarity_fn=sim_pearson)
```

    [(0.9912407071619302, 'Lisa Rose'),
     (0.9244734516419049, 'Mick LaSalle'),
     (0.8934051474415642, 'Claudia Puig')]

Как работает эта функция:
*  считаем схожесть интересующего нас критика с каждым другим критиком используя функцию **similarity**
*  сортируем результаты
*  возвращаем топ **n** результатов

Пример вычисления топ-3 критиков похожих на Toby по евклидову расстоянию:

```python
top_matches(critics, "Toby", n=3, similarity_fn=sim_distance)
```

    [(0.3076923076923077, 'Mick LaSalle'),
     (0.2857142857142857, 'Michael Phillips'),
     (0.23529411764705882, 'Claudia Puig')]

```python
pp.pprint({k: v for k, v in critics.items() if k in ("Toby", "Mick LaSalle", "Lisa Rose")})
```

    {   'Lisa Rose': {   'Just My Luck': 3.0,
                         'Lady in the Water': 2.5,
                         'Snakes on a Plane': 3.5,
                         'Superman Returns': 3.5,
                         'The Night Listener': 3.0,
                         'You, Me and Dupree': 2.5},
        'Mick LaSalle': {   'Just My Luck': 2.0,
                            'Lady in the Water': 3.0,
                            'Snakes on a Plane': 4.0,
                            'Superman Returns': 3.0,
                            'The Night Listener': 3.0,
                            'You, Me and Dupree': 2.0},
        'Toby': {   'Snakes on a Plane': 4.5,
                    'Superman Returns': 4.0,
                    'You, Me and Dupree': 1.0}}

Выбор расстояния для подбора похожести зависит от конкретной задачи. Имеет смысл попробовать разные критерии и посмотреть, какой из них дает наилучший результат.

## Рекомендация фильмов (User-based подход)

Мы умеем находить людей с похожим мнением как у рассматриваемого человека. Но как получить конкретную рекомендацию с набором фильмов? Можно было бы просто посмотреть на то, какие фильмы понравились похожему человеку, и выбрать из них не просмотренные. Однако, такой способ не стабилен: можем, например, отобрать критика, которому по каким-то причинам понравился фильм, получивший негативные оценки от остальных.

Чтобы разрешить эту проблему, можно ранжировать сами фильмы, вычисляя взвешенную сумму оценок критиков. Мы будем брать оценку фильма у критика и умножать его на коэффициент похожести с тем, для которого мы строим рекомендацию. Затем полученный результат суммировать и нормировать.

Идея такого подхода в том, что люди похожие на тебя вносят больший вклад в "рейтинг" фильма.

<p align="center">

</p>

Этот подход реализован ниже в функции **get_recommendations()**. Функция принимает на вход следующие аргументы:

* словарь критиков *critics\_dict*
* имя персоны *person*, для которой подбираем соответствия
* имя функции-критерия для сравнения

Как работает эта функция:
*  считаем схожесть пользователя с каждым другим пользователем
*  перебираем фильмы, которые смотрели другие пользователи, но не смотрел текущий пользователь
*  нормируем рейтинг фильмов на основе того насколько он понравился другим пользователям, и того насколько каждый из других пользователей похож на текущего пользователя
*  сортируем список

```python
def get_recommendations(prefs: dict, person: str, similarity: Callable = sim_pearson):
    """Calculates recomendations for a `person` using weighted mean from `prefs`

    Получить рекомендации для заданного человека, пользуясь взвешенным средним оценок, данных всеми остальными пользователями
    """
    # инициализируем словари
    totals = {}
    sim_sums = {}

    # для каждого пользователя
    for other in prefs:
        # сравнивать person с person не нужно
        if other == person:
            continue
        # считаем похожесть
        sim = similarity(prefs, person, other)

        # игнорировать нулевые и отрицательные значения похожести
        if sim <= 0:
            continue

        # для каждого фильма в оцененных other
        for item in prefs[other]:
            # оценивать только фильмы, которые я еще не смотрел
            if item not in prefs[person] or prefs[person][item] == 0:

                # коэффициент подобия * Оценка
                totals.setdefault(item, 0)
                totals[item] = totals[item] + prefs[other][item] * sim

                # cумма коэффициентов подобия
                sim_sums.setdefault(item, 0)
                sim_sums[item] = sim_sums[item] + sim
    # создать нормированный список
    rankings = [(total / sim_sums[item], item) for item, total in totals.items()]

    # вернуть отсортированный список
    rankings.sort(reverse=True)
    return rankings
```

```python
get_recommendations(critics, "Toby", sim_pearson)
```

    [(3.347789526713101, 'The Night Listener'),
     (2.832549918264162, 'Lady in the Water'),
     (2.5309807037655645, 'Just My Luck')]

```python
critics["Toby"]
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

```python
get_recommendations(critics, "Toby", sim_distance)
```

    [(3.5002478401415877, 'The Night Listener'),
     (2.7561242939959363, 'Lady in the Water'),
     (2.461988486074374, 'Just My Luck')]

Мы получили ранжированный список фильмов, а также прогноз оценки, которую поставит Toby этим фильмам.

## Коллаборативная фильтрация по сходству объектов (Item-based collaborative filtering)

Мы реализовали механизм выдачи рекомендаций таким образом, что для создания набора данных необходимы оценки, выставленные каждым
пользователем. Для нескольких тысяч людей это, возможно, и будет работать, но, когда речь идет о миллионах пользователей и товаров, сравнение каждого пользователя со всеми другими, а затем сравнение товаров, займет недопустимо много времени.

Техника, которую мы применяли до сих пор, называется **коллаборативной фильтрацией по схожести пользователей**. Альтернатива известна под названием **коллаборативная фильтрация по схожести образцов**.

Основная идея процедуры фильтрации по схожести образцов заключается в том, чтобы для каждого образца заранее вычислить большинство похожих на него. Тогда для выдачи рекомендаций алгоритму достаточно будет найти те образцы, которым пользователь выставил наивысшие оценки, а затем сформировать список образцов, максимально похожих на эти.

Отметим одно существенное отличие: хотя на первом шаге необходимо исследовать все данные, результаты сравнения образцов изменяются не так часто, как результаты сравнения пользователей.

Чтобы сравнивать образцы, нужно первым делом построить полный набор данных о похожих образцах. В первую очередь, нам нужно переделать словарь предпочтений критиков таким образом, чтобы теперь был словарь фильмов и их оценок:

```python
film_dict = {
    'film_name_1':
    {'critic_1': 'score_1',
     'critic_2': 'score_2',
     ...
    },
    ...
}
```

Для подсчёта такого словаря будем исопльзовать функцию ниже:

```python
def transform_prefs(critics: dict) -> dict:
    """Inverts nested dict of critics-films-scores into films-critics-scores dict"""
    # инициилизируем новый словарь с фильмами и их оценками
    result = {}

    # для каждого критика в словаре
    for person in critics:
        # для каждого оцененного им фильма
        for item in critics[person]:
            # добавляем такой объект в словарь
            result.setdefault(item, {})
            # меняеи местами человека и предмет
            result[item][person] = critics[person][item]

    return result
```

Далее для каждого фильма посчитать наиболее похожие образцы и коэффициенты похожести для них. Для подсчета схожести будем использовать евклидово расстояние.

Ниже есть функция **calculate_similar_items()**, которая делает сразу оба шага. На вход её нужно подать словарь с оценками критиков *critics* и число образцов, которые мы считаем наиболее похожими (остальные не выводим).

```python
def calculate_similar_items(prefs: dict, n: int = 10, similarity_fn: Callable = sim_distance):
    """Creates a dict with most similar items for each item"""
    result = {}
    # обращаем матрицу предпочтений, чтобы строки соответствовали образцам
    item_prefs = transform_prefs(prefs)

    c = 0
    for item in item_prefs:
        # проверка состояния для больших наборов данных
        c = c + 1

        # найти образцы, максимально похожие на данный
        scores = top_matches(item_prefs, item, n=n, similarity_fn=similarity_fn)
        result[item] = scores

        if c % 100 == 0:
            print("%d / %d" % (c, len(item_prefs)))

    return result
```

```python
itemsim_dist = calculate_similar_items(critics, n=10)
itemsim_pears = calculate_similar_items(critics, n=10, similarity_fn=sim_pearson)
```

```python
pp.pprint(itemsim)
```

    {   'Just My Luck': [   (0.2222222222222222, 'Lady in the Water'),
                            (0.18181818181818182, 'You, Me and Dupree'),
                            (0.15384615384615385, 'The Night Listener'),
                            (0.10526315789473684, 'Snakes on a Plane'),
                            (0.06451612903225806, 'Superman Returns')],
        'Lady in the Water': [   (0.4, 'You, Me and Dupree'),
                                 (0.2857142857142857, 'The Night Listener'),
                                 (0.2222222222222222, 'Snakes on a Plane'),
                                 (0.2222222222222222, 'Just My Luck'),
                                 (0.09090909090909091, 'Superman Returns')],
        'Snakes on a Plane': [   (0.2222222222222222, 'Lady in the Water'),
                                 (0.18181818181818182, 'The Night Listener'),
                                 (0.16666666666666666, 'Superman Returns'),
                                 (0.10526315789473684, 'Just My Luck'),
                                 (0.05128205128205128, 'You, Me and Dupree')],
        'Superman Returns': [   (0.16666666666666666, 'Snakes on a Plane'),
                                (0.10256410256410256, 'The Night Listener'),
                                (0.09090909090909091, 'Lady in the Water'),
                                (0.06451612903225806, 'Just My Luck'),
                                (0.05333333333333334, 'You, Me and Dupree')],
        'The Night Listener': [   (0.2857142857142857, 'Lady in the Water'),
                                  (0.18181818181818182, 'Snakes on a Plane'),
                                  (0.15384615384615385, 'Just My Luck'),
                                  (0.14814814814814814, 'You, Me and Dupree'),
                                  (0.10256410256410256, 'Superman Returns')],
        'You, Me and Dupree': [   (0.4, 'Lady in the Water'),
                                  (0.18181818181818182, 'Just My Luck'),
                                  (0.14814814814814814, 'The Night Listener'),
                                  (0.05333333333333334, 'Superman Returns'),
                                  (0.05128205128205128, 'Snakes on a Plane')]}

```python
itemsim["Superman Returns"]
```

    [(0.16666666666666666, 'Snakes on a Plane'),
     (0.10256410256410256, 'The Night Listener'),
     (0.09090909090909091, 'Lady in the Water'),
     (0.06451612903225806, 'Just My Luck'),
     (0.05333333333333334, 'You, Me and Dupree')]

Получили вложенный словарь с фильмами и их похожестью на другие фильмы.

Теперь мы готовы выдавать рекомендации, пользуясь словарем данных о схожести образцов. По аналогии с тем, что мы уже делали, необходимо получить список всех образцов, которым пользователь выставлял оценки, найти похожие и взвесить их с учетом коэффициентов подобия (умножить оценку на коэффициент подобия).

Этот механизм реализован в функции **get_recommended_items()**. На вход подается:

* словарь с оценками пользователя (в нашем случае это словарь критиков, *critics*)
* словарь с данными о схожести образцов, *itemsim*
* имя критика, для которого строим рекомендации

```python
def get_recommended_items(prefs: dict, item_match: dict, user: str):
    """Calculates recomendations for `user` using mean scores based on items similarity"""
    # инициализируем словари
    user_ratings = prefs[user]
    scores = {}
    total_sim = {}

    # цикл по образцам, оцененным данным пользователем (хранятся в user_ratings)
    for item, rating in user_ratings.items():
        # цикл по образцам, похожим на данный (хранятся в словаре item_match)
        for similarity, item2 in item_match[item]:
            # пропускаем объект, если пользователь уже оценивал данный образец
            if item2 in user_ratings:
                continue
            # взвешенная суммы оценок, умноженных на коэффициент подобия
            scores.setdefault(item2, 0)
            scores[item2] = scores[item2] + similarity * rating
            # сумма всех коэффициентов подобия
            total_sim.setdefault(item2, 0)
            total_sim[item2] = total_sim[item2] + similarity

            if total_sim[item2] == 0:
                total_sim[item2] = 0.0000001  # чтобы избежать деления на ноль

    # делим каждую итоговую оценку на взвешенную сумму, чтобы вычислить среднее
    rankings = [(score / total_sim[item], item) for item, score in scores.items()]

    # возвращает список rankings, отсортированный по убыванию
    rankings.sort(reverse=True)

    return rankings
```

Получим рекомендации для пользователя Toby:

```python
critics["Toby"]
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

```python
get_recommended_items(critics, itemsim_dist, "Toby")
```

    [(3.182634730538922, 'The Night Listener'),
     (2.5983318700614575, 'Just My Luck'),
     (2.4730878186968837, 'Lady in the Water')]

```python
get_recommended_items(critics, itemsim_pears, "Toby")
```

    [(3.6100310668021796, 'Lady in the Water'),
     (3.5313950341859823, 'The Night Listener'),
     (2.9609998607242685, 'Just My Luck')]

## Метрики качества для рекомендательных систем

Для оценки качества работы рекомендательных систем можно рассмотреть две специальные метрики **Hit Rate** (HR) и **Mean Reciprocal Rank** (MRR).

Определим как **попадание** (**hit**) - ситуацию, когда фильм был рекомендован пользователю в топ-N и пользователь посмотрел его.

**Hit Rate (HR)**

**Hit Rate (процент попаданий)** определяется как общее число попаданий, нормированное на количество пользователей.

$$
\text{HR} = \frac{1}{\text{# test users}} \sum_{\text{test users}}{\text{hit}}, \quad
$$

Чем больше процент попаданий, тем лучше будет наша система рекомендаций.

*Замечание*: в некотором смысле, Hit Rate представляет собой производное от классических ML метрик precision и recall.

**Mean Reciprocal Rank (MRR)**

Отличие метрики **Mean Reciprocal Rank** от предыдущей в том, что мы учитываем не общее количество попаданий, а обратное рангу (месту в списке рекомендаций) **первое попадание**. Например, если фильм, просмотренный пользователем (попадание), стоял на втором месте, обратное рангу будет равно $\frac{1}{2}$, а если бы стоял на третьем - $\frac{1}{3}$ и так далее.

$$
\text{MRR} = \frac{1}{\text{# test users}} \sum_{\text{test users}}{\frac{1}{\text{hit rank}}}
$$

Рассмотрим на примере. Toby получил следующий список рекомендаций:

```python
get_recommendations(critics, "Toby", sim_pearson)
```

    [(3.347789526713101, 'The Night Listener'),
     (2.832549918264162, 'Lady in the Water'),
     (2.5309807037655645, 'Just My Luck')]

Допустим, после этого Toby посмотрел 5 фильмов и выставил им следующие оценки:

|Score|Film|
|----|--------------------|
|4.5 | The Departed |
|4   | The Night Listener |
|3.9 | Lady in the Water |
|3.7 | The Firm |
|3   | Just My Luck |

Как мы видим, только два фильма из списка рекомендаций попали в личный топ-3 Toby. Получается, что **попадание (hits)** у него 2.

**HR = $\dfrac{1}{1} * 2 = 2$**, так как выборка тестовых пользователей (test\_users) состоит из одного Toby.

* **$\frac{1}{hit\_rank}$** для фильма The Night Listener - это $\dfrac{1}{2}$
* **$\frac{1}{hit\_rank}$** для фильма Lady in the Water - это $\dfrac{1}{3}$

Но фильм **The Night Listener** стоит выше в рейтинге Toby, чем **Lady in the Water**, поэтому:

**MRR = $\dfrac{1}{1}*\dfrac{1}{2} = \dfrac{1}{2}$**, так как, опять же, выборка тестовых пользователей (test\_users) состоит из одного Toby.

## Бонус* : Рекомендация на данных MovieLens

В этом примере мы рассмотрим реальный набор данных с оценками фильмов, который называется MovieLens. Источник данных: http://grouplens.org/datasets/movielens/

Датасет представляет собой набор фильмов, которые 162541 пользователей сайта MovieLens оценивали по 5-бальной шкале. В наборе также есть дополнительная информация такая как описание фильмов тегами, но в нашем упражнении мы её использовать не будем для упрощения. Также, чтобы уменьшить время вычислений, будем использовать данный только 943 пользователей. Каждый пользователь в датасете оценил минимум 20 фильмов.

Загруженный датасет будет в следующем виде:

```python

preferences = {
    'user_id_1' : {
        'film_name_1' : rate,
        'film_name_2' : rate,
        ...
        'film_name_21' : rate,
    },
    ...
    'user_id_943' : {
        'film_name_1' : rate,
        'film_name_2' : rate,
        ...
        'film_name_30' : rate,
    }
}

```

Чтобы загрузить набор данных MovieLens, ниже есть функция **load_movie_lens()**. Она возвращает словарь предпочтений фильмов для набора людей. Пользователи зашифрованы идентификатором. Для каждого пользователя имеется название
фильма, а также оценка, выставленная фильму данным пользователе.

Перед запуском функции необходимо предварительно загрузить из внешнего источника файлы с данными с помощью **wget**

```python
!wget -nc https://raw.githubusercontent.com/v-goncharenko/madmo-adv/3b0f120b745cd0679af6e7ebe78f2c4187a17d8a/09_recommendations/u.item
!wget -nc https://github.com/v-goncharenko/madmo-adv/raw/3b0f120b745cd0679af6e7ebe78f2c4187a17d8a/09_recommendations/u.data
```

    File ‘u.item’ already there; not retrieving.

    File ‘u.data’ already there; not retrieving.

```python
def load_movie_lens():
    # получить названия фильмов
    movies = {}
    for line in open("u.item", encoding="ISO-8859-1"):
        film_id, title = line.split("|")[:2]
        movies[int(film_id)] = title

    # загрузить данные
    prefs = {}
    for line in open("u.data"):
        user, movieid, rating, ts = line.split("\t")
        user_id = int(user)

        prefs.setdefault(user_id, {})
        movie_title = movies[int(movieid)]
        prefs[user_id][movie_title] = float(rating)

    return prefs
```

```python
preferences = load_movie_lens()

len(preferences)
```

    943

```python
preferences[196]
```

    {'Kolya (1996)': 3.0,
     'Mrs. Doubtfire (1993)': 4.0,
     "Muriel's Wedding (1994)": 4.0,
     'Shall We Dance? (1996)': 3.0,
     'Stand by Me (1986)': 5.0,
     'Ace Ventura: Pet Detective (1994)': 5.0,
     'Mrs. Brown (Her Majesty, Mrs. Brown) (1997)': 4.0,
     'Raising Arizona (1987)': 4.0,
     'Being There (1979)': 5.0,
     'Truth About Cats & Dogs, The (1996)': 4.0,
     'Englishman Who Went Up a Hill, But Came Down a Mountain, The (1995)': 2.0,
     'Birdcage, The (1996)': 4.0,
     'English Patient, The (1996)': 5.0,
     'Home Alone (1990)': 3.0,
     'American President, The (1995)': 5.0,
     'Babe (1995)': 5.0,
     'Harold and Maude (1971)': 4.0,
     'Up in Smoke (1978)': 4.0,
     'Four Weddings and a Funeral (1994)': 3.0,
     'While You Were Sleeping (1995)': 3.0,
     'Men in Black (1997)': 2.0,
     'Kids in the Hall: Brain Candy (1996)': 4.0,
     'Groundhog Day (1993)': 3.0,
     'Boogie Nights (1997)': 3.0,
     "Marvin's Room (1996)": 3.0,
     'Cold Comfort Farm (1995)': 3.0,
     'Adventures of Priscilla, Queen of the Desert, The (1994)': 4.0,
     'Secrets & Lies (1996)': 5.0,
     'Van, The (1996)': 3.0,
     'Waiting for Guffman (1996)': 4.0,
     'Nutty Professor, The (1996)': 4.0,
     'Fish Called Wanda, A (1988)': 5.0,
     'Mighty Aphrodite (1995)': 2.0,
     'Beautiful Girls (1996)': 3.0,
     'Princess Bride, The (1987)': 2.0,
     'Fast, Cheap & Out of Control (1997)': 4.0,
     'That Thing You Do! (1996)': 4.0,
     'Full Monty, The (1997)': 3.0,
     'Operation Dumbo Drop (1995)': 1.0}

Выведем фильмы для пользователя с ID 87 и рейтинги этих фильмов

```python
pp.pprint(preferences[87])
```

    {   '2001: A Space Odyssey (1968)': 5.0,
        'Ace Ventura: Pet Detective (1994)': 4.0,
        'Addams Family Values (1993)': 2.0,
        'Addicted to Love (1997)': 4.0,
        'Adventures of Priscilla, Queen of the Desert, The (1994)': 3.0,
        'Adventures of Robin Hood, The (1938)': 5.0,
        'Air Force One (1997)': 3.0,
        'Air Up There, The (1994)': 3.0,
        'Alien (1979)': 4.0,
        'American President, The (1995)': 5.0,
        'Annie Hall (1977)': 4.0,
        'Apocalypse Now (1979)': 4.0,
        'Babe (1995)': 5.0,
        'Baby-Sitters Club, The (1995)': 2.0,
        'Back to the Future (1985)': 5.0,
        'Bad Boys (1995)': 4.0,
        'Bananas (1971)': 5.0,
        'Barcelona (1994)': 3.0,
        'Batman & Robin (1997)': 4.0,
        'Batman (1989)': 3.0,
        'Batman Returns (1992)': 3.0,
        'Big Green, The (1995)': 3.0,
        'Big Squeeze, The (1996)': 2.0,
        'Birdcage, The (1996)': 4.0,
        'Blade Runner (1982)': 4.0,
        'Blues Brothers, The (1980)': 5.0,
        'Boomerang (1992)': 3.0,
        'Boot, Das (1981)': 4.0,
        'Brady Bunch Movie, The (1995)': 2.0,
        'Braveheart (1995)': 4.0,
        'Bridge on the River Kwai, The (1957)': 5.0,
        'Broken Arrow (1996)': 3.0,
        'Butch Cassidy and the Sundance Kid (1969)': 5.0,
        'Citizen Kane (1941)': 4.0,
        "City Slickers II: The Legend of Curly's Gold (1994)": 3.0,
        'Clear and Present Danger (1994)': 5.0,
        'Cliffhanger (1993)': 3.0,
        'Clockwork Orange, A (1971)': 4.0,
        'Clueless (1995)': 4.0,
        'Con Air (1997)': 4.0,
        'Conan the Barbarian (1981)': 3.0,
        'Coneheads (1993)': 4.0,
        'Cool Hand Luke (1967)': 5.0,
        'Cops and Robbersons (1994)': 3.0,
        'Cowboy Way, The (1994)': 3.0,
        'Crow, The (1994)': 3.0,
        'Dances with Wolves (1990)': 5.0,
        "Dante's Peak (1997)": 3.0,
        'Dave (1993)': 4.0,
        'Days of Thunder (1990)': 5.0,
        'Dead Man Walking (1995)': 4.0,
        'Dead Poets Society (1989)': 5.0,
        'Deer Hunter, The (1978)': 3.0,
        'Demolition Man (1993)': 3.0,
        'Desperado (1995)': 3.0,
        'Die Hard (1988)': 4.0,
        'Die Hard: With a Vengeance (1995)': 4.0,
        'Down Periscope (1996)': 4.0,
        'Dragonheart (1996)': 4.0,
        'Dumb & Dumber (1994)': 4.0,
        'Dunston Checks In (1996)': 1.0,
        'E.T. the Extra-Terrestrial (1982)': 3.0,
        'Ed Wood (1994)': 3.0,
        'Empire Strikes Back, The (1980)': 5.0,
        'Endless Summer 2, The (1994)': 3.0,
        'Evil Dead II (1987)': 2.0,
        'Executive Decision (1996)': 3.0,
        'Fargo (1996)': 5.0,
        'First Wives Club, The (1996)': 2.0,
        'Fish Called Wanda, A (1988)': 5.0,
        'Forget Paris (1995)': 4.0,
        'Four Weddings and a Funeral (1994)': 5.0,
        'French Kiss (1995)': 5.0,
        'Fugitive, The (1993)': 5.0,
        'Full Metal Jacket (1987)': 4.0,
        'Get Shorty (1995)': 5.0,
        'Glory (1989)': 4.0,
        'Godfather, The (1972)': 4.0,
        'GoldenEye (1995)': 4.0,
        'Good, The Bad and The Ugly, The (1966)': 5.0,
        'GoodFellas (1990)': 4.0,
        'Grease (1978)': 4.0,
        'Great White Hype, The (1996)': 3.0,
        'Groundhog Day (1993)': 5.0,
        'Grumpier Old Men (1995)': 4.0,
        'Hard Target (1993)': 4.0,
        'Heat (1995)': 3.0,
        'Heathers (1989)': 3.0,
        'Home Alone (1990)': 4.0,
        'Hoop Dreams (1994)': 4.0,
        'Hot Shots! Part Deux (1993)': 4.0,
        'House Arrest (1996)': 3.0,
        'I Love Trouble (1994)': 3.0,
        'I.Q. (1994)': 5.0,
        'In the Army Now (1994)': 4.0,
        'In the Line of Fire (1993)': 5.0,
        'Independence Day (ID4) (1996)': 5.0,
        'Indiana Jones and the Last Crusade (1989)': 5.0,
        'Inkwell, The (1994)': 3.0,
        "It's a Wonderful Life (1946)": 5.0,
        'Jack (1996)': 3.0,
        'Jeffrey (1995)': 3.0,
        'Jimmy Hollywood (1994)': 3.0,
        "Joe's Apartment (1996)": 2.0,
        'Junior (1994)': 4.0,
        'Jurassic Park (1993)': 5.0,
        'Kingpin (1996)': 4.0,
        'Lightning Jack (1994)': 3.0,
        'Lost World: Jurassic Park, The (1997)': 3.0,
        'Low Down Dirty Shame, A (1994)': 3.0,
        'M*A*S*H (1970)': 5.0,
        'Magnificent Seven, The (1954)': 5.0,
        'Major Payne (1994)': 3.0,
        'Man of the House (1995)': 3.0,
        'Manchurian Candidate, The (1962)': 4.0,
        'Mars Attacks! (1996)': 3.0,
        'Mask, The (1994)': 3.0,
        'Matilda (1996)': 3.0,
        'Maverick (1994)': 3.0,
        'Michael (1996)': 4.0,
        'Mighty Aphrodite (1995)': 3.0,
        'Milk Money (1994)': 4.0,
        'Mission: Impossible (1996)': 4.0,
        "Monty Python's Life of Brian (1979)": 4.0,
        'Mother (1996)': 2.0,
        'Mrs. Doubtfire (1993)': 4.0,
        'Multiplicity (1996)': 3.0,
        'Muppet Treasure Island (1996)': 3.0,
        'My Favorite Year (1982)': 3.0,
        'Naked Gun 33 1/3: The Final Insult (1994)': 4.0,
        'Net, The (1995)': 5.0,
        'Nine Months (1995)': 4.0,
        'Nutty Professor, The (1996)': 4.0,
        'Platoon (1986)': 3.0,
        'Prefontaine (1997)': 5.0,
        'Private Benjamin (1980)': 4.0,
        'Professional, The (1994)': 4.0,
        'Program, The (1993)': 3.0,
        'Pulp Fiction (1994)': 4.0,
        "Pyromaniac's Love Story, A (1995)": 3.0,
        'Quiet Man, The (1952)': 5.0,
        'Raging Bull (1980)': 3.0,
        'Raiders of the Lost Ark (1981)': 5.0,
        'Raising Arizona (1987)': 3.0,
        'Reality Bites (1994)': 3.0,
        'Renaissance Man (1994)': 5.0,
        'Return of the Jedi (1983)': 5.0,
        'Return of the Pink Panther, The (1974)': 4.0,
        'River Wild, The (1994)': 4.0,
        'Sabrina (1995)': 4.0,
        'Santa Clause, The (1994)': 4.0,
        "Schindler's List (1993)": 4.0,
        'Searching for Bobby Fischer (1993)': 4.0,
        'Serial Mom (1994)': 1.0,
        'Shadow, The (1994)': 3.0,
        'Shawshank Redemption, The (1994)': 5.0,
        "Singin' in the Rain (1952)": 4.0,
        'Sleeper (1973)': 4.0,
        'Sleepers (1996)': 4.0,
        'Sleepless in Seattle (1993)': 5.0,
        'Sneakers (1992)': 4.0,
        'So I Married an Axe Murderer (1993)': 2.0,
        'Something to Talk About (1995)': 2.0,
        'Son in Law (1993)': 4.0,
        'Speechless (1994)': 4.0,
        'Speed (1994)': 5.0,
        'Speed 2: Cruise Control (1997)': 3.0,
        'Star Trek III: The Search for Spock (1984)': 4.0,
        'Star Trek IV: The Voyage Home (1986)': 5.0,
        'Star Trek: First Contact (1996)': 4.0,
        'Star Trek: The Motion Picture (1979)': 3.0,
        'Star Trek: The Wrath of Khan (1982)': 5.0,
        'Star Wars (1977)': 5.0,
        'Stargate (1994)': 5.0,
        'Sting, The (1973)': 5.0,
        'Strange Days (1995)': 3.0,
        'Strictly Ballroom (1992)': 3.0,
        'Striptease (1996)': 2.0,
        'Supercop (1992)': 3.0,
        'Swimming with Sharks (1995)': 3.0,
        'Switchblade Sisters (1975)': 2.0,
        'Terminator 2: Judgment Day (1991)': 5.0,
        'Terminator, The (1984)': 5.0,
        'That Old Feeling (1997)': 4.0,
        'That Thing You Do! (1996)': 4.0,
        'This Is Spinal Tap (1984)': 5.0,
        'To Die For (1995)': 3.0,
        'To Kill a Mockingbird (1962)': 4.0,
        'To Wong Foo, Thanks for Everything! Julie Newmar (1995)': 3.0,
        'Tommy Boy (1995)': 4.0,
        'Top Gun (1986)': 5.0,
        'Treasure of the Sierra Madre, The (1948)': 4.0,
        'True Lies (1994)': 5.0,
        'Truth About Cats & Dogs, The (1996)': 4.0,
        'Twelve Monkeys (1995)': 4.0,
        'Twister (1996)': 4.0,
        "Ulee's Gold (1997)": 3.0,
        'Under Siege (1992)': 4.0,
        'Up in Smoke (1978)': 3.0,
        'Vegas Vacation (1997)': 4.0,
        'Victor/Victoria (1982)': 4.0,
        'Waterworld (1995)': 4.0,
        "Weekend at Bernie's (1989)": 3.0,
        'When Harry Met Sally... (1989)': 5.0,
        'While You Were Sleeping (1995)': 5.0,
        'Wizard of Oz, The (1939)': 5.0,
        'Wyatt Earp (1994)': 3.0,
        'Young Frankenstein (1974)': 5.0,
        'Young Guns (1988)': 3.0,
        'Young Guns II (1990)': 2.0}

Посмотрим, какие рекомендации получим с помощью коллаборативной фильтрации по схожести пользователей. Напомним, что для этого реализована функция **get_recommendations()**, которая выдает рекомендации для данного пользователя по похожести на других и предположительную его оценку фильму.

Функция принимает на вход следующие аргументы:
* словарь пользователей, в этом случае это *preferences*
* имя персоны *person*, для которой подбираем соответствия
* имя функции-критерия для сравнения

Выведем первые топ-30 рекомендаций для пользователя с ID 87, для этого в квадратных скобочках укажем [0:30].

*Замечание*: может появиться предупреждение RuntimeWarning, это связано с тем, что теперь мы работаем с большой выборкой и при подсчете корреляции Пирсона может возникать деление на ноль. В этом случае мы присваиваем корреляции значение ноль.

```python
get_recommendations(preferences, 87, sim_pearson)[0:10]
```

    /usr/local/Caskroom/miniconda/base/envs/ml-mipt/lib/python3.8/site-packages/scipy/stats/stats.py:3913: PearsonRConstantInputWarning: An input array is constant; the correlation coefficent is not defined.
      warnings.warn(PearsonRConstantInputWarning())

    [(5.0, 'They Made Me a Criminal (1939)'),
     (5.0, 'Star Kid (1997)'),
     (5.0, 'Santa with Muscles (1996)'),
     (5.0, 'Saint of Fort Washington, The (1993)'),
     (5.0, 'Marlene Dietrich: Shadow and Light (1996) '),
     (5.0, 'Great Day in Harlem, A (1994)'),
     (5.0, 'Entertaining Angels: The Dorothy Day Story (1996)'),
     (5.0, 'Boys, Les (1997)'),
     (4.898844431289231, 'Legal Deceit (1997)'),
     (4.815019082242706, 'Letter From Death Row, A (1998)')]

Интересно также сравнить полученный результат с рекомендациями по схожести образцов. Напомним, чтобы сравнить образцы, нужно в первую очередь перестроить наш набор данных из набора предпочтений пользователей в набор оценок фильмов. Для этого используем функцию **calculate_similar_items()**. На вход её нужно подать словарь с оценками пользователей *prefernces*. Кроме этого, мы передадим аргумент *n = 50*, так как хотим оставить только 50 наиболее похожих образцов в новом словаре.

В этот раз мы работаем с большим набором данных и вычисления могут занять некоторое время, поэтому функция также будет выводить сообщение о том, сколько фильмов она уже просмотрела.

```python
item_similarities = calculate_similar_items(preferences, n=50)
```

    100 / 1664
    200 / 1664
    300 / 1664
    400 / 1664
    500 / 1664
    600 / 1664
    700 / 1664
    800 / 1664
    900 / 1664
    1000 / 1664
    1100 / 1664
    1200 / 1664
    1300 / 1664
    1400 / 1664
    1500 / 1664
    1600 / 1664

Посмотрим на результат:

```python
item_similarities["What's Eating Gilbert Grape (1993)"]
```

    [(1.0, 'Yankee Zulu (1994)'),
     (1.0, 'Woman in Question, The (1950)'),
     (1.0, 'Witness (1985)'),
     (1.0, "Wend Kuuni (God's Gift) (1982)"),
     (1.0, 'Vie est belle, La (Life is Rosey) (1987)'),
     (1.0, 'Vermont Is For Lovers (1992)'),
     (1.0, 'Van, The (1996)'),
     (1.0, 'Two or Three Things I Know About Her (1966)'),
     (1.0, 'Two Much (1996)'),
     (1.0, 'Two Deaths (1995)'),
     (1.0, 'Two Bits (1995)'),
     (1.0, 'Touki Bouki (Journey of the Hyena) (1973)'),
     (1.0, 'Total Eclipse (1995)'),
     (1.0, 'To Have, or Not (1995)'),
     (1.0, 'To Cross the Rubicon (1991)'),
     (1.0, 'Tigrero: A Film That Was Never Made (1994)'),
     (1.0, 'The Courtyard (1995)'),
     (1.0, 'Terror in a Texas Town (1958)'),
     (1.0, 'Talking About Sex (1994)'),
     (1.0, 'T-Men (1947)'),
     (1.0, 'Symphonie pastorale, La (1946)'),
     (1.0, 'Swept from the Sea (1997)'),
     (1.0, 'Stranger in the House (1997)'),
     (1.0, 'Stonewall (1995)'),
     (1.0, 'Squeeze (1996)'),
     (1.0, 'Spirits of the Dead (Tre passi nel delirio) (1968)'),
     (1.0, 'Somebody to Love (1994)'),
     (1.0, 'So Dear to My Heart (1949)'),
     (1.0, 'Sliding Doors (1998)'),
     (1.0, 'Shooting Fish (1997)'),
     (1.0, 'Shadows (Cienie) (1988)'),
     (1.0, 'Secret Adventures of Tom Thumb, The (1993)'),
     (1.0, 'Scream of Stone (Schrei aus Stein) (1991)'),
     (1.0, 'Salut cousin! (1996)'),
     (1.0, 'Romper Stomper (1992)'),
     (1.0, 'Relative Fear (1994)'),
     (1.0, 'Quartier Mozart (1992)'),
     (1.0, 'Promise, The (Versprechen, Das) (1994)'),
     (1.0, 'Prefontaine (1997)'),
     (1.0, "Pharaoh's Army (1995)"),
     (1.0, 'Nosferatu a Venezia (1986)'),
     (1.0, 'Nina Takes a Lover (1994)'),
     (1.0, 'Niagara, Niagara (1997)'),
     (1.0, 'Mr. Jones (1993)'),
     (1.0, 'Mostro, Il (1994)'),
     (1.0, 'Modern Affair, A (1995)'),
     (1.0, 'Mighty, The (1998)'),
     (1.0, 'Metisse (Café au Lait) (1993)'),
     (1.0, 'Man from Down Under, The (1943)'),
     (1.0, 'Liebelei (1933)')]

Теперь получим топ-30 рекомендаций для пользователя с учетом похожести фильмов. Используем функцию **get_recommended_items()**, которой на вход подается:

* словарь с оценками пользователя (*preferences*)
* словарь с данными о схожести фильмов, *item_similarities*
* ID пользователя, для которого строим рекомендации

```python
get_recommended_items(preferences, item_similarities, 87)[0:30]
```

    [(5.0, "What's Eating Gilbert Grape (1993)"),
     (5.0, 'Vertigo (1958)'),
     (5.0, 'Usual Suspects, The (1995)'),
     (5.0, 'Toy Story (1995)'),
     (5.0, 'Titanic (1997)'),
     (5.0, 'Sword in the Stone, The (1963)'),
     (5.0, 'Stand by Me (1986)'),
     (5.0, 'Sling Blade (1996)'),
     (5.0, 'Silence of the Lambs, The (1991)'),
     (5.0, 'Shining, The (1980)'),
     (5.0, 'Shine (1996)'),
     (5.0, 'Sense and Sensibility (1995)'),
     (5.0, 'Scream (1996)'),
     (5.0, 'Rumble in the Bronx (1995)'),
     (5.0, 'Rock, The (1996)'),
     (5.0, 'Robin Hood: Prince of Thieves (1991)'),
     (5.0, 'Reservoir Dogs (1992)'),
     (5.0, 'Police Story 4: Project S (Chao ji ji hua) (1993)'),
     (5.0, 'House of the Spirits, The (1993)'),
     (5.0, 'Fresh (1994)'),
     (5.0, 'Denise Calls Up (1995)'),
     (5.0, 'Day the Sun Turned Cold, The (Tianguo niezi) (1994)'),
     (5.0, 'Before the Rain (Pred dozhdot) (1994)'),
     (5.0, 'Assignment, The (1997)'),
     (5.0, '1-900 (1994)'),
     (4.875, "Ed's Next Move (1996)"),
     (4.833333333333333, 'Anna (1996)'),
     (4.8, 'Dark City (1998)'),
     (4.75, 'Flower of My Secret, The (Flor de mi secreto, La) (1995)'),
     (4.75, 'Broken English (1996)')]

Выведем список фильмов, которые пользователь с id `87` оценил максимальным баллом:

```python
pref_87 = preferences[87]
{film: pref_87[film] for film in pref_87 if pref_87[film] == 5.0}
```

    {'Jurassic Park (1993)': 5.0,
     'Indiana Jones and the Last Crusade (1989)': 5.0,
     'Good, The Bad and The Ugly, The (1966)': 5.0,
     'Dead Poets Society (1989)': 5.0,
     'M*A*S*H (1970)': 5.0,
     '2001: A Space Odyssey (1968)': 5.0,
     'Four Weddings and a Funeral (1994)': 5.0,
     'Prefontaine (1997)': 5.0,
     'This Is Spinal Tap (1984)': 5.0,
     'Cool Hand Luke (1967)': 5.0,
     'True Lies (1994)': 5.0,
     'Sleepless in Seattle (1993)': 5.0,
     'Young Frankenstein (1974)': 5.0,
     'Fugitive, The (1993)': 5.0,
     'In the Line of Fire (1993)': 5.0,
     'Speed (1994)': 5.0,
     'Terminator 2: Judgment Day (1991)': 5.0,
     'Shawshank Redemption, The (1994)': 5.0,
     'Clear and Present Danger (1994)': 5.0,
     'Dances with Wolves (1990)': 5.0,
     'Adventures of Robin Hood, The (1938)': 5.0,
     'Blues Brothers, The (1980)': 5.0,
     'Babe (1995)': 5.0,
     'Top Gun (1986)': 5.0,
     'Get Shorty (1995)': 5.0,
     'Wizard of Oz, The (1939)': 5.0,
     'When Harry Met Sally... (1989)': 5.0,
     'Star Trek IV: The Voyage Home (1986)': 5.0,
     'Net, The (1995)': 5.0,
     'Fargo (1996)': 5.0,
     'Stargate (1994)': 5.0,
     'Groundhog Day (1993)': 5.0,
     'Bridge on the River Kwai, The (1957)': 5.0,
     'Star Trek: The Wrath of Khan (1982)': 5.0,
     'Bananas (1971)': 5.0,
     'French Kiss (1995)': 5.0,
     'Empire Strikes Back, The (1980)': 5.0,
     'Return of the Jedi (1983)': 5.0,
     'Butch Cassidy and the Sundance Kid (1969)': 5.0,
     'Star Wars (1977)': 5.0,
     'Magnificent Seven, The (1954)': 5.0,
     'Back to the Future (1985)': 5.0,
     'Terminator, The (1984)': 5.0,
     'Fish Called Wanda, A (1988)': 5.0,
     "It's a Wonderful Life (1946)": 5.0,
     'Renaissance Man (1994)': 5.0,
     'American President, The (1995)': 5.0,
     'I.Q. (1994)': 5.0,
     'While You Were Sleeping (1995)': 5.0,
     'Independence Day (ID4) (1996)': 5.0,
     'Raiders of the Lost Ark (1981)': 5.0,
     'Quiet Man, The (1952)': 5.0,
     'Sting, The (1973)': 5.0,
     'Days of Thunder (1990)': 5.0}

Валидация для моделей рекомендательных систем устроена достаточно сложно.
Например, нам нужно забыть часть рейтингов фильмов для пользователя, а потом попытаться восстановить их с
помощью нашей модели.

В данном случае, мы можем оценить адекватность полученных рекомендаций, просмотрев списки фильмов. Однако, на текущем датасете, из-за размера выборки, такое сделать сложно.

Приведем примеры интерпретации:
* В случае использования расстояния, основанного на коррелции, модель рекомендует в первую очередь боевики
* В случае использование похожестей фильмов мы видим как общепризнанные шедевры, так и боевики
* В реальности пользователю нравятся фильмы, которые нравятся всем, приключенческие фильмы и боевики

С такой точки зрения оба списка рекомендаций кажутся адекватным, а то, что они не пересекаются связано с тем,
что есть много похожих друг на друга фильмов - и мы не можем рекомендовать сразу все такие фильмы пользователю.

Для рекомендательных систем есть специальные библиотеки, например, https://surprise.readthedocs.io/, с ней вы познакомитесь в домашней работе.


# Interacting with PyLie's database

As explained in Appendix D of PyR@TE 3's manual, it might be necessary to use Clebsch-Gordan coefficients (CGCs) to build the Lagrangian of a given model. The general syntax in the model file is:
Definitions: {
    C : cgc(groupName, field_1, ... field_N, P),
  # or, equivalently,
    C : cgc(group, representation 1, ... representation N, P)
}
Here, `P` indicates that we are using the `P`th invariant tensor returned by PyLie's `invariants()` function. When there is only one possible gauge invariant contraction, this last argument can simply be omitted. In other cases however, there might be several such invariants. In this case, we have to interrogate PyLie's database to get the return value of the `invariants()` function.
In this tutorial notebook, we show how to interact with the database to get various types of information, such as the CGCs, and some other info related to the Lie algebras.

---
First, let's get into PyLie's main directory (located in `/src/PyLie` from PyR@TE's directory) :

```python
%cd .../PyR@TE_3/src/PyLie
```

Let's import the PyLieDB class and load the database (DB) by instantiating a database object, `db` :

```python
from PyLieDB import PyLieDB

# Create an instance of the Database object
db = PyLieDB()
```

The content of the DB can be printed at any time calling `db.visit()`, or, equivalently, `print(db)`. The argument `shorter=True` can be provided if we do not want to display the whole content of the contained matrices and tensors.

```python
db.visit(shorter=True)
```

    Content of the DB (unzipped size = 2.54MB):

    E6
       /adjointrep : [0 0 0 0 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [4 4]
                [5 2]
                [5 5]]
          /s : [6 6]
          /v : [b'2' b'-1' b'-1' ... b'2' b'-1' b'2']
       /dimension : 78
       /fullname : E6
       /name : E6
       /rank : 6
       /structureconstants
          /k : [[ 0  1  6]
                [ 0  6  1]
                [ 0  7 11]
                ...
                [77 61 25]
                [77 70 34]
                [77 71 35]]
          /s : [78 78 78]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1/2' b'-1/2']

    E7
       /adjointrep : [1 0 0 0 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [5 5]
                [6 2]
                [6 6]]
          /s : [7 7]
          /v : [b'2' b'-1' b'-1' ... b'2' b'-1' b'2']
       /dimension : 133
       /fullname : E7
       /name : E7
       /rank : 7
       /structureconstants
          /k : [[  0   1   7]
                [  0   7   1]
                [  0   8  13]
                ...
                [132 120  57]
                [132 121  58]
                [132 122  59]]
          /s : [133 133 133]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1/2' b'1/2']

    E8
       /adjointrep : [0 0 0 0 0 0 1 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [6 6]
                [7 2]
                [7 7]]
          /s : [8 8]
          /v : [b'2' b'-1' b'-1' ... b'2' b'-1' b'2']
       /dimension : 248
       /fullname : E8
       /name : E8
       /rank : 8
       /structureconstants
          /k : [[  0   1   8]
                [  0   8   1]
                [  0   9  15]
                ...
                [247 232 112]
                [247 233 113]
                [247 234 114]]
          /s : [248 248 248]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1/2' b'1/2']

    F4
       /adjointrep : [1 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [2 3]
                [3 2]
                [3 3]]
          /s : [4 4]
          /v : [b'2' b'-1' b'-1' b'2' b'-2' b'-1' b'2' b'-1' b'-1' b'2']
       /dimension : 52
       /dimr
          /(1,0,0,0) : 52
       /fullname : F4
       /name : F4
       /rank : 4
       /structureconstants
          /k : [[ 0  1  4]
                [ 0  4  1]
                [ 0  5  7]
                ...
                [51 42 18]
                [51 43 19]
                [51 44 20]]
          /s : [52 52 52]
          /v : [b'-1/2' b'1/2' b'1/2' ... b's(2)/4' b'-s(2)/2' b's(2)/4']

    G2
       /adjointrep : [1 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                [1 1]]
          /s : [2 2]
          /v : [b'2' b'-3' b'-1' b'2']
       /dimension : 14
       /fullname : G2
       /name : G2
       /rank : 2
       /structureconstants
          /k : [[ 0  1  2]
                [ 0  2  1]
                [ 0  4  5]
                ...
                [13  8  2]
                [13  9  3]
                [13 10  4]]
          /s : [14 14 14]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-s(3)/6' b'-s(3)/6' b's(3)/2']

    SO10
       /adjointrep : [0 1 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [3 3]
                [4 2]
                [4 4]]
          /s : [5 5]
          /v : [b'2' b'-1' b'-1' ... b'2' b'-1' b'2']
       /dimension : 45
       /dynkinlabels
          /-126 : [0 0 0 2 0]
          /126 : [0 0 0 0 2]
          /16 : [0 0 0 0 1]
       /fullname : SO10
       /name : D5
       /rank : 5
       /structureconstants
          /k : [[ 0  1  5]
                [ 0  5  1]
                [ 0  6  9]
                ...
                [44 34 14]
                [44 35 15]
                [44 36 16]]
          /s : [45 45 45]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'1/2' b'-1/2']

    SO5
       /adjointrep : [0 2]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                [1 1]]
          /s : [2 2]
          /v : [b'2' b'-2' b'-1' b'2']
       /dimension : 10
       /fullname : SO5
       /name : B2
       /rank : 2
       /structureconstants
          /k : [[0 1 2]
                [0 2 1]
                [0 4 8]
                ...
                [9 4 0]
                [9 5 1]
                [9 7 3]]
          /s : [10 10 10]
          /v : [b'-1/2' b'1/2' b's(2)/2' ... b's(2)/2' b'-s(2)/2' b'-s(2)/2']

    SO6
       /adjointrep : [0 1 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [0 2]
                ...
                [1 1]
                [2 0]
                [2 2]]
          /s : [3 3]
          /v : [b'2' b'-1' b'-1' b'-1' b'2' b'-1' b'2']
       /dimension : 15
       /fullname : SO6
       /name : D3
       /rank : 3
       /structureconstants
          /k : [[ 0  1  3]
                [ 0  2  4]
                [ 0  3  1]
                ...
                [14  9  3]
                [14 10  4]
                [14 11  5]]
          /s : [15 15 15]
          /v : [b'-1/2' b'-1/2' b'1/2' ... b'-1/2' b'1/2' b'-1/2']

    SO7
       /adjointrep : [0 1 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [1 2]
                [2 1]
                [2 2]]
          /s : [3 3]
          /v : [b'2' b'-1' b'-1' b'2' b'-2' b'-1' b'2']
       /dimension : 21
       /fullname : SO7
       /name : B3
       /rank : 3
       /structureconstants
          /k : [[ 0  1  3]
                [ 0  3  1]
                [ 0  4  5]
                ...
                [20 12  3]
                [20 15  6]
                [20 16  7]]
          /s : [21 21 21]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-s(2)/2' b'-s(2)/2' b's(2)/2']

    SO8
       /adjointrep : [0 1 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [2 2]
                [3 1]
                [3 3]]
          /s : [4 4]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'-1' b'2' b'-1' b'2']
       /dimension : 28
       /fullname : SO8
       /name : D4
       /rank : 4
       /structureconstants
          /k : [[ 0  1  4]
                [ 0  4  1]
                [ 0  5  7]
                ...
                [27 20  8]
                [27 21  9]
                [27 22 10]]
          /s : [28 28 28]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-1/2' b'-1/2' b'1/2']

    SO9
       /adjointrep : [0 1 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [2 3]
                [3 2]
                [3 3]]
          /s : [4 4]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'2' b'-2' b'-1' b'2']
       /dimension : 36
       /fullname : SO9
       /name : B4
       /rank : 4
       /structureconstants
          /k : [[ 0  1  4]
                [ 0  4  1]
                [ 0  5  7]
                ...
                [35 25  9]
                [35 27 11]
                [35 28 12]]
          /s : [36 36 36]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-s(2)/2' b's(2)/2' b'-s(2)/2']

    SP10
       /adjointrep : [2 0 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [3 4]
                [4 3]
                [4 4]]
          /s : [5 5]
          /v : [b'2' b'-1' b'-1' ... b'-1' b'-2' b'2']
       /dimension : 55
       /fullname : SP10
       /name : C5
       /rank : 5
       /structureconstants
          /k : [[ 0  1  5]
                [ 0  5  1]
                [ 0  6  9]
                ...
                [54 38 13]
                [54 39 14]
                [54 41 16]]
          /s : [55 55 55]
          /v : [b'-s(2)/4' b's(2)/4' b's(2)/4' ... b'-1/2' b'1/2' b'-1/2']

    SP12
       /adjointrep : [2 0 0 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [4 5]
                [5 4]
                [5 5]]
          /s : [6 6]
          /v : [b'2' b'-1' b'-1' ... b'-1' b'-2' b'2']
       /dimension : 78
       /fullname : SP12
       /name : C6
       /rank : 6
       /structureconstants
          /k : [[ 0  1  6]
                [ 0  6  1]
                [ 0  7 11]
                ...
                [77 56 20]
                [77 57 21]
                [77 60 24]]
          /s : [78 78 78]
          /v : [b'-s(2)/4' b's(2)/4' b's(2)/4' ... b'1/2' b'-1/2' b'1/2']

    SP4
       /adjointrep : [2 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                [1 1]]
          /s : [2 2]
          /v : [b'2' b'-1' b'-2' b'2']
       /dimension : 10
       /fullname : SP4
       /name : C2
       /rank : 2
       /structureconstants
          /k : [[0 1 2]
                [0 2 1]
                [0 2 3]
                ...
                [9 4 0]
                [9 5 1]
                [9 6 2]]
          /s : [10 10 10]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1' b'1/2']

    SP6
       /adjointrep : [2 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [1 2]
                [2 1]
                [2 2]]
          /s : [3 3]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-2' b'2']
       /dimension : 21
       /fullname : SP6
       /name : C3
       /rank : 3
       /structureconstants
          /k : [[ 0  1  3]
                [ 0  3  1]
                [ 0  4  5]
                ...
                [20 12  3]
                [20 13  4]
                [20 14  5]]
          /s : [21 21 21]
          /v : [b'-s(2)/4' b's(2)/4' b's(2)/4' ... b'-1/2' b'1/2' b'-1/2']

    SP8
       /adjointrep : [2 0 0 0]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [2 3]
                [3 2]
                [3 3]]
          /s : [4 4]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'2' b'-1' b'-2' b'2']
       /dimension : 36
       /fullname : SP8
       /name : C4
       /rank : 4
       /structureconstants
          /k : [[ 0  1  4]
                [ 0  4  1]
                [ 0  5  7]
                ...
                [35 23  7]
                [35 24  8]
                [35 26 10]]
          /s : [36 36 36]
          /v : [b'-s(2)/4' b's(2)/4' b's(2)/4' ... b'1/2' b'-1/2' b'1/2']

    SU2
       /adjointrep : [2]
       /cartanmatrix
          /k : [[0 0]]
          /s : [1 1]
          /v : [b'2']
       /dimension : 3
       /dimr
          /(0,) : 1
          /(1,) : 2
       /dynkinindex
          /(0,) : 0
          /(1,) : b'1/2'
       /dynkinlabels
          /1 : [0]
          /2 : [1]
          /3 : [2]
       /frobenius
          /(0,) : 0
          /(1,) : -1
       /fullname : SU2
       /invariants
          /((1,),(1,));00;1;00
          /((1,),(1,));01;1;00
          /((2,),(2,));00;1;00
          /((2,),(2,));00;1;11
       /name : A1
       /rank : 1
       /repmatrices
          /(0,);0;1
          /(1,);0;0
          /(1,);1;0
          /(2,);0;0
          /(2,);0;1
       /structureconstants
          /k : [[0 1 2]
                [0 2 1]
                [1 0 2]
                [1 2 0]
                [2 0 1]
                [2 1 0]]
          /s : [3 3 3]
          /v : [b'1' b'-1' b'-1' b'1' b'1' b'-1']

    SU3
       /adjointrep : [1 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                [1 1]]
          /s : [2 2]
          /v : [b'2' b'-1' b'-1' b'2']
       /dimension : 8
       /dimr
          /(0,0) : 1
          /(0,1) : 3
          /(1,0) : 3
       /dynkinindex
          /(0,0) : 0
          /(0,1) : b'1/2'
          /(1,0) : b'1/2'
       /dynkinlabels
          /1 : [0 0]
          /15 : [[2 1]
                 [4 0]]
          /3 : [1 0]
       /frobenius
          /(0,0) : 0
          /(0,1) : 1
          /(1,0) : 1
       /fullname : SU3
       /invariants
          /((0,1),(0,1),(1,0),(1,0));0000;1;0000
          /((0,1),(1,0));00;1;00
       /name : A2
       /rank : 2
       /repmatrices
          /(0,0);0;1
          /(0,1);0;0
          /(1,0);0;0
       /structureconstants
          /k : [[0 1 2]
                [0 2 1]
                [0 3 6]
                ...
                [7 3 0]
                [7 4 1]
                [7 5 2]]
          /s : [8 8 8]
          /v : [b'-1/2' b'1/2' b's(3)/2' ... b'1/2' b'-1' b'1/2']

    SU4
       /adjointrep : [1 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [1 2]
                [2 1]
                [2 2]]
          /s : [3 3]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'2']
       /dimension : 15
       /fullname : SU4
       /name : A3
       /rank : 3
       /structureconstants
          /k : [[ 0  1  3]
                [ 0  3  1]
                [ 0  4  5]
                ...
                [14  9  3]
                [14 10  4]
                [14 11  5]]
          /s : [15 15 15]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-1/2' b'1/2' b'-1/2']

    SU5
       /adjointrep : [1 0 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [2 3]
                [3 2]
                [3 3]]
          /s : [4 4]
          /v : [b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'2' b'-1' b'-1' b'2']
       /dimension : 24
       /dynkinlabels
          /10 : [0 1 0 0]
          /5 : [1 0 0 0]
       /fullname : SU5
       /invariants
          /((0,1,0,0),(0,1,0,0),(1,0,0,0));000;1;000
       /name : A4
       /rank : 4
       /structureconstants
          /k : [[ 0  1  4]
                [ 0  4  1]
                [ 0  5  7]
                ...
                [23 17  7]
                [23 18  8]
                [23 19  9]]
          /s : [24 24 24]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1/2' b'1/2']

    SU6
       /adjointrep : [1 0 0 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [3 4]
                [4 3]
                [4 4]]
          /s : [5 5]
          /v : [b'2' b'-1' b'-1' ... b'-1' b'-1' b'2']
       /dimension : 35
       /fullname : SU6
       /name : A5
       /rank : 5
       /structureconstants
          /k : [[ 0  1  5]
                [ 0  5  1]
                [ 0  6  9]
                ...
                [34 27 12]
                [34 28 13]
                [34 29 14]]
          /s : [35 35 35]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-1/2' b'1/2' b'-1/2']

    SU7
       /adjointrep : [1 0 0 0 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [4 5]
                [5 4]
                [5 5]]
          /s : [6 6]
          /v : [b'2' b'-1' b'-1' ... b'-1' b'-1' b'2']
       /dimension : 48
       /fullname : SU7
       /name : A6
       /rank : 6
       /structureconstants
          /k : [[ 0  1  6]
                [ 0  6  1]
                [ 0  7 11]
                ...
                [47 39 18]
                [47 40 19]
                [47 41 20]]
          /s : [48 48 48]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'1/2' b'-1/2' b'1/2']

    SU8
       /adjointrep : [1 0 0 0 0 0 1]
       /cartanmatrix
          /k : [[0 0]
                [0 1]
                [1 0]
                ...
                [5 6]
                [6 5]
                [6 6]]
          /s : [7 7]
          /v : [b'2' b'-1' b'-1' ... b'-1' b'-1' b'2']
       /dimension : 63
       /fullname : SU8
       /name : A7
       /rank : 7
       /structureconstants
          /k : [[ 0  1  7]
                [ 0  7  1]
                [ 0  8 13]
                ...
                [62 53 25]
                [62 54 26]
                [62 55 27]]
          /s : [63 63 63]
          /v : [b'-1/2' b'1/2' b'1/2' ... b'-1/2' b'1/2' b'-1/2']

### [Optional] Load the database

At this point, we may load the database using `db.load()`. If we skip this step, any request sent to the database will trigger the following operations :
1. Uncompress the DB
2. Load the DB
3. Send the request / print the result
4. Update the content of the DB
4. Close the DB
5. Compress the DB (using gzip)

If the `PyLieDB` file is large, the above steps (in particular steps 1 and 6) will take quite some time. In such cases, the user should load the database once, and close it afterwards using `db.close()`. While the database is loaded, a lock will prevent any other program to access it. Therefore it is very important to close it in order to release this lock and allow PyR@TE 3 to access its content at a later time.

```python
# Optional : load the database (uncomment if needed)

# db.load()
```

---

```python
# Load sympy's printing system in order to get good-looking output in the following
from sympy import init_session

init_session(quiet=True)
```

---
## Getting information from the DB

### Some basic information

Before discussing the CGCs, let us try and get some information about the gauge groups. Here and after, the standard way to ask the DB some information is to use the method `db.get(Group, Type of info, [Arguments, [Keyword arguments]])`. If the information is not already present in the DB, it will be automatically computed and stored.

```python
print('SU2 dimension : ', db.get('SU2', 'dimension'))
print('SO10 dimension: ', db.get('SO10', 'dimension'))
print('SU3 rank : ', db.get('SU3', 'rank'))
print('SU5 adjoint representation :', db.get('SU5', 'adjoint'))
print('E8 dimension and rank : ', db.get('E8', 'dimension'), ' ; ', db.get('E8', 'rank'))
print('G2 cartan matrix :', db.get('G2', 'cartan'))

print('\n\nSU2 structure constants:')
print(db.get('SU2', 'structureConstants'))
```

    SU2 dimension :  3
    SO10 dimension:  45
    SU3 rank :  2
    SU5 adjoint representation : [1, 0, 0, 1]
    E8 dimension and rank :  248  ;  8
    G2 cartan matrix : Matrix([[2, -3], [-1, 2]])

    SU2 structure constants:
    [Matrix([
    [0,  0, 0],
    [0,  0, 1],
    [0, -1, 0]]), Matrix([
    [0, 0, -1],
    [0, 0,  0],
    [1, 0,  0]]), Matrix([
    [ 0, 1, 0],
    [-1, 0, 0],
    [ 0, 0, 0]])]

Here is a sumary of the commands used to get basic information about the Lie algebras :

| Command | Description  |
| ------- |-----------|
| `'name'` | Name of the algebra (according to Cartan classification) |
| `'rank'` | Rank of the algebra |
| `'dimension'`, `'dim'` | Dimension of the algebra (i.e. of its adjoint representation) |
| `'cartanMatrix'`, `'cartan'` | Cartan matrix of the algebra |
| `'adjointRep'`, `'adjoint'` | Dynkin labels of the adjoint representation |
| `'structureConstants'`, `'struct'` | Structure constants of the algebra |

---
### Representations and Dynkin labels

In PyLie, the irredicible representations of the gauge groups are uniquely defined by their Dynkin labels. Various functions are implemented in PyLieDB that can help identify the Dynkin labels of a given representation.

##### 1. Identification of a representation by its dimension

To identify the Dynkin labels of a representation based on its dimension, one can call `db.get([group], 'labels', [dimension])`. For instance:

```python
# Adjoint representation of SU(2)
print( db.get('SU2', 'labels', 3) )

# Representation 10 of SU(5)
print( db.get('SU5', 'labels', 10) )

# Representation 126 and 126* of SO(10)
print( db.get('SO10', 'labels', 126) , " and ", db.get('SO10', 'labels', -126) )
```

    [2]
    [0, 1, 0, 0]
    [0, 0, 0, 0, 2]  and  [0, 0, 0, 2, 0]

If several representations have the same dimension, the output will be a list of Dynkin labels:

```python
# 15-dimensional representations of SU(3)
db.get('SU3', 'labels', 15)
```

##### 2. Display the first representations of a given algebra

To display the first few representation of a gauge group, one can call `db.get([group], 'firstReps', [N])` where `N` is a positive integer:

```python
db.get('SU4', 'firstReps', 5)
```

    -- First representations of SU4 --

    [1, 0, 0] : 4
    [0, 0, 1] : -4
    [0, 1, 0] : 6
    [0, 0, 2] : 10
    [2, 0, 0] : -10
    [1, 0, 1] : 15
    [0, 1, 1] : 20
    [1, 1, 0] : -20
    [0, 2, 0] : 20'
    [0, 0, 3] : 20''
    [3, 0, 0] : -20''

The output is in the form `[Dynkin labels] : name`, where `name` refers to the usual Particle Physics notation based on the dimension of the representation. Here we see that $\mathit{SU}(4)$ contains three 20-dimensional representations, distinguished from one another adding one or more "primes" `'` to the dimension. We emphasize that a minus sign indicates a conjugate representation.

##### 3. The other way around : from Dynkin labels to dimension

One can get the dimension of a given representation from its Dynkin labels using `db.get([group], 'dimR', [representation])` :

```python
db.get('F4', 'dimR', [1,0,0,0])
```

More conveniently, its Particle Physics notation can be retrieved calling `db.get([group], 'repName', [representation], latex = True/False, iLatex=True/False)`. The `latex` keyword produces a Latex string, the `iLatex` displays this latex output in an interactive session like this one :

```python
db.get('SU3', 'repName', [0,4], iLatex=True), db.get('SU5', 'repName', [1,0,0,1], iLatex=True)
```

The following table summarizes the various commands that can be called to get information about the representations :

| Command | Argument | Keyword Arguments | Description |
| ------- |-----------|-------------------| -----------|
| `'dynkinLabels'`, `'labels'` | Dimension of the representation | / | |
| `'firstReps'` | A positive integer | / | |
| `'dimR'` | Dynkin labels of a representation | / | |
| `'repName'` | Dynkin labels of a representation | `latex=True/False`, `iLatex=True/False` | |
| `'conjugate'` | Dynkin labels of a representation | / | Returns the conjugate representation |
| `'frobenius'` | Dynkin labels of a representation | / | Returns the Frobenius-Schur indicator |
| `'dynkinIndex'`, `'index'` | Dynkin labels of a representation | / | Returns the Dynkin index |

The Frobenius-Schur indicator is an integer whose value depends on the "reality" of the representation. It may take the three following values :
- $1$ if the representation is complex
- $0$ if the representation is real
- $-1$ if the representation is pseudo-real

We note that for pseudo-real representation, the conjugate representation can be accessed in the Dynkin-labels notation appending `True` to the list. For instance, the $\overline{\mathbf{2}}$ of $\mathit{SU}(2)$ is represented by `[1, True]`.

---
Now that we have introduced the Dynkin-labels notation, we may call three additional commands to get useful information from the database.

### Products of representations

The decomposition of products of representations in a direct sum may be obtained using the command `'repProduct'`, taking as argument a list of representations (in the Dynkin-labels notation). For instance:

```python
db.get('SU3', 'repProduct', [[1,0], [0,1]], iLatex=True)
```

Here, we used the `'iLatex'` keyword argument to get a nice looking output. The information we get is that, in $\mathit{SU}(3)$,
$$
    \mathbf{3} \otimes \mathbf{3} = \mathbf{8}\oplus\mathbf{1}\,.
$$

Another more complicated example would be the product $\mathbf{16} \otimes \overline{\mathbf{126}} \otimes \mathbf{16}$ of $\mathit{SO}(10)$:

```python
# Get the Dynkin labels of the 16 and 126* of SO10
r16 = db.get('SO10', 'labels', 16)
r126 = db.get('SO10', 'labels', -126)

db.get('SO10', 'repProduct', [r16, r126, r16], iLatex=True)
```

where the presence of the trivial representation indicates that we may construct a gauge singlet out of these three representations.

---
### Gauge generators

Another useful information is the expression of the generators of a given representation of a gauge group. We will refer to them as 'representation matrices'. The standard way to get representation matrices is to call `.get(Group, 'repMatrices', [representation])`. For instance, let us try to get the generators of the fundamental representations of $\mathit{SU}(2)$ and $\mathit{SU}(3)$:

```python
db.get('SU2', 'repMatrices', [1]) # We expect the Pauli matrices to show up ...
```

```python
db.get('SU3', 'repMatrices', [1,0]) # We expect the Gell-Mann matrices to show up ...
```

The Pauli matrices did show up in the first case, but the second output does not exactly match the usual expression for the Gell-Mann matrices (altough it looks similar). This brings us to an important remark :

**The explicit form of the generators of a representation is not uniquely defined. For a given representation, there exists infinitely many sets of gauge generators satisfying the commutation relations of the Lie algebra. Some quantities, such as the CGCs, depend in general on the choice of generators.**

#### A subtelty : real representations

By default, generators of real representations are not imaginary and antisymmetric. For instance, the generators of the adjoint representation of $\mathit{SU}(2)$ are:

```python
db.get('SU2', 'repMatrices', [2])
```

This can be an issue in some cases where the generators must be expressed in a real basis in order to guarantee gauge invariance at the level of the Lagrangian. An option, `realBasis`, was implemented in PyLie that can be used to rotate the generators to such a basis:

```python
db.get('SU2', 'repMatrices', [2], realBasis = True)
```

For adjoint representations, the "rotated" generators thus obtained are also required to satisfy the relation
$$
    (T_a)^i_j = -i\,f_{a\ j}^{\ i}\,.
$$
In our case, we can check this easily:

```python
db.get('SU2', 'structureConstants')
```

---
### Clebsch-Gordan coefficients

We finally arrive to the computation of CGCs, which, as stated before, may be useful in some cases to construct the model file. The standard way of computing CGCs is to call `db.get([group], 'cgcs', [list of representations], [options])`.

To begin with a simple example, let us contract two fields transforming respectively in the $\mathbf{3}$ and $\overline{\mathbf{3}}$ of $\mathit{SU}(3)$ :

```python
db.get('SU3', 'cgcs', [[1,0], [0,1]])
```

    [a[1]*b[1] + a[2]*b[2] + a[3]*b[3]]

The output is a list containing the various invariants that can be formed out of the input representations. Here, the list contains a single element, meaning that there is only one way to contract the two fields. As expected, the two fields `a` and `b` may only be contracted as $a^i b_i$.

If we now consider the four representations $\mathbf{3}$, $\overline{\mathbf{3}}$, $\mathbf{3}$ and $\overline{\mathbf{3}}$, we get two possible invariant combinations:

```python
db.get('SU3', 'cgcs', [[1,0], [0,1], [1,0], [0,1]])
```

    [a[1]*b[1]*c[1]*d[1] + a[1]*b[1]*c[2]*d[2]/2 + a[1]*b[1]*c[3]*d[3]/2 + a[1]*b[2]*c[2]*d[1]/2 + a[1]*b[3]*c[3]*d[1]/2 + a[2]*b[1]*c[1]*d[2]/2 + a[2]*b[2]*c[1]*d[1]/2 + a[2]*b[2]*c[2]*d[2] + a[2]*b[2]*c[3]*d[3]/2 + a[2]*b[3]*c[3]*d[2]/2 + a[3]*b[1]*c[1]*d[3]/2 + a[3]*b[2]*c[2]*d[3]/2 + a[3]*b[3]*c[1]*d[1]/2 + a[3]*b[3]*c[2]*d[2]/2 + a[3]*b[3]*c[3]*d[3],
     a[1]*b[1]*c[2]*d[2] + a[1]*b[1]*c[3]*d[3] - a[1]*b[2]*c[2]*d[1] - a[1]*b[3]*c[3]*d[1] - a[2]*b[1]*c[1]*d[2] + a[2]*b[2]*c[1]*d[1] + a[2]*b[2]*c[3]*d[3] - a[2]*b[3]*c[3]*d[2] - a[3]*b[1]*c[1]*d[3] - a[3]*b[2]*c[2]*d[3] + a[3]*b[3]*c[1]*d[1] + a[3]*b[3]*c[2]*d[2]]

Suppose now that we want to use the above CGCs to compute the contractions $F\times G \times F \times G$ where $F$ and $G$ are two fields transforming respectively under $\mathbf{3}$ and $\overline{\mathbf{3}}$ of $\mathit{SU}(3)$. We would need to apply the following substitutions :
$$
    a \rightarrow F \,,\\
    b \rightarrow G \,,\\
    c \rightarrow F \,,\\
    d \rightarrow G \,.
$$
To this purpose, the option `fields` may be used to indicate the name of the fields which must be substituted in place of `a`, `b`, `c` and `d`:

```python
db.get('SU3', 'cgcs', [[1,0], [0,1], [1,0], [0,1]], fields=['F', 'G', 'F', 'G'])
```

After the substitution, we see that only one non-vanishing invariant is left. This is a consequence of the transformation properties of the various invariants under the permutation group. In this case, the second invariant was in fact antisymmetric under the permutations $a \leftrightarrow c$ and $b \leftrightarrow d$.

We end up this discussion with some additional examples :

```python
# 2x2* of SU2
db.get('SU2', 'cgcs', [[1], [1, True]])
```

    [a[1]*b[1] + a[2]*b[2]]

```python
# 2x2 of SU2
db.get('SU2', 'cgcs', [[1], [1]])
```

    [a[1]*b[2] - a[2]*b[1]]

```python
# 3x3 of SU2  --  when the generators -are not- rotated to the real basis
db.get('SU2', 'cgcs', [[2], [2]])
```

    [a[1]*b[3] - a[2]*b[2] + a[3]*b[1]]

```python
# 3x3 of SU2  --  when the generators -are- rotated to the real basis
db.get('SU2', 'cgcs', [[2], [2]], realBasis=True)
```

    [a[1]*b[1] + a[2]*b[2] + a[3]*b[3]]

```python
# 10*5*10 of SU5
r10 = db.get('SU5', 'labels', 10)
r5 = db.get('SU5', 'labels', 5)

db.get('SU5', 'cgcs', [r10, r5, r10])
```

    [a[10]*b[1]*c[5] - a[10]*b[2]*c[2] + a[10]*b[3]*c[1] + a[1]*b[3]*c[10] - a[1]*b[4]*c[9] + a[1]*b[5]*c[8] - a[2]*b[2]*c[10] + a[2]*b[4]*c[7] - a[2]*b[5]*c[6] + a[3]*b[2]*c[9] - a[3]*b[3]*c[7] + a[3]*b[5]*c[5] - a[4]*b[2]*c[8] + a[4]*b[3]*c[6] - a[4]*b[4]*c[5] + a[5]*b[1]*c[10] - a[5]*b[4]*c[4] + a[5]*b[5]*c[3] - a[6]*b[1]*c[9] + a[6]*b[3]*c[4] - a[6]*b[5]*c[2] + a[7]*b[1]*c[8] - a[7]*b[3]*c[3] + a[7]*b[4]*c[2] + a[8]*b[1]*c[7] - a[8]*b[2]*c[4] + a[8]*b[5]*c[1] - a[9]*b[1]*c[6] + a[9]*b[2]*c[3] - a[9]*b[4]*c[1]]

```python
# If the database was loaded earlier using db.load(), we must close it now.

# db.close()
```


```python
import ipywidgets
```

```python
import jupyter_contrib_nbextensions
```

```python
import pyparsing
```

```python
import mkl
```

```python
import mpld3
```

```python
import scikitlearn
```

```python
import scikit-learn
```

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection

from sklearn.linear_model import LinearRegression
from sklearn.isotonic import IsotonicRegression
from sklearn.utils import check_random_state

```

```python
import sympy
```

```python
import cython
```

```python
import statsmodels
```

```python
import tqdm
```

```python
packages='''jupyter
notebook
pyparsing
matplotlib
mpld3
seaborn
pip
pandas
scipy
numpy
statsmodels
tqdm'''
```

```python
packages
```

    'jupyter\nnotebook\nipywidgets\njupyter_contrib_nbextensions\npyparsing\nmatplotlib\nmkl\nmpld3\nseaborn\npip\npandas\nscikit-learn\nscipy\nnumpy\nstatsmodels\ntqdm'

```python
!conda install -qy mkl
```

    Fetching package metadata ...........
    Solving package specifications: ..........

    # All requested packages already installed.
    # packages in environment at /opt/conda:
    #
    mkl                       2017.0.1                      0

```python
for p in  packages.split('\n') :
    print (p)
    module = __import__ (p)
```

```python
import mpld3
```

```python

```