# IUM - końcowa
### Zespół
* Kamil Przybyła (300254)
* Michał Szaknis (300274)
### Temat zadania
Są osoby, które wchodzą na naszą stronę i nie mogą się zdecydować, którym produktom przyjrzeć się nieco lepiej. Może dało by się im coś polecić?
#### Kontekst
Sklep internetowego z elektroniką i grami komputerowym, na którym użytkownik ma możliwość przeglądania aktualnej oferty sklepu oraz obejrzenia szczegółowych informacji o interesującym przedmiocie, na osobnej stronie.
#### Zadanie biznesowe
Sugerowanie użytkownikowi kilku produktów, które mogłyby go szczególnie zainteresować i wyświetlenie ich w wyróżnionej pozycji na stronie oferty sklepu.
#### Biznesowe kryterium sukcesu
Biorąc pod uwagę, że obecna średnia dzienna liczba wyświetleń produktów wynosi około $1654$, przyjmujemy że system powinien generować $150$ nowych wyświetleń produktów.
#### Zadanie modelowania
Przygotowanie systemu rekomendacyjnego, na podstawie dostarczanych logów sklepu (informacje o sesji użytkownika - w szczególności zarejestrowane zdarzenia wyświetlenia produktu, dane produktów, dane użytkowników).
#### Analityczne kryterium sukcesu
$$
\frac{\mbox{liczba trafnych rekomendacji}}{\mbox{liczba wszystkich rekomendacji}} \gt \frac{150}{\mbox{dzienna liczba wyświetleń strony oferty sklepu}}
$$
#### Przyjęte założenia
* format logów sklepu pozostanie niezmienny w czasie funkcjonowania systemu
* model będzie aktualizowany wraz z ofertą sklepu
* strona zawiera miejsce na rekomendowany produkt
## Modele rekomendacji
W celu realizacji zadania modelowania, sporządzono dwa modele oraz przygotowano system, pozwalający na wykonanie testów A/B oraz porównanie ich skuteczności.
### Bestsellery
Model ten sugeruje użytkownikowi wybraną liczbę towarów, które były najchętniej oglądane lub kupowane przez klientów sklepu, spośród tych, które należą do tej samej kategorii towarów co ostatnio oglądany przez użytkownika produkt.
Do sporządzenia tego modelu wykorzystano bazę produktów oraz informacji o sesjach użytkownika. Wydarzenia zapisane w sesji są grupowane po kategoriach produktów z którymi dokonano interakcji (zakup lub wyświetlenie). Zliczana jest liczba wystąpień danego produktu w obrębie swojej kategorii. Wynik sortowany jest malejąco.
Na etapie rekomendacji, pobierana jest kategoria produktu ostatnio oglądanego i system rekomenduje pierwsze $n$ produktów z posortowanej listy.
```python
class BestsellersModel(AIModel):
def __init__(self):
pass
def train(self, dataset: Type[DataSet]) -> Type[None]:
sessions = dataset.sessions
self.products = dataset.products
self.products['last_category'] = self.products['category_path'].transform(lambda row: row.split(';')[-1])
merged = pd.merge(sessions, self.products, on='product_id')
self.bestsellers = merged.groupby(['last_category', 'product_id']).count()['session_id'].sort_values(ascending=False)
def recommend(self, user_id: int, req: Type[RecommendationRequest]) -> List[int]:
last_product_id = req.product_ids[-1]
category = self.products[self.products['product_id'] == last_product_id]['last_category'].values[0]
recommendation_list = list(self.bestsellers[category].head(req.results_n).values)
return recommendation_list
```
### K-najbliższych sąsiadów
Implementacja tego modelu została zaczerpnięta z biblioteki *scikit-learn*. Model trenowany jest na podstawie macierzy wyświetleń danych produktów przez każdego z użytkowników. Podczas rekomendacji, znajdywanych jest $n$ najbardziej podobnych produktów do tego ostatnio przeglądanego przez użytkownika, któremu rekomendujemy produkty.
```python
class KNNModel(AIModel):
def __init__(self, settings):
self.model = NearestNeighbors()
self.model.set_params(**settings)
def train(self, dataset: Type[DataSet]) -> Type[None]:
sessions = dataset.sessions
self.rating_matrix = sessions.loc[ sessions['event_type'] == 'VIEW_PRODUCT' ] \
.groupby(['user_id', 'product_id']).count().loc[:, ['session_id']].reset_index() \
.pivot(index='product_id', columns='user_id', values='session_id') \
.fillna(0)
self.index_to_product_mapping = self.rating_matrix.index.to_frame().reset_index(drop=True)['product_id'].to_dict()
self.product_to_index_mapping = { j:i for i,j in self.index_to_product_mapping.items() }
self.csr_rating_matrix = csr_matrix(self.rating_matrix)
self.model.fit(self.csr_rating_matrix)
def recommend(self, user_id: int, req: Type[RecommendationRequest]) -> List[int]:
last_product_id = req.product_ids[-1]
last_product_index = self.product_to_index_mapping[last_product_id]
last_product = self.csr_rating_matrix[last_product_index]
distances, indices = self.model.kneighbors(last_product, req.results_n + 1)
response = sorted(zip(indices.squeeze().tolist(), distances.squeeze().tolist()), key = lambda x: x[1])
recommendation_list = list(map(lambda x: self.index_to_product_mapping[x[0]], response))
recommendation_list.remove(last_product_id)
return recommendation_list
```
## Testy A/B
System rekomendacji może działać w trybie eksperymentu lub korzystać z jednego modelu. Tryb eksperymentu przed rekomendacją przypisuje użytkownikowi, któremu mają zostać zarekomendowane produkty jeden z dwóch wybranych modeli. Model ten jest potem wykorzystywany do sporządzenia rekomendacji.
Tryb jednego modelu zawsze korzysta z wybranego modelu, bez względu na identyfikator użytkownika dla którego sporządza się rekomendację.
### Zbierane dane
Każde żądanie rekomendacji i jego rezultat w postaci listy poleconych produktów zapisywany jest bazie systemu. Wpis ten, poza listą produktów, zawiera identyfikator użytkownika oraz wykorzystany model rekomendacji. Informacje te, w połączeniu z danymi dotyczącymi sesji użytkownika, pozwolą na porównanie skuteczności obu modelu. Można bowiem zliczyć jaka część zarekomendowanych produktów dla poszczególnego modelu została później wyświetlona. Model, który częściej rekomenduje produkty chętniej wyświetlane przez użytkowników może potem zostać wdrożony dla każdego użytkownika.
```python
async def _log_recommendation(self, model_name: str, user_id: int, recommendation: List[int]):
log_item: Type[LogItem] = LogItem(user_id=user_id, product_ids=recommendation, model_name=model_name)
await self.engine.save(log_item)
```
## Testy
W celu wstępnego sprawdzenia dokładności modeli, sporządzono testy na podstawie historii sesji użytkowników. Baza sesji dzielona jest na dwie części (np. 90% i 10%) z których pierwsza wykorzystywana jest do trenowania modelu, a druga do testowania dokładności. W czasie testowania model dostaje na wejściu informacje o ostatnio oglądanym produkcie i sprawdzamy czy model zarekomenduje ten sam produkt, który użytkownik następnie wyświetlił.
```
curl -X 'POST' \
'http://localhost:5000/test' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"models": [
"KNNModel",
"BestSellersModel"
],
"testing_set_ratio": 0.9
}'
```
Wynik:
```
{
"KNNModel": 0.032862826312060604,
"BestSellersModel": 0.08790669791269279
}
```
Jak widać modele nie wypadają w tym teście zbyt dobrze, a mniej złożony model osiąga lepsze wyniki.
## API mikroserwisu
Dokumentacja API znajduje się w osobnym pliku.
## Pokaz działania
__Włączenie trybu eksperymentu:__

```
curl -X 'POST' \
'http://localhost:5000/mode/expermient/names' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '[
"KNNModel",
"BestSellersModel"
]'
```
__Włączony tryb eksperymentu:__

```
curl -X 'GET' \
'http://localhost:5000/mode' \
-H 'accept: application/json'
```
__Przykładowe rekomendacje:__

```
curl -X 'POST' \
'http://localhost:5000/user/105/recommendation' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_id": 105,
"product_ids": [
1077
],
"results_n": 5
}'
```

```
curl -X 'POST' \
'http://localhost:5000/user/154/recommendation' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_id": 154,
"product_ids": [
1077
],
"results_n": 5
}'
```

```
curl -X 'POST' \
'http://localhost:5000/user/123/recommendation' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_id": 123,
"product_ids": [
1021
],
"results_n": 3
}'
```

```
curl -X 'POST' \
'http://localhost:5000/user/155/recommendation' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user_id": 155,
"product_ids": [
1021
],
"results_n": 3
}'
```
__Log rekomendacji z mongodb do późniejszej analizy__

```
curl -X 'GET' \
'http://localhost:5000/log' \
-H 'accept: application/json'
```
## Porównanie rekomendacji
### Bestsellers
1. Product id: 1044 - Assassin's Creed 3 (PS3) Gry PlayStation3
* Product id: 1043 - Fight Night Champion (PS3)
* Product id: 1084 - Sing It High School Musical 3 (PS3)
* Product id: 1047 - Pirates of the Caribbean At World's End (PS3)
* Product id: 1040 - Crysis 2 (PS3)
* Product id: 1044 - Assassin's Creed 3 (PS3)
2. Product id: 1076 - Samsung CLX-6260FR
* Product id: 1077 - Kyocera FS-C2026MFP
* Product id: 1080 - Kyocera FS-3140MFP
* Product id: 1078 - Kyocera FS-3540MFP
* Product id: 1003 - Kyocera FS-3640MFP
* Product id: 1079 - Kyocera FS-3040MFP
### Model KNN
1. Produkt id: 1044 - Assassin's Creed 3 (PS3) Gry PlayStation3
* 1040 - Crysis 2 (PS3)
* 1041 - Crysis 3 (PS3)
* 1047 - Pirates of the Caribbean At World's End (PS3)
* 1042 - GTA 5 (PS3)
* 1046 - LEGO Batman 2 DC Super Heroes (PS3)
2. Product id: 1076 - Samsung CLX-6260FR
* Product id: 1078 - Kyocera FS-3540MFP
* Product id: 1002 - Kyocera FS-1135MFP
* Product id: 1079 - Kyocera FS-3040MFP
* Product id: 1080 - Kyocera FS-3140MFP
* Product id: 1075 - Ricoh SG3110DN
### Komentarz
W przypadku rekomendacji produktów dla użytkownika, który ostatnio przeglądał grę, wydaje się, że model KNN wypada lepiej, gdyż rekomenduje gry podobnego gatunku. Model _Bestsellers_ polecił grę zupełnie inną niż ta ostatnio przeglądana.
Kiedy ostatnim oglądanym produktem była zaś drukarka, oba modele dawały podobnie adekwatnie rekomendacje.